Compare commits
	
		
			188 Commits
		
	
	
		
			status-api
			...
			imagery-en
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | eb97e94cd6 | ||
|   | e7b8c42f02 | ||
|   | 3810b6c441 | ||
|   | 1499286bee | ||
|   | 2c62b4c1bc | ||
|   | ca47fb6f2d | ||
|   | 5d4d87cd89 | ||
|   | 6226763c37 | ||
|   | 7623a0648f | ||
|   | f32602343d | ||
|   | bf82abd464 | ||
|   | ab31581ea4 | ||
|   | 35bad9cb82 | ||
|   | 03a104c9f5 | ||
|   | b7085f7f62 | ||
|   | 99ace5ec9b | ||
|   | 55c851873c | ||
|   | 2b143dfc0f | ||
|   | 93785544f1 | ||
|   | 9405272f3b | ||
|   | a9be9f1827 | ||
|   | abb1a5c75b | ||
|   | ed0095fc00 | ||
|   | 5e2fe7dc42 | ||
|   | 63cf6e8156 | ||
|   | e8600d23e1 | ||
|   | 36e720ad85 | ||
|   | 46e926aa08 | ||
|   | 429a628c92 | ||
|   | 03e1229576 | ||
|   | 05c8a8a2f0 | ||
|   | 7a7ec7c9b7 | ||
|   | b5fcda3107 | ||
|   | ab4e770b79 | ||
|   | 3f140de03a | ||
|   | 9ee6cca07d | ||
|   | 54182e400a | ||
|   | 863533910e | ||
|   | e4d6e90c35 | ||
|   | edbdf432d1 | ||
|   | 35256b6e96 | ||
|   | 375bbd244e | ||
|   | 8090e27b7b | ||
|   | 275410f99c | ||
|   | 31ab08c9d3 | ||
|   | 082a89440e | ||
|   | c729732541 | ||
|   | 8d3737912b | ||
|   | d6a71adb7f | ||
|   | 8397b13c57 | ||
|   | 6a4ceb5219 | ||
|   | 84d9a525a9 | ||
|   | c25b196b8f | ||
|   | cd5cc4c76c | ||
|   | f8b818e78b | ||
|   | 6cea1a77e5 | ||
|   | 0aca0ce6a6 | ||
|   | 88ff09857b | ||
|   | b409d3cb1e | ||
|   | a64e3e5ca0 | ||
|   | d6ba2f8b4c | ||
|   | 4f1642a8d6 | ||
|   | 9b73b45ba9 | ||
|   | 98a048062f | ||
|   | 9b114c49df | ||
|   | 2c0c998e29 | ||
|   | a001e07600 | ||
|   | 157564487d | ||
|   | fcbd8c682a | ||
|   | 4b40233bf3 | ||
|   | fa2197f9c1 | ||
|   | f3f833a337 | ||
|   | e6e8b8e048 | ||
|   | 6a2c079336 | ||
|   | 0d23fe3d14 | ||
|   | 1d645a8472 | ||
|   | 334aeb42ae | ||
|   | 5900bb0d98 | ||
|   | a2c350b105 | ||
|   | 174f212328 | ||
|   | a28ec45f71 | ||
|   | fcc6bb9873 | ||
|   | 45578b113f | ||
|   | 5ef14b0975 | ||
|   | 2be429a04f | ||
|   | c4ce405b1e | ||
|   | b95f844a4e | ||
|   | 3804fe1a1e | ||
|   | 7576673e77 | ||
|   | 63e04caab6 | ||
|   | 956cfbd01f | ||
|   | 6c77be32c7 | ||
|   | af4c7c9ca0 | ||
|   | c0742d521c | ||
|   | 1697362994 | ||
|   | e69911385f | ||
|   | 6478267cbe | ||
|   | 22d53c1ccd | ||
|   | 633a95dd27 | ||
|   | f732167e02 | ||
|   | 50ff26ad5d | ||
|   | f056e8e57b | ||
|   | 1d56fd98dc | ||
|   | 320217f8c4 | ||
|   | b43fef6e21 | ||
|   | d04c29345b | ||
|   | 49afec5cdd | ||
|   | 24b96cdb47 | ||
|   | 14ce4a1aa0 | ||
|   | 43a8901c34 | ||
|   | 28d97be60e | ||
|   | 4bb2b35124 | ||
|   | 1f6e91c6b5 | ||
|   | 0b078497f1 | ||
|   | 060a1b17db | ||
|   | db50b8b732 | ||
|   | 62de05808e | ||
|   | 417f81b7fd | ||
|   | f219394abd | ||
|   | 4e5c74ecef | ||
|   | 218530e436 | ||
|   | 0890499a2b | ||
|   | d9dad09dfd | ||
|   | 9af5df0f20 | ||
|   | 1580a61092 | ||
|   | c236444a05 | ||
|   | 39c1eb1d5b | ||
|   | 92737b43af | ||
|   | 95caab944d | ||
|   | ac89e51d1b | ||
|   | 85d9ed8287 | ||
|   | aedc24a2da | ||
|   | 8b0f6885ee | ||
|   | 823eda4465 | ||
|   | 7eaa1d3e2b | ||
|   | 9af2d15cef | ||
|   | 4633436cbd | ||
|   | e60d8d08a4 | ||
|   | b68a7e27c9 | ||
|   | 3e9b567fce | ||
|   | 6f51de85db | ||
|   | f202ae19cb | ||
|   | 668bd75025 | ||
|   | e6e07cf959 | ||
|   | 2f8431905f | ||
|   | 23aba14dfe | ||
|   | b0fa955914 | ||
|   | 98207a3e0d | ||
|   | 26b81345f2 | ||
|   | ac2034b243 | ||
|   | 351848ad56 | ||
|   | cbac495f93 | ||
|   | 15ef5b7623 | ||
|   | 46c7ac944f | ||
|   | aa4bfab462 | ||
|   | f5cbb37e5a | ||
|   | 8d9079984a | ||
|   | 41783d8939 | ||
|   | 441ad58fe7 | ||
|   | 06a6a3f773 | ||
|   | 52fab78625 | ||
|   | 5eb6c15959 | ||
|   | ce8c31cfa4 | ||
|   | d80c0eef8e | ||
|   | 55829dcf05 | ||
|   | d78956327c | ||
|   | 4a0728a55b | ||
|   | 2e1d57aa8c | ||
|   | 1c2b0678be | ||
|   | 6f810add43 | ||
|   | 12727adb16 | ||
|   | 9da750c3bb | ||
|   | 176226ddef | ||
|   | acea18fa70 | ||
|   | d1656f8561 | ||
|   | 87751e882c | ||
|   | dff393a714 | ||
|   | fd9c9aee03 | ||
|   | 59bf981fb0 | ||
|   | 4bbdac759f | ||
|   | 13fe7509de | ||
|   | 6fd8f6cd43 | ||
|   | 6375ecda34 | ||
|   | d232dacc65 | ||
|   | 59946e89ef | ||
|   | d75c4b4049 | ||
|   | 30ca4b707d | ||
|   | 27704c9a48 | 
							
								
								
									
										20
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.eslintrc.js
									
									
									
									
									
								
							| @@ -54,7 +54,7 @@ module.exports = { | ||||
|             { | ||||
|                 "anonymous": "always", | ||||
|                 "asyncArrow": "always", | ||||
|                 "named": "never", | ||||
|                 "named": "never" | ||||
|             } | ||||
|         ], | ||||
|         "array-bracket-spacing": "error", | ||||
| @@ -178,7 +178,10 @@ module.exports = { | ||||
|         //https://eslint.org/docs/rules/no-whitespace-before-property | ||||
|         "no-whitespace-before-property": "error", | ||||
|         // https://eslint.org/docs/rules/object-curly-newline | ||||
|         "object-curly-newline": ["error", {"consistent": true, "multiline": true}], | ||||
|         "object-curly-newline": ["error", { | ||||
|             "consistent": true, | ||||
|             "multiline": true | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/object-property-newline | ||||
|         "object-property-newline": "error", | ||||
|         // https://eslint.org/docs/rules/brace-style | ||||
| @@ -188,7 +191,7 @@ module.exports = { | ||||
|         // https://eslint.org/docs/rules/operator-linebreak | ||||
|         "operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}], | ||||
|         // https://eslint.org/docs/rules/padding-line-between-statements | ||||
|         "padding-line-between-statements":["error", { | ||||
|         "padding-line-between-statements": ["error", { | ||||
|             "blankLine": "always", | ||||
|             "prev": "multiline-block-like", | ||||
|             "next": "*" | ||||
| @@ -200,11 +203,17 @@ module.exports = { | ||||
|         // https://eslint.org/docs/rules/space-infix-ops | ||||
|         "space-infix-ops": "error", | ||||
|         // https://eslint.org/docs/rules/space-unary-ops | ||||
|         "space-unary-ops": ["error", {"words": true, "nonwords": false}], | ||||
|         "space-unary-ops": ["error", { | ||||
|             "words": true, | ||||
|             "nonwords": false | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/arrow-spacing | ||||
|         "arrow-spacing": "error", | ||||
|         // https://eslint.org/docs/rules/semi-spacing | ||||
|         "semi-spacing": ["error", {"before": false, "after": true}], | ||||
|         "semi-spacing": ["error", { | ||||
|             "before": false, | ||||
|             "after": true | ||||
|         }], | ||||
|  | ||||
|         "vue/html-indent": [ | ||||
|             "error", | ||||
| @@ -237,6 +246,7 @@ module.exports = { | ||||
|         }], | ||||
|         "vue/multiline-html-element-content-newline": "off", | ||||
|         "vue/singleline-html-element-content-newline": "off", | ||||
|         "vue/no-mutating-props": "off" | ||||
|  | ||||
|     }, | ||||
|     "overrides": [ | ||||
|   | ||||
| @@ -182,7 +182,7 @@ The following guidelines are provided for anyone contributing source code to the | ||||
| 1. Avoid the use of "magic" values. | ||||
|    eg. | ||||
|    ```JavaScript | ||||
|    Const UNAUTHORIZED = 401 | ||||
|    const UNAUTHORIZED = 401; | ||||
|    if (responseCode === UNAUTHORIZED) | ||||
|    ``` | ||||
|    is preferable to | ||||
|   | ||||
| @@ -138,7 +138,7 @@ define([ | ||||
|                     "id": "styleguide:home", | ||||
|                     "priority": "preferred", | ||||
|                     "model": { | ||||
|                         "type": "folder", | ||||
|                         "type": "noneditable.folder", | ||||
|                         "name": "Style Guide Home", | ||||
|                         "location": "ROOT", | ||||
|                         "composition": [ | ||||
| @@ -155,7 +155,7 @@ define([ | ||||
|                     "id": "styleguide:ui-elements", | ||||
|                     "priority": "preferred", | ||||
|                     "model": { | ||||
|                         "type": "folder", | ||||
|                         "type": "noneditable.folder", | ||||
|                         "name": "UI Elements", | ||||
|                         "location": "styleguide:home", | ||||
|                         "composition": [ | ||||
|   | ||||
| @@ -131,10 +131,10 @@ | ||||
|                         } | ||||
|                     ], | ||||
|                     // maximum recent bounds to retain in conductor history | ||||
|                     records: 10, | ||||
|                     records: 10 | ||||
|                     // maximum duration between start and end bounds | ||||
|                     // for utc-based time systems this is in milliseconds | ||||
|                     limit: ONE_DAY | ||||
|                     // limit: ONE_DAY | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Realtime", | ||||
|   | ||||
| @@ -86,7 +86,7 @@ module.exports = (config) => { | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 65 | ||||
|                     lines: 66 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.4.1-SNAPSHOT", | ||||
|   "version": "1.6.2-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
| @@ -23,7 +23,7 @@ | ||||
|     "d3-time": "1.0.x", | ||||
|     "d3-time-format": "2.1.x", | ||||
|     "eslint": "7.0.0", | ||||
|     "eslint-plugin-vue": "^6.0.0", | ||||
|     "eslint-plugin-vue": "^7.5.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0", | ||||
|     "eventemitter3": "^1.2.0", | ||||
|     "exports-loader": "^0.7.0", | ||||
|   | ||||
| @@ -143,8 +143,8 @@ define([ | ||||
|                             "$window" | ||||
|                         ], | ||||
|                         "group": "windowing", | ||||
|                         "cssClass": "icon-new-window", | ||||
|                         "priority": "preferred" | ||||
|                         "priority": 10, | ||||
|                         "cssClass": "icon-new-window" | ||||
|                     } | ||||
|                 ], | ||||
|                 "runs": [ | ||||
|   | ||||
| @@ -139,7 +139,9 @@ define([ | ||||
|                         ], | ||||
|                         "description": "Edit", | ||||
|                         "category": "view-control", | ||||
|                         "cssClass": "major icon-pencil" | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "group": "action", | ||||
|                         "priority": 10 | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "properties", | ||||
| @@ -150,6 +152,8 @@ define([ | ||||
|                         "implementation": PropertiesAction, | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "name": "Edit Properties...", | ||||
|                         "group": "action", | ||||
|                         "priority": 10, | ||||
|                         "description": "Edit properties of this object.", | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|   | ||||
| @@ -44,9 +44,9 @@ define( | ||||
|             // is also invoked during the create process which should be allowed, | ||||
|             // because it may be saved elsewhere | ||||
|             if ((key === 'edit' && category === 'view-control') || key === 'properties') { | ||||
|                 let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId()); | ||||
|                 let identifier = this.openmct.objects.parseKeyString(domainObject.getId()); | ||||
|  | ||||
|                 return this.openmct.objects.isPersistable(newStyleObject); | ||||
|                 return this.openmct.objects.isPersistable(identifier); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|   | ||||
| @@ -43,7 +43,8 @@ define( | ||||
|                 ); | ||||
|  | ||||
|                 mockObjectAPI = jasmine.createSpyObj('objectAPI', [ | ||||
|                     'isPersistable' | ||||
|                     'isPersistable', | ||||
|                     'parseKeyString' | ||||
|                 ]); | ||||
|  | ||||
|                 mockAPI = { | ||||
|   | ||||
| @@ -20,12 +20,12 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="c-object-label" | ||||
|      ng-class="{ 'is-missing': model.status === 'missing' }" | ||||
|      ng-class="{ 'is-status--missing': model.status === 'missing' }" | ||||
| > | ||||
|     <div class="c-object-label__type-icon {{type.getCssClass()}}" | ||||
|          ng-class="{ 'l-icon-link':location.isLink() }" | ||||
|     > | ||||
|         <span class="is-missing__indicator" title="This item is missing"></span> | ||||
|         <span class="is-status__indicator" title="This item is missing or suspect"></span> | ||||
|     </div> | ||||
|     <div class='c-object-label__name'>{{model.name}}</div> | ||||
| </div> | ||||
|   | ||||
| @@ -48,9 +48,9 @@ define( | ||||
|             // prevents editing of objects that cannot be persisted, so we can assume that this | ||||
|             // is a new object. | ||||
|             if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) { | ||||
|                 let newStyleObject = objectUtils.toNewFormat(parent, parent.getId()); | ||||
|                 let identifier = this.openmct.objects.parseKeyString(parent.getId()); | ||||
|  | ||||
|                 return this.openmct.objects.isPersistable(newStyleObject); | ||||
|                 return this.openmct.objects.isPersistable(identifier); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|   | ||||
| @@ -33,7 +33,8 @@ define( | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 objectAPI = jasmine.createSpyObj('objectsAPI', [ | ||||
|                     'isPersistable' | ||||
|                     'isPersistable', | ||||
|                     'parseKeyString' | ||||
|                 ]); | ||||
|  | ||||
|                 mockOpenMCT = { | ||||
|   | ||||
| @@ -21,32 +21,24 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     "./src/actions/MoveAction", | ||||
|     "./src/actions/CopyAction", | ||||
|     "./src/actions/LinkAction", | ||||
|     "./src/actions/SetPrimaryLocationAction", | ||||
|     "./src/services/LocatingCreationDecorator", | ||||
|     "./src/services/LocatingObjectDecorator", | ||||
|     "./src/policies/CopyPolicy", | ||||
|     "./src/policies/CrossSpacePolicy", | ||||
|     "./src/policies/MovePolicy", | ||||
|     "./src/capabilities/LocationCapability", | ||||
|     "./src/services/MoveService", | ||||
|     "./src/services/LinkService", | ||||
|     "./src/services/CopyService", | ||||
|     "./src/services/LocationService" | ||||
| ], function ( | ||||
|     MoveAction, | ||||
|     CopyAction, | ||||
|     LinkAction, | ||||
|     SetPrimaryLocationAction, | ||||
|     LocatingCreationDecorator, | ||||
|     LocatingObjectDecorator, | ||||
|     CopyPolicy, | ||||
|     CrossSpacePolicy, | ||||
|     MovePolicy, | ||||
|     LocationCapability, | ||||
|     MoveService, | ||||
|     LinkService, | ||||
|     CopyService, | ||||
|     LocationService | ||||
| @@ -60,41 +52,14 @@ define([ | ||||
|             "configuration": {}, | ||||
|             "extensions": { | ||||
|                 "actions": [ | ||||
|                     { | ||||
|                         "key": "move", | ||||
|                         "name": "Move", | ||||
|                         "description": "Move object to another location.", | ||||
|                         "cssClass": "icon-move", | ||||
|                         "category": "contextual", | ||||
|                         "implementation": MoveAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
|                             "locationService", | ||||
|                             "moveService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "copy", | ||||
|                         "name": "Duplicate", | ||||
|                         "description": "Duplicate object to another location.", | ||||
|                         "cssClass": "icon-duplicate", | ||||
|                         "category": "contextual", | ||||
|                         "implementation": CopyAction, | ||||
|                         "depends": [ | ||||
|                             "$log", | ||||
|                             "policyService", | ||||
|                             "locationService", | ||||
|                             "copyService", | ||||
|                             "dialogService", | ||||
|                             "notificationService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "link", | ||||
|                         "name": "Create Link", | ||||
|                         "description": "Create Link to object in another location.", | ||||
|                         "cssClass": "icon-link", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 7, | ||||
|                         "implementation": LinkAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
| @@ -135,10 +100,6 @@ define([ | ||||
|                     { | ||||
|                         "category": "action", | ||||
|                         "implementation": CopyPolicy | ||||
|                     }, | ||||
|                     { | ||||
|                         "category": "action", | ||||
|                         "implementation": MovePolicy | ||||
|                     } | ||||
|                 ], | ||||
|                 "capabilities": [ | ||||
| @@ -154,17 +115,6 @@ define([ | ||||
|                     } | ||||
|                 ], | ||||
|                 "services": [ | ||||
|                     { | ||||
|                         "key": "moveService", | ||||
|                         "name": "Move Service", | ||||
|                         "description": "Provides a service for moving objects", | ||||
|                         "implementation": MoveService, | ||||
|                         "depends": [ | ||||
|                             "openmct", | ||||
|                             "linkService", | ||||
|                             "$q" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "linkService", | ||||
|                         "name": "Link Service", | ||||
|   | ||||
| @@ -1,168 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     ['./AbstractComposeAction', './CancelError'], | ||||
|     function (AbstractComposeAction, CancelError) { | ||||
|  | ||||
|         /** | ||||
|          * The CopyAction is available from context menus and allows a user to | ||||
|          * deep copy an object to another location of their choosing. | ||||
|          * | ||||
|          * @implements {Action} | ||||
|          * @constructor | ||||
|          * @memberof platform/entanglement | ||||
|          */ | ||||
|         function CopyAction( | ||||
|             $log, | ||||
|             policyService, | ||||
|             locationService, | ||||
|             copyService, | ||||
|             dialogService, | ||||
|             notificationService, | ||||
|             context | ||||
|         ) { | ||||
|             this.dialog = undefined; | ||||
|             this.notification = undefined; | ||||
|             this.dialogService = dialogService; | ||||
|             this.notificationService = notificationService; | ||||
|             this.$log = $log; | ||||
|             //Extend the behaviour of the Abstract Compose Action | ||||
|             AbstractComposeAction.call( | ||||
|                 this, | ||||
|                 policyService, | ||||
|                 locationService, | ||||
|                 copyService, | ||||
|                 context, | ||||
|                 "Duplicate", | ||||
|                 "To a Location" | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         CopyAction.prototype = Object.create(AbstractComposeAction.prototype); | ||||
|  | ||||
|         /** | ||||
|          * Updates user about progress of copy. Should not be invoked by | ||||
|          * client code under any circumstances. | ||||
|          * | ||||
|          * @private | ||||
|          * @param phase | ||||
|          * @param totalObjects | ||||
|          * @param processed | ||||
|          */ | ||||
|         CopyAction.prototype.progress = function (phase, totalObjects, processed) { | ||||
|             /* | ||||
|              Copy has two distinct phases. In the first phase a copy plan is | ||||
|              made in memory. During this phase of execution, the user is | ||||
|              shown a blocking 'modal' dialog. | ||||
|  | ||||
|              In the second phase, the copying is taking place, and the user | ||||
|              is shown non-invasive banner notifications at the bottom of the screen. | ||||
|              */ | ||||
|             if (phase.toLowerCase() === 'preparing' && !this.dialog) { | ||||
|                 this.dialog = this.dialogService.showBlockingMessage({ | ||||
|                     title: "Preparing to copy objects", | ||||
|                     hint: "Do not navigate away from this page or close this browser tab while this message is displayed.", | ||||
|                     unknownProgress: true, | ||||
|                     severity: "info" | ||||
|                 }); | ||||
|             } else if (phase.toLowerCase() === "copying") { | ||||
|                 if (this.dialog) { | ||||
|                     this.dialog.dismiss(); | ||||
|                 } | ||||
|  | ||||
|                 if (!this.notification) { | ||||
|                     this.notification = this.notificationService | ||||
|                         .notify({ | ||||
|                             title: "Copying objects", | ||||
|                             unknownProgress: false, | ||||
|                             severity: "info" | ||||
|                         }); | ||||
|                 } | ||||
|  | ||||
|                 this.notification.model.progress = (processed / totalObjects) * 100; | ||||
|                 this.notification.model.title = ["Copied ", processed, "of ", | ||||
|                     totalObjects, "objects"].join(" "); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Executes the CopyAction. The CopyAction uses the default behaviour of | ||||
|          * the AbstractComposeAction, but extends it to support notification | ||||
|          * updates of progress on copy. | ||||
|          */ | ||||
|         CopyAction.prototype.perform = function () { | ||||
|             var self = this; | ||||
|  | ||||
|             function success(domainObject) { | ||||
|                 var domainObjectName = domainObject.model.name; | ||||
|  | ||||
|                 self.notification.dismiss(); | ||||
|                 self.notificationService.info(domainObjectName + " copied successfully."); | ||||
|             } | ||||
|  | ||||
|             function error(errorDetails) { | ||||
|                 // No need to notify user of their own cancellation | ||||
|                 if (errorDetails instanceof CancelError) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 var errorDialog, | ||||
|                     errorMessage = { | ||||
|                         title: "Error copying objects.", | ||||
|                         severity: "error", | ||||
|                         hint: errorDetails.message, | ||||
|                         minimized: true, // want the notification to be minimized initially (don't show banner) | ||||
|                         options: [{ | ||||
|                             label: "OK", | ||||
|                             callback: function () { | ||||
|                                 errorDialog.dismiss(); | ||||
|                             } | ||||
|                         }] | ||||
|                     }; | ||||
|  | ||||
|                 self.dialog.dismiss(); | ||||
|                 if (self.notification) { | ||||
|                     self.notification.dismiss(); // Clear the progress notification | ||||
|                 } | ||||
|  | ||||
|                 self.$log.error("Error copying objects. ", errorDetails); | ||||
|                 //Show a minimized notification of error for posterity | ||||
|                 self.notificationService.notify(errorMessage); | ||||
|                 //Display a blocking message | ||||
|                 errorDialog = self.dialogService.showBlockingMessage(errorMessage); | ||||
|  | ||||
|             } | ||||
|  | ||||
|             function notification(details) { | ||||
|                 self.progress(details.phase, details.totalObjects, details.processed); | ||||
|             } | ||||
|  | ||||
|             return AbstractComposeAction.prototype.perform.call(this) | ||||
|                 .then(success, error, notification); | ||||
|         }; | ||||
|  | ||||
|         CopyAction.appliesTo = AbstractComposeAction.appliesTo; | ||||
|  | ||||
|         return CopyAction; | ||||
|     } | ||||
| ); | ||||
| @@ -1,104 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     function () { | ||||
|         /** | ||||
|          * MoveService provides an interface for moving objects from one | ||||
|          * location to another.  It also provides a method for determining if | ||||
|          * an object can be copied to a specific location. | ||||
|          * @constructor | ||||
|          * @memberof platform/entanglement | ||||
|          * @implements {platform/entanglement.AbstractComposeService} | ||||
|          */ | ||||
|         function MoveService(openmct, linkService) { | ||||
|             this.openmct = openmct; | ||||
|             this.linkService = linkService; | ||||
|         } | ||||
|  | ||||
|         MoveService.prototype.validate = function (object, parentCandidate) { | ||||
|             var currentParent = object | ||||
|                 .getCapability('context') | ||||
|                 .getParent(); | ||||
|  | ||||
|             if (!parentCandidate || !parentCandidate.getId) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidate.getId() === currentParent.getId()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidate.getId() === object.getId()) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return this.openmct.composition.checkPolicy( | ||||
|                 parentCandidate.useCapability('adapter'), | ||||
|                 object.useCapability('adapter') | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|         MoveService.prototype.perform = function (object, parentObject) { | ||||
|             function relocate(objectInNewContext) { | ||||
|                 var newLocationCapability = objectInNewContext | ||||
|                         .getCapability('location'), | ||||
|                     oldLocationCapability = object | ||||
|                         .getCapability('location'); | ||||
|  | ||||
|                 if (!newLocationCapability | ||||
|                         || !oldLocationCapability) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (oldLocationCapability.isOriginal()) { | ||||
|                     return newLocationCapability.setPrimaryLocation( | ||||
|                         newLocationCapability | ||||
|                             .getContextualLocation() | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!this.validate(object, parentObject)) { | ||||
|                 throw new Error( | ||||
|                     "Tried to move objects without validating first." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             return this.linkService | ||||
|                 .perform(object, parentObject) | ||||
|                 .then(relocate) | ||||
|                 .then(function () { | ||||
|                     return object | ||||
|                         .getCapability('action') | ||||
|                         .perform('remove', true); | ||||
|                 }); | ||||
|         }; | ||||
|  | ||||
|         return MoveService; | ||||
|     } | ||||
| ); | ||||
|  | ||||
| @@ -1,243 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../../src/actions/CopyAction', | ||||
|         '../services/MockCopyService', | ||||
|         '../DomainObjectFactory' | ||||
|     ], | ||||
|     function (CopyAction, MockCopyService, domainObjectFactory) { | ||||
|  | ||||
|         describe("Copy Action", function () { | ||||
|  | ||||
|             var copyAction, | ||||
|                 policyService, | ||||
|                 locationService, | ||||
|                 locationServicePromise, | ||||
|                 copyService, | ||||
|                 context, | ||||
|                 selectedObject, | ||||
|                 selectedObjectContextCapability, | ||||
|                 currentParent, | ||||
|                 newParent, | ||||
|                 notificationService, | ||||
|                 notification, | ||||
|                 dialogService, | ||||
|                 mockDialog, | ||||
|                 mockLog, | ||||
|                 abstractComposePromise, | ||||
|                 domainObject = {model: {name: "mockObject"}}, | ||||
|                 progress = { | ||||
|                     phase: "copying", | ||||
|                     totalObjects: 10, | ||||
|                     processed: 1 | ||||
|                 }; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 policyService = jasmine.createSpyObj( | ||||
|                     'policyService', | ||||
|                     ['allow'] | ||||
|                 ); | ||||
|                 policyService.allow.and.returnValue(true); | ||||
|  | ||||
|                 selectedObjectContextCapability = jasmine.createSpyObj( | ||||
|                     'selectedObjectContextCapability', | ||||
|                     [ | ||||
|                         'getParent' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 selectedObject = domainObjectFactory({ | ||||
|                     name: 'selectedObject', | ||||
|                     model: { | ||||
|                         name: 'selectedObject' | ||||
|                     }, | ||||
|                     capabilities: { | ||||
|                         context: selectedObjectContextCapability | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 currentParent = domainObjectFactory({ | ||||
|                     name: 'currentParent' | ||||
|                 }); | ||||
|  | ||||
|                 selectedObjectContextCapability | ||||
|                     .getParent | ||||
|                     .and.returnValue(currentParent); | ||||
|  | ||||
|                 newParent = domainObjectFactory({ | ||||
|                     name: 'newParent' | ||||
|                 }); | ||||
|  | ||||
|                 locationService = jasmine.createSpyObj( | ||||
|                     'locationService', | ||||
|                     [ | ||||
|                         'getLocationFromUser' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 locationServicePromise = jasmine.createSpyObj( | ||||
|                     'locationServicePromise', | ||||
|                     [ | ||||
|                         'then' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 abstractComposePromise = jasmine.createSpyObj( | ||||
|                     'abstractComposePromise', | ||||
|                     [ | ||||
|                         'then' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 abstractComposePromise.then.and.callFake(function (success, error, notify) { | ||||
|                     notify(progress); | ||||
|                     success(domainObject); | ||||
|                 }); | ||||
|  | ||||
|                 locationServicePromise.then.and.callFake(function (callback) { | ||||
|                     callback(newParent); | ||||
|  | ||||
|                     return abstractComposePromise; | ||||
|                 }); | ||||
|  | ||||
|                 locationService | ||||
|                     .getLocationFromUser | ||||
|                     .and.returnValue(locationServicePromise); | ||||
|  | ||||
|                 dialogService = jasmine.createSpyObj('dialogService', | ||||
|                     ['showBlockingMessage'] | ||||
|                 ); | ||||
|  | ||||
|                 mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]); | ||||
|                 dialogService.showBlockingMessage.and.returnValue(mockDialog); | ||||
|  | ||||
|                 notification = jasmine.createSpyObj('notification', | ||||
|                     ['dismiss', 'model'] | ||||
|                 ); | ||||
|  | ||||
|                 notificationService = jasmine.createSpyObj('notificationService', | ||||
|                     ['notify', 'info'] | ||||
|                 ); | ||||
|  | ||||
|                 notificationService.notify.and.returnValue(notification); | ||||
|  | ||||
|                 mockLog = jasmine.createSpyObj('log', ['error']); | ||||
|  | ||||
|                 copyService = new MockCopyService(); | ||||
|             }); | ||||
|  | ||||
|             describe("with context from context-action", function () { | ||||
|                 beforeEach(function () { | ||||
|                     context = { | ||||
|                         domainObject: selectedObject | ||||
|                     }; | ||||
|  | ||||
|                     copyAction = new CopyAction( | ||||
|                         mockLog, | ||||
|                         policyService, | ||||
|                         locationService, | ||||
|                         copyService, | ||||
|                         dialogService, | ||||
|                         notificationService, | ||||
|                         context | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("initializes happily", function () { | ||||
|                     expect(copyAction).toBeDefined(); | ||||
|                 }); | ||||
|  | ||||
|                 describe("when performed it", function () { | ||||
|                     beforeEach(function () { | ||||
|                         spyOn(copyAction, 'progress').and.callThrough(); | ||||
|                         copyAction.perform(); | ||||
|                     }); | ||||
|  | ||||
|                     it("prompts for location", function () { | ||||
|                         expect(locationService.getLocationFromUser) | ||||
|                             .toHaveBeenCalledWith( | ||||
|                                 "Duplicate selectedObject To a Location", | ||||
|                                 "Duplicate To", | ||||
|                                 jasmine.any(Function), | ||||
|                                 currentParent | ||||
|                             ); | ||||
|                     }); | ||||
|  | ||||
|                     it("waits for location and handles cancellation by user", function () { | ||||
|                         expect(locationServicePromise.then) | ||||
|                             .toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function)); | ||||
|                     }); | ||||
|  | ||||
|                     it("copies object to selected location", function () { | ||||
|                         locationServicePromise | ||||
|                             .then | ||||
|                             .calls.mostRecent() | ||||
|                             .args[0](newParent); | ||||
|  | ||||
|                         expect(copyService.perform) | ||||
|                             .toHaveBeenCalledWith(selectedObject, newParent); | ||||
|                     }); | ||||
|  | ||||
|                     it("notifies the user of progress", function () { | ||||
|                         expect(notificationService.info).toHaveBeenCalled(); | ||||
|                     }); | ||||
|  | ||||
|                     it("notifies the user with name of object copied", function () { | ||||
|                         expect(notificationService.info) | ||||
|                             .toHaveBeenCalledWith("mockObject copied successfully."); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("with context from drag-drop", function () { | ||||
|                 beforeEach(function () { | ||||
|                     context = { | ||||
|                         selectedObject: selectedObject, | ||||
|                         domainObject: newParent | ||||
|                     }; | ||||
|  | ||||
|                     copyAction = new CopyAction( | ||||
|                         mockLog, | ||||
|                         policyService, | ||||
|                         locationService, | ||||
|                         copyService, | ||||
|                         dialogService, | ||||
|                         notificationService, | ||||
|                         context | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("initializes happily", function () { | ||||
|                     expect(copyAction).toBeDefined(); | ||||
|                 }); | ||||
|  | ||||
|                 it("performs copy immediately", function () { | ||||
|                     copyAction.perform(); | ||||
|                     expect(copyService.perform) | ||||
|                         .toHaveBeenCalledWith(selectedObject, newParent); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,178 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../../src/actions/MoveAction', | ||||
|         '../services/MockMoveService', | ||||
|         '../DomainObjectFactory' | ||||
|     ], | ||||
|     function (MoveAction, MockMoveService, domainObjectFactory) { | ||||
|  | ||||
|         describe("Move Action", function () { | ||||
|  | ||||
|             var moveAction, | ||||
|                 policyService, | ||||
|                 locationService, | ||||
|                 locationServicePromise, | ||||
|                 moveService, | ||||
|                 context, | ||||
|                 selectedObject, | ||||
|                 selectedObjectContextCapability, | ||||
|                 currentParent, | ||||
|                 newParent; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 policyService = jasmine.createSpyObj( | ||||
|                     'policyService', | ||||
|                     ['allow'] | ||||
|                 ); | ||||
|                 policyService.allow.and.returnValue(true); | ||||
|  | ||||
|                 selectedObjectContextCapability = jasmine.createSpyObj( | ||||
|                     'selectedObjectContextCapability', | ||||
|                     [ | ||||
|                         'getParent' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 selectedObject = domainObjectFactory({ | ||||
|                     name: 'selectedObject', | ||||
|                     model: { | ||||
|                         name: 'selectedObject' | ||||
|                     }, | ||||
|                     capabilities: { | ||||
|                         context: selectedObjectContextCapability | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 currentParent = domainObjectFactory({ | ||||
|                     name: 'currentParent' | ||||
|                 }); | ||||
|  | ||||
|                 selectedObjectContextCapability | ||||
|                     .getParent | ||||
|                     .and.returnValue(currentParent); | ||||
|  | ||||
|                 newParent = domainObjectFactory({ | ||||
|                     name: 'newParent' | ||||
|                 }); | ||||
|  | ||||
|                 locationService = jasmine.createSpyObj( | ||||
|                     'locationService', | ||||
|                     [ | ||||
|                         'getLocationFromUser' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 locationServicePromise = jasmine.createSpyObj( | ||||
|                     'locationServicePromise', | ||||
|                     [ | ||||
|                         'then' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 locationService | ||||
|                     .getLocationFromUser | ||||
|                     .and.returnValue(locationServicePromise); | ||||
|  | ||||
|                 moveService = new MockMoveService(); | ||||
|             }); | ||||
|  | ||||
|             describe("with context from context-action", function () { | ||||
|                 beforeEach(function () { | ||||
|                     context = { | ||||
|                         domainObject: selectedObject | ||||
|                     }; | ||||
|  | ||||
|                     moveAction = new MoveAction( | ||||
|                         policyService, | ||||
|                         locationService, | ||||
|                         moveService, | ||||
|                         context | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("initializes happily", function () { | ||||
|                     expect(moveAction).toBeDefined(); | ||||
|                 }); | ||||
|  | ||||
|                 describe("when performed it", function () { | ||||
|                     beforeEach(function () { | ||||
|                         moveAction.perform(); | ||||
|                     }); | ||||
|  | ||||
|                     it("prompts for location", function () { | ||||
|                         expect(locationService.getLocationFromUser) | ||||
|                             .toHaveBeenCalledWith( | ||||
|                                 "Move selectedObject To a New Location", | ||||
|                                 "Move To", | ||||
|                                 jasmine.any(Function), | ||||
|                                 currentParent | ||||
|                             ); | ||||
|                     }); | ||||
|  | ||||
|                     it("waits for location and handles cancellation by user", function () { | ||||
|                         expect(locationServicePromise.then) | ||||
|                             .toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function)); | ||||
|                     }); | ||||
|  | ||||
|                     it("moves object to selected location", function () { | ||||
|                         locationServicePromise | ||||
|                             .then | ||||
|                             .calls.mostRecent() | ||||
|                             .args[0](newParent); | ||||
|  | ||||
|                         expect(moveService.perform) | ||||
|                             .toHaveBeenCalledWith(selectedObject, newParent); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("with context from drag-drop", function () { | ||||
|                 beforeEach(function () { | ||||
|                     context = { | ||||
|                         selectedObject: selectedObject, | ||||
|                         domainObject: newParent | ||||
|                     }; | ||||
|  | ||||
|                     moveAction = new MoveAction( | ||||
|                         policyService, | ||||
|                         locationService, | ||||
|                         moveService, | ||||
|                         context | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("initializes happily", function () { | ||||
|                     expect(moveAction).toBeDefined(); | ||||
|                 }); | ||||
|  | ||||
|                 it("performs move immediately", function () { | ||||
|                     moveAction.perform(); | ||||
|                     expect(moveService.perform) | ||||
|                         .toHaveBeenCalledWith(selectedObject, newParent); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,124 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     '../../src/policies/MovePolicy', | ||||
|     '../DomainObjectFactory' | ||||
| ], function (MovePolicy, domainObjectFactory) { | ||||
|  | ||||
|     describe("MovePolicy", function () { | ||||
|         var testMetadata, | ||||
|             testContext, | ||||
|             mockDomainObject, | ||||
|             mockParent, | ||||
|             mockParentType, | ||||
|             mockType, | ||||
|             mockAction, | ||||
|             policy; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             var mockContextCapability = | ||||
|                     jasmine.createSpyObj('context', ['getParent']); | ||||
|  | ||||
|             mockType = | ||||
|                 jasmine.createSpyObj('type', ['hasFeature']); | ||||
|             mockParentType = | ||||
|                 jasmine.createSpyObj('parent-type', ['hasFeature']); | ||||
|  | ||||
|             testMetadata = {}; | ||||
|  | ||||
|             mockDomainObject = domainObjectFactory({ | ||||
|                 capabilities: { | ||||
|                     context: mockContextCapability, | ||||
|                     type: mockType | ||||
|                 } | ||||
|             }); | ||||
|             mockParent = domainObjectFactory({ | ||||
|                 capabilities: { | ||||
|                     type: mockParentType | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             mockContextCapability.getParent.and.returnValue(mockParent); | ||||
|  | ||||
|             mockType.hasFeature.and.callFake(function (feature) { | ||||
|                 return feature === 'creation'; | ||||
|             }); | ||||
|             mockParentType.hasFeature.and.callFake(function (feature) { | ||||
|                 return feature === 'creation'; | ||||
|             }); | ||||
|  | ||||
|             mockAction = jasmine.createSpyObj('action', ['getMetadata']); | ||||
|             mockAction.getMetadata.and.returnValue(testMetadata); | ||||
|  | ||||
|             testContext = { domainObject: mockDomainObject }; | ||||
|  | ||||
|             policy = new MovePolicy(); | ||||
|         }); | ||||
|  | ||||
|         describe("for move actions", function () { | ||||
|             beforeEach(function () { | ||||
|                 testMetadata.key = 'move'; | ||||
|             }); | ||||
|  | ||||
|             describe("when an object is non-modifiable", function () { | ||||
|                 beforeEach(function () { | ||||
|                     mockType.hasFeature.and.returnValue(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("disallows the action", function () { | ||||
|                     expect(policy.allow(mockAction, testContext)).toBe(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("when a parent is non-modifiable", function () { | ||||
|                 beforeEach(function () { | ||||
|                     mockParentType.hasFeature.and.returnValue(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("disallows the action", function () { | ||||
|                     expect(policy.allow(mockAction, testContext)).toBe(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("when an object and its parent are modifiable", function () { | ||||
|                 it("allows the action", function () { | ||||
|                     expect(policy.allow(mockAction, testContext)).toBe(true); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe("for other actions", function () { | ||||
|             beforeEach(function () { | ||||
|                 testMetadata.key = 'foo'; | ||||
|             }); | ||||
|  | ||||
|             it("simply allows the action", function () { | ||||
|                 expect(policy.allow(mockAction, testContext)).toBe(true); | ||||
|                 mockType.hasFeature.and.returnValue(false); | ||||
|                 expect(policy.allow(mockAction, testContext)).toBe(true); | ||||
|                 mockParentType.hasFeature.and.returnValue(false); | ||||
|                 expect(policy.allow(mockAction, testContext)).toBe(true); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,96 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     function () { | ||||
|  | ||||
|         /** | ||||
|          * MockMoveService provides the same interface as the moveService, | ||||
|          * returning promises where it would normally do so.  At it's core, | ||||
|          * it is a jasmine spy object, but it also tracks the promises it | ||||
|          * returns and provides shortcut methods for resolving those promises | ||||
|          * synchronously. | ||||
|          * | ||||
|          * Usage: | ||||
|          * | ||||
|          * ```javascript | ||||
|          * var moveService = new MockMoveService(); | ||||
|          * | ||||
|          * // validate is a standard jasmine spy. | ||||
|          * moveService.validate.and.returnValue(true); | ||||
|          * var isValid = moveService.validate(object, parentCandidate); | ||||
|          * expect(isValid).toBe(true); | ||||
|          * | ||||
|          * // perform returns promises and tracks them. | ||||
|          * var whenCopied = jasmine.createSpy('whenCopied'); | ||||
|          * moveService.perform(object, parentObject).then(whenCopied); | ||||
|          * expect(whenCopied).not.toHaveBeenCalled(); | ||||
|          * moveService.perform.calls.mostRecent().resolve('someArg'); | ||||
|          * expect(whenCopied).toHaveBeenCalledWith('someArg'); | ||||
|          * ``` | ||||
|          */ | ||||
|         function MockMoveService() { | ||||
|             // track most recent call of a function, | ||||
|             // perform automatically returns | ||||
|             var mockMoveService = jasmine.createSpyObj( | ||||
|                 'MockMoveService', | ||||
|                 [ | ||||
|                     'validate', | ||||
|                     'perform' | ||||
|                 ] | ||||
|             ); | ||||
|  | ||||
|             mockMoveService.perform.and.callFake(() => { | ||||
|                 var performPromise, | ||||
|                     callExtensions, | ||||
|                     spy; | ||||
|  | ||||
|                 performPromise = jasmine.createSpyObj( | ||||
|                     'performPromise', | ||||
|                     ['then'] | ||||
|                 ); | ||||
|  | ||||
|                 callExtensions = { | ||||
|                     promise: performPromise, | ||||
|                     resolve: function (resolveWith) { | ||||
|                         performPromise.then.calls.all().forEach(function (call) { | ||||
|                             call.args[0](resolveWith); | ||||
|                         }); | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 spy = mockMoveService.perform; | ||||
|  | ||||
|                 Object.keys(callExtensions).forEach(function (key) { | ||||
|                     spy.calls.mostRecent()[key] = callExtensions[key]; | ||||
|                     spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key]; | ||||
|                 }); | ||||
|  | ||||
|                 return performPromise; | ||||
|             }); | ||||
|  | ||||
|             return mockMoveService; | ||||
|         } | ||||
|  | ||||
|         return MockMoveService; | ||||
|     } | ||||
| ); | ||||
| @@ -1,260 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../../src/services/MoveService', | ||||
|         '../services/MockLinkService', | ||||
|         '../DomainObjectFactory', | ||||
|         '../ControlledPromise' | ||||
|     ], | ||||
|     function ( | ||||
|         MoveService, | ||||
|         MockLinkService, | ||||
|         domainObjectFactory, | ||||
|         ControlledPromise | ||||
|     ) { | ||||
|  | ||||
|         xdescribe("MoveService", function () { | ||||
|  | ||||
|             var moveService, | ||||
|                 policyService, | ||||
|                 object, | ||||
|                 objectContextCapability, | ||||
|                 currentParent, | ||||
|                 parentCandidate, | ||||
|                 linkService; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 objectContextCapability = jasmine.createSpyObj( | ||||
|                     'objectContextCapability', | ||||
|                     [ | ||||
|                         'getParent' | ||||
|                     ] | ||||
|                 ); | ||||
|  | ||||
|                 object = domainObjectFactory({ | ||||
|                     name: 'object', | ||||
|                     id: 'a', | ||||
|                     capabilities: { | ||||
|                         context: objectContextCapability, | ||||
|                         type: { type: 'object' } | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 currentParent = domainObjectFactory({ | ||||
|                     name: 'currentParent', | ||||
|                     id: 'b' | ||||
|                 }); | ||||
|  | ||||
|                 objectContextCapability.getParent.and.returnValue(currentParent); | ||||
|  | ||||
|                 parentCandidate = domainObjectFactory({ | ||||
|                     name: 'parentCandidate', | ||||
|                     model: { composition: [] }, | ||||
|                     id: 'c', | ||||
|                     capabilities: { | ||||
|                         type: { type: 'parentCandidate' } | ||||
|                     } | ||||
|                 }); | ||||
|                 policyService = jasmine.createSpyObj( | ||||
|                     'policyService', | ||||
|                     ['allow'] | ||||
|                 ); | ||||
|                 linkService = new MockLinkService(); | ||||
|                 policyService.allow.and.returnValue(true); | ||||
|                 moveService = new MoveService(policyService, linkService); | ||||
|             }); | ||||
|  | ||||
|             describe("validate", function () { | ||||
|                 var validate; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     validate = function () { | ||||
|                         return moveService.validate(object, parentCandidate); | ||||
|                     }; | ||||
|                 }); | ||||
|  | ||||
|                 it("does not allow an invalid parent", function () { | ||||
|                     parentCandidate = undefined; | ||||
|                     expect(validate()).toBe(false); | ||||
|                     parentCandidate = {}; | ||||
|                     expect(validate()).toBe(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not allow moving to current parent", function () { | ||||
|                     parentCandidate.id = currentParent.id = 'xyz'; | ||||
|                     expect(validate()).toBe(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not allow moving to self", function () { | ||||
|                     object.id = parentCandidate.id = 'xyz'; | ||||
|                     expect(validate()).toBe(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not allow moving to the same location", function () { | ||||
|                     object.id = 'abc'; | ||||
|                     parentCandidate.model.composition = ['abc']; | ||||
|                     expect(validate()).toBe(false); | ||||
|                 }); | ||||
|  | ||||
|                 describe("defers to policyService", function () { | ||||
|  | ||||
|                     it("calls policy service with correct args", function () { | ||||
|                         validate(); | ||||
|                         expect(policyService.allow).toHaveBeenCalledWith( | ||||
|                             "composition", | ||||
|                             parentCandidate, | ||||
|                             object | ||||
|                         ); | ||||
|                     }); | ||||
|  | ||||
|                     it("and returns false", function () { | ||||
|                         policyService.allow.and.returnValue(false); | ||||
|                         expect(validate()).toBe(false); | ||||
|                     }); | ||||
|  | ||||
|                     it("and returns true", function () { | ||||
|                         policyService.allow.and.returnValue(true); | ||||
|                         expect(validate()).toBe(true); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("perform", function () { | ||||
|  | ||||
|                 var actionCapability, | ||||
|                     locationCapability, | ||||
|                     locationPromise, | ||||
|                     newParent, | ||||
|                     moveResult; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     newParent = parentCandidate; | ||||
|  | ||||
|                     actionCapability = jasmine.createSpyObj( | ||||
|                         'actionCapability', | ||||
|                         ['perform'] | ||||
|                     ); | ||||
|  | ||||
|                     locationCapability = jasmine.createSpyObj( | ||||
|                         'locationCapability', | ||||
|                         [ | ||||
|                             'isOriginal', | ||||
|                             'setPrimaryLocation', | ||||
|                             'getContextualLocation' | ||||
|                         ] | ||||
|                     ); | ||||
|  | ||||
|                     locationPromise = new ControlledPromise(); | ||||
|                     locationCapability.setPrimaryLocation | ||||
|                         .and.returnValue(locationPromise); | ||||
|  | ||||
|                     object = domainObjectFactory({ | ||||
|                         name: 'object', | ||||
|                         capabilities: { | ||||
|                             action: actionCapability, | ||||
|                             location: locationCapability, | ||||
|                             context: objectContextCapability, | ||||
|                             type: { type: 'object' } | ||||
|                         } | ||||
|                     }); | ||||
|                     moveResult = moveService.perform(object, newParent); | ||||
|                 }); | ||||
|  | ||||
|                 it("links object to newParent", function () { | ||||
|                     expect(linkService.perform).toHaveBeenCalledWith( | ||||
|                         object, | ||||
|                         newParent | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("returns a promise", function () { | ||||
|                     expect(moveResult.then).toEqual(jasmine.any(Function)); | ||||
|                 }); | ||||
|  | ||||
|                 it("waits for result of link", function () { | ||||
|                     expect(linkService.perform.calls.mostRecent().promise.then) | ||||
|                         .toHaveBeenCalledWith(jasmine.any(Function)); | ||||
|                 }); | ||||
|  | ||||
|                 it("throws an error when performed on invalid inputs", function () { | ||||
|                     function perform() { | ||||
|                         moveService.perform(object, newParent); | ||||
|                     } | ||||
|  | ||||
|                     spyOn(moveService, "validate"); | ||||
|                     moveService.validate.and.returnValue(true); | ||||
|                     expect(perform).not.toThrow(); | ||||
|                     moveService.validate.and.returnValue(false); | ||||
|                     expect(perform).toThrow(); | ||||
|                 }); | ||||
|  | ||||
|                 describe("when moving an original", function () { | ||||
|                     beforeEach(function () { | ||||
|                         locationCapability.getContextualLocation | ||||
|                             .and.returnValue('new-location'); | ||||
|                         locationCapability.isOriginal.and.returnValue(true); | ||||
|                         linkService.perform.calls.mostRecent().promise.resolve(); | ||||
|                     }); | ||||
|  | ||||
|                     it("updates location", function () { | ||||
|                         expect(locationCapability.setPrimaryLocation) | ||||
|                             .toHaveBeenCalledWith('new-location'); | ||||
|                     }); | ||||
|  | ||||
|                     describe("after location update", function () { | ||||
|                         beforeEach(function () { | ||||
|                             locationPromise.resolve(); | ||||
|                         }); | ||||
|  | ||||
|                         it("removes object from parent without user warning dialog", function () { | ||||
|                             expect(actionCapability.perform) | ||||
|                                 .toHaveBeenCalledWith('remove', true); | ||||
|                         }); | ||||
|  | ||||
|                     }); | ||||
|  | ||||
|                 }); | ||||
|  | ||||
|                 describe("when moving a link", function () { | ||||
|                     beforeEach(function () { | ||||
|                         locationCapability.isOriginal.and.returnValue(false); | ||||
|                         linkService.perform.calls.mostRecent().promise.resolve(); | ||||
|                     }); | ||||
|  | ||||
|                     it("does not update location", function () { | ||||
|                         expect(locationCapability.setPrimaryLocation) | ||||
|                             .not | ||||
|                             .toHaveBeenCalled(); | ||||
|                     }); | ||||
|  | ||||
|                     it("removes object from parent without user warning dialog", function () { | ||||
|                         expect(actionCapability.perform) | ||||
|                             .toHaveBeenCalledWith('remove', true); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -47,6 +47,8 @@ define([ | ||||
|                             "implementation": ExportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-export", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "openmct", | ||||
|                                 "exportService", | ||||
| @@ -61,6 +63,8 @@ define([ | ||||
|                             "implementation": ImportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-import", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "exportService", | ||||
|                                 "identifierService", | ||||
|   | ||||
| @@ -32,7 +32,8 @@ | ||||
|     function indexItem(id, model) { | ||||
|         indexedItems.push({ | ||||
|             id: id, | ||||
|             name: model.name.toLowerCase() | ||||
|             name: model.name.toLowerCase(), | ||||
|             type: model.type | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -125,13 +125,12 @@ define([ | ||||
|      * @param topic the topicService. | ||||
|      */ | ||||
|     GenericSearchProvider.prototype.indexOnMutation = function (topic) { | ||||
|         var mutationTopic = topic('mutation'), | ||||
|             provider = this; | ||||
|         let mutationTopic = topic('mutation'); | ||||
|  | ||||
|         mutationTopic.listen(function (mutatedObject) { | ||||
|             var editor = mutatedObject.getCapability('editor'); | ||||
|         mutationTopic.listen(mutatedObject => { | ||||
|             let editor = mutatedObject.getCapability('editor'); | ||||
|             if (!editor || !editor.inEditContext()) { | ||||
|                 provider.index( | ||||
|                 this.index( | ||||
|                     mutatedObject.getId(), | ||||
|                     mutatedObject.getModel() | ||||
|                 ); | ||||
| @@ -147,10 +146,15 @@ define([ | ||||
|      * @param {String} id to be indexed. | ||||
|      */ | ||||
|     GenericSearchProvider.prototype.scheduleForIndexing = function (id) { | ||||
|         if (!this.indexedIds[id] && !this.pendingIndex[id]) { | ||||
|             this.indexedIds[id] = true; | ||||
|             this.pendingIndex[id] = true; | ||||
|             this.idsToIndex.push(id); | ||||
|         const identifier = objectUtils.parseKeyString(id); | ||||
|         const objectProvider = this.openmct.objects.getProvider(identifier); | ||||
|  | ||||
|         if (objectProvider === undefined || objectProvider.search === undefined) { | ||||
|             if (!this.indexedIds[id] && !this.pendingIndex[id]) { | ||||
|                 this.indexedIds[id] = true; | ||||
|                 this.pendingIndex[id] = true; | ||||
|                 this.idsToIndex.push(id); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.keepIndexing(); | ||||
| @@ -262,6 +266,7 @@ define([ | ||||
|                 return { | ||||
|                     id: hit.item.id, | ||||
|                     model: hit.item.model, | ||||
|                     type: hit.item.type, | ||||
|                     score: hit.matchCount | ||||
|                 }; | ||||
|             }); | ||||
|   | ||||
| @@ -41,7 +41,8 @@ | ||||
|         indexedItems.push({ | ||||
|             id: id, | ||||
|             vector: vector, | ||||
|             model: model | ||||
|             model: model, | ||||
|             type: model.type | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -46,6 +46,8 @@ define([ | ||||
|     './api/Branding', | ||||
|     './plugins/licenses/plugin', | ||||
|     './plugins/remove/plugin', | ||||
|     './plugins/move/plugin', | ||||
|     './plugins/duplicate/plugin', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
| @@ -73,6 +75,8 @@ define([ | ||||
|     BrandingAPI, | ||||
|     LicensesPlugin, | ||||
|     RemoveActionPlugin, | ||||
|     MoveActionPlugin, | ||||
|     DuplicateActionPlugin, | ||||
|     Vue | ||||
| ) { | ||||
|     /** | ||||
| @@ -215,7 +219,7 @@ define([ | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objects | ||||
|          */ | ||||
|         this.objects = new api.ObjectAPI(); | ||||
|         this.objects = new api.ObjectAPI.default(this.types); | ||||
|  | ||||
|         /** | ||||
|          * An interface for retrieving and interpreting telemetry data associated | ||||
| @@ -242,7 +246,11 @@ define([ | ||||
|  | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|  | ||||
|         this.contextMenu = new api.ContextMenuRegistry(); | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|  | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|         this.router = new ApplicationRouter(); | ||||
|  | ||||
| @@ -259,6 +267,8 @@ define([ | ||||
|         this.install(LegacyIndicatorsPlugin()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
|         this.install(RemoveActionPlugin.default()); | ||||
|         this.install(MoveActionPlugin.default()); | ||||
|         this.install(DuplicateActionPlugin.default()); | ||||
|         this.install(this.plugins.FolderView()); | ||||
|         this.install(this.plugins.Tabs()); | ||||
|         this.install(ImageryPlugin.default()); | ||||
| @@ -271,6 +281,9 @@ define([ | ||||
|         this.install(this.plugins.URLTimeSettingsSynchronizer()); | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.NonEditableFolder()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
| @@ -359,7 +372,7 @@ define([ | ||||
|      *        MCT; if undefined, MCT will be run in the body of the document | ||||
|      */ | ||||
|     MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) { | ||||
|         if (!this.plugins.DisplayLayout._installed) { | ||||
|         if (this.types.get('layout') === undefined) { | ||||
|             this.install(this.plugins.DisplayLayout({ | ||||
|                 showAsView: ['summary-widget'] | ||||
|             })); | ||||
|   | ||||
| @@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) { | ||||
|  | ||||
|     legacyActions.filter(contextualCategoryOnly) | ||||
|         .map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction)) | ||||
|         .forEach(openmct.contextMenu.registerAction); | ||||
|         .forEach(openmct.actions.register); | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,8 @@ export default class LegacyContextMenuAction { | ||||
|         this.description = LegacyAction.definition.description; | ||||
|         this.cssClass = LegacyAction.definition.cssClass; | ||||
|         this.LegacyAction = LegacyAction; | ||||
|         this.group = LegacyAction.definition.group; | ||||
|         this.priority = LegacyAction.definition.priority; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath) { | ||||
|   | ||||
| @@ -61,6 +61,7 @@ define([ | ||||
|             const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId()); | ||||
|             const keystring = utils.makeKeyString(newStyleObject.identifier); | ||||
|  | ||||
|             this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject); | ||||
|             this.eventEmitter.emit(keystring + ":*", newStyleObject); | ||||
|             this.eventEmitter.emit('mutation', newStyleObject); | ||||
|         }.bind(this); | ||||
| @@ -138,6 +139,12 @@ define([ | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) { | ||||
|         const searchService = this.$injector.get('searchService'); | ||||
|  | ||||
|         return searchService.query(query); | ||||
|     }; | ||||
|  | ||||
|     // Injects new object API as a decorator so that it hijacks all requests. | ||||
|     // Object providers implemented on new API should just work, old API should just work, many things may break. | ||||
|     function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) { | ||||
|   | ||||
							
								
								
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 EventEmitter from 'EventEmitter'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionCollection extends EventEmitter { | ||||
|     constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) { | ||||
|         super(); | ||||
|  | ||||
|         this.applicableActions = applicableActions; | ||||
|         this.openmct = openmct; | ||||
|         this.objectPath = objectPath; | ||||
|         this.view = view; | ||||
|         this.skipEnvironmentObservers = skipEnvironmentObservers; | ||||
|         this.objectUnsubscribes = []; | ||||
|  | ||||
|         let debounceOptions = { | ||||
|             leading: false, | ||||
|             trailing: true | ||||
|         }; | ||||
|  | ||||
|         this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions); | ||||
|         this._update = _.debounce(this._update.bind(this), 150, debounceOptions); | ||||
|  | ||||
|         if (!skipEnvironmentObservers) { | ||||
|             this._observeObjectPath(); | ||||
|             this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this._initializeActions(); | ||||
|     } | ||||
|  | ||||
|     disable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     enable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     hide(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     show(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
|             }); | ||||
|  | ||||
|             this.openmct.editor.off('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let visibleActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (!action.isHidden) { | ||||
|                 visibleActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return visibleActions; | ||||
|     } | ||||
|  | ||||
|     getStatusBarActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let statusBarActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (action.showInStatusBar && !action.isDisabled && !action.isHidden) { | ||||
|                 statusBarActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return statusBarActions; | ||||
|     } | ||||
|  | ||||
|     getActionsObject() { | ||||
|         return this.applicableActions; | ||||
|     } | ||||
|  | ||||
|     _update() { | ||||
|         this.emit('update', this.applicableActions); | ||||
|     } | ||||
|  | ||||
|     _observeObjectPath() { | ||||
|         let actionCollection = this; | ||||
|  | ||||
|         function updateObject(oldObject, newObject) { | ||||
|             Object.assign(oldObject, newObject); | ||||
|  | ||||
|             actionCollection._updateActions(); | ||||
|         } | ||||
|  | ||||
|         this.objectPath.forEach(object => { | ||||
|             if (object) { | ||||
|                 let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object)); | ||||
|  | ||||
|                 this.objectUnsubscribes.push(unsubscribe); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _initializeActions() { | ||||
|         Object.keys(this.applicableActions).forEach(key => { | ||||
|             this.applicableActions[key].callBack = () => { | ||||
|                 return this.applicableActions[key].invoke(this.objectPath, this.view); | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _updateActions() { | ||||
|         let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); | ||||
|  | ||||
|         this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); | ||||
|         this._initializeActions(); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     _mergeOldAndNewActions(oldActions, newActions) { | ||||
|         let mergedActions = {}; | ||||
|         Object.keys(newActions).forEach(key => { | ||||
|             if (oldActions[key]) { | ||||
|                 mergedActions[key] = oldActions[key]; | ||||
|             } else { | ||||
|                 mergedActions[key] = newActions[key]; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return mergedActions; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionCollection; | ||||
							
								
								
									
										229
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,229 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 ActionCollection from './ActionCollection'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe('The ActionCollection', () => { | ||||
|     let openmct; | ||||
|     let actionCollection; | ||||
|     let mockApplicableActions; | ||||
|     let mockObjectPath; | ||||
|     let mockView; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'mock parent folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-parent-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [ | ||||
|             'create', | ||||
|             'update' | ||||
|         ])); | ||||
|         mockView = { | ||||
|             getViewContext: () => { | ||||
|                 return { | ||||
|                     onlyAppliesToTestCase: true | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|         mockApplicableActions = { | ||||
|             'test-action-object-path': { | ||||
|                 name: 'Test Action Object Path', | ||||
|                 key: 'test-action-object-path', | ||||
|                 cssClass: 'test-action-object-path', | ||||
|                 description: 'This is a test action for object path', | ||||
|                 group: 'action', | ||||
|                 priority: 9, | ||||
|                 appliesTo: (objectPath) => { | ||||
|                     if (objectPath.length) { | ||||
|                         return objectPath[0].type === 'fake-folder'; | ||||
|                     } | ||||
|  | ||||
|                     return false; | ||||
|                 }, | ||||
|                 invoke: () => { | ||||
|                 } | ||||
|             }, | ||||
|             'test-action-view': { | ||||
|                 name: 'Test Action View', | ||||
|                 key: 'test-action-view', | ||||
|                 cssClass: 'test-action-view', | ||||
|                 description: 'This is a test action for view', | ||||
|                 group: 'action', | ||||
|                 priority: 9, | ||||
|                 showInStatusBar: true, | ||||
|                 appliesTo: (objectPath, view = {}) => { | ||||
|                     if (view.getViewContext) { | ||||
|                         let viewContext = view.getViewContext(); | ||||
|  | ||||
|                         return viewContext.onlyAppliesToTestCase; | ||||
|                     } | ||||
|  | ||||
|                     return false; | ||||
|                 }, | ||||
|                 invoke: () => { | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         actionCollection.destroy(); | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("disable method invoked with action keys", () => { | ||||
|         it("marks those actions as isDisabled", () => { | ||||
|             let actionKey = 'test-action-object-path'; | ||||
|             let actionsObject = actionCollection.getActionsObject(); | ||||
|             let action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isDisabled).toBeFalsy(); | ||||
|  | ||||
|             actionCollection.disable([actionKey]); | ||||
|             actionsObject = actionCollection.getActionsObject(); | ||||
|             action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isDisabled).toBeTrue(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("enable method invoked with action keys", () => { | ||||
|         it("marks the isDisabled property as false", () => { | ||||
|             let actionKey = 'test-action-object-path'; | ||||
|  | ||||
|             actionCollection.disable([actionKey]); | ||||
|  | ||||
|             let actionsObject = actionCollection.getActionsObject(); | ||||
|             let action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isDisabled).toBeTrue(); | ||||
|  | ||||
|             actionCollection.enable([actionKey]); | ||||
|             actionsObject = actionCollection.getActionsObject(); | ||||
|             action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isDisabled).toBeFalse(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("hide method invoked with action keys", () => { | ||||
|         it("marks those actions as isHidden", () => { | ||||
|             let actionKey = 'test-action-object-path'; | ||||
|             let actionsObject = actionCollection.getActionsObject(); | ||||
|             let action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isHidden).toBeFalsy(); | ||||
|  | ||||
|             actionCollection.hide([actionKey]); | ||||
|             actionsObject = actionCollection.getActionsObject(); | ||||
|             action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isHidden).toBeTrue(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("show method invoked with action keys", () => { | ||||
|         it("marks the isHidden property as false", () => { | ||||
|             let actionKey = 'test-action-object-path'; | ||||
|  | ||||
|             actionCollection.hide([actionKey]); | ||||
|  | ||||
|             let actionsObject = actionCollection.getActionsObject(); | ||||
|             let action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isHidden).toBeTrue(); | ||||
|  | ||||
|             actionCollection.show([actionKey]); | ||||
|             actionsObject = actionCollection.getActionsObject(); | ||||
|             action = actionsObject[actionKey]; | ||||
|  | ||||
|             expect(action.isHidden).toBeFalse(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("getVisibleActions method", () => { | ||||
|         it("returns an array of non hidden actions", () => { | ||||
|             let action1Key = 'test-action-object-path'; | ||||
|             let action2Key = 'test-action-view'; | ||||
|  | ||||
|             actionCollection.hide([action1Key]); | ||||
|  | ||||
|             let visibleActions = actionCollection.getVisibleActions(); | ||||
|  | ||||
|             expect(Array.isArray(visibleActions)).toBeTrue(); | ||||
|             expect(visibleActions.length).toEqual(1); | ||||
|             expect(visibleActions[0].key).toEqual(action2Key); | ||||
|  | ||||
|             actionCollection.show([action1Key]); | ||||
|             visibleActions = actionCollection.getVisibleActions(); | ||||
|  | ||||
|             expect(visibleActions.length).toEqual(2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("getStatusBarActions method", () => { | ||||
|         it("returns an array of non disabled, non hidden statusBar actions", () => { | ||||
|             let action2Key = 'test-action-view'; | ||||
|  | ||||
|             let statusBarActions = actionCollection.getStatusBarActions(); | ||||
|  | ||||
|             expect(Array.isArray(statusBarActions)).toBeTrue(); | ||||
|             expect(statusBarActions.length).toEqual(1); | ||||
|             expect(statusBarActions[0].key).toEqual(action2Key); | ||||
|  | ||||
|             actionCollection.disable([action2Key]); | ||||
|             statusBarActions = actionCollection.getStatusBarActions(); | ||||
|  | ||||
|             expect(statusBarActions.length).toEqual(0); | ||||
|  | ||||
|             actionCollection.enable([action2Key]); | ||||
|             statusBarActions = actionCollection.getStatusBarActions(); | ||||
|  | ||||
|             expect(statusBarActions.length).toEqual(1); | ||||
|             expect(statusBarActions[0].key).toEqual(action2Key); | ||||
|  | ||||
|             actionCollection.hide([action2Key]); | ||||
|             statusBarActions = actionCollection.getStatusBarActions(); | ||||
|  | ||||
|             expect(statusBarActions.length).toEqual(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 EventEmitter from 'EventEmitter'; | ||||
| import ActionCollection from './ActionCollection'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionsAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._allActions = {}; | ||||
|         this._actionCollections = new WeakMap(); | ||||
|         this._openmct = openmct; | ||||
|  | ||||
|         this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; | ||||
|  | ||||
|         this.register = this.register.bind(this); | ||||
|         this.get = this.get.bind(this); | ||||
|         this._applicableActions = this._applicableActions.bind(this); | ||||
|         this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); | ||||
|     } | ||||
|  | ||||
|     register(actionDefinition) { | ||||
|         this._allActions[actionDefinition.key] = actionDefinition; | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         if (view) { | ||||
|  | ||||
|             return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); | ||||
|         } else { | ||||
|  | ||||
|             return this._newActionCollection(objectPath, view, true); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateGroupOrder(groupArray) { | ||||
|         this._groupOrder = groupArray; | ||||
|     } | ||||
|  | ||||
|     _get(objectPath, view) { | ||||
|         let actionCollection = this._newActionCollection(objectPath, view); | ||||
|  | ||||
|         this._actionCollections.set(view, actionCollection); | ||||
|         actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|         return actionCollection; | ||||
|     } | ||||
|  | ||||
|     _getCachedActionCollection(objectPath, view) { | ||||
|         let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
|         return cachedActionCollection; | ||||
|     } | ||||
|  | ||||
|     _newActionCollection(objectPath, view, skipEnvironmentObservers) { | ||||
|         let applicableActions = this._applicableActions(objectPath, view); | ||||
|  | ||||
|         return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers); | ||||
|     } | ||||
|  | ||||
|     _updateCachedActionCollections(key) { | ||||
|         if (this._actionCollections.has(key)) { | ||||
|             let actionCollection = this._actionCollections.get(key); | ||||
|             actionCollection.off('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|             this._actionCollections.delete(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _applicableActions(objectPath, view) { | ||||
|         let actionsObject = {}; | ||||
|  | ||||
|         let keys = Object.keys(this._allActions).filter(key => { | ||||
|             let actionDefinition = this._allActions[key]; | ||||
|  | ||||
|             if (actionDefinition.appliesTo === undefined) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return actionDefinition.appliesTo(objectPath, view); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         keys.forEach(key => { | ||||
|             let action = _.clone(this._allActions[key]); | ||||
|  | ||||
|             actionsObject[key] = action; | ||||
|         }); | ||||
|  | ||||
|         return actionsObject; | ||||
|     } | ||||
|  | ||||
|     _groupAndSortActions(actionsArray) { | ||||
|         if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { | ||||
|             actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); | ||||
|         } | ||||
|  | ||||
|         let actionsObject = {}; | ||||
|         let groupedSortedActionsArray = []; | ||||
|  | ||||
|         function sortDescending(a, b) { | ||||
|             return b.priority - a.priority; | ||||
|         } | ||||
|  | ||||
|         actionsArray.forEach(action => { | ||||
|             if (actionsObject[action.group] === undefined) { | ||||
|                 actionsObject[action.group] = [action]; | ||||
|             } else { | ||||
|                 actionsObject[action.group].push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this._groupOrder.forEach(group => { | ||||
|             let groupArray = actionsObject[group]; | ||||
|  | ||||
|             if (groupArray) { | ||||
|                 groupedSortedActionsArray.push(groupArray.sort(sortDescending)); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return groupedSortedActionsArray; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionsAPI; | ||||
							
								
								
									
										153
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 ActionsAPI from './ActionsAPI'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
| import ActionCollection from './ActionCollection'; | ||||
|  | ||||
| describe('The Actions API', () => { | ||||
|     let openmct; | ||||
|     let actionsAPI; | ||||
|     let mockAction; | ||||
|     let mockObjectPath; | ||||
|     let mockObjectPathAction; | ||||
|     let mockViewContext1; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         actionsAPI = new ActionsAPI(openmct); | ||||
|         mockObjectPathAction = { | ||||
|             name: 'Test Action Object Path', | ||||
|             key: 'test-action-object-path', | ||||
|             cssClass: 'test-action-object-path', | ||||
|             description: 'This is a test action for object path', | ||||
|             group: 'action', | ||||
|             priority: 9, | ||||
|             appliesTo: (objectPath) => { | ||||
|                 if (objectPath.length) { | ||||
|                     return objectPath[0].type === 'fake-folder'; | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|             invoke: () => { | ||||
|             } | ||||
|         }; | ||||
|         mockAction = { | ||||
|             name: 'Test Action View', | ||||
|             key: 'test-action-view', | ||||
|             cssClass: 'test-action-view', | ||||
|             description: 'This is a test action for view', | ||||
|             group: 'action', | ||||
|             priority: 9, | ||||
|             appliesTo: (objectPath, view = {}) => { | ||||
|                 if (view.getViewContext) { | ||||
|                     let viewContext = view.getViewContext(); | ||||
|  | ||||
|                     return viewContext.onlyAppliesToTestCase; | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|             invoke: () => { | ||||
|             } | ||||
|         }; | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'mock parent folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-parent-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         mockViewContext1 = { | ||||
|             getViewContext: () => { | ||||
|                 return { | ||||
|                     onlyAppliesToTestCase: true | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("register method", () => { | ||||
|         it("adds action to ActionsAPI", () => { | ||||
|             actionsAPI.register(mockAction); | ||||
|  | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("get method", () => { | ||||
|         beforeEach(() => { | ||||
|             actionsAPI.register(mockAction); | ||||
|             actionsAPI.register(mockObjectPathAction); | ||||
|         }); | ||||
|  | ||||
|         it("returns an ActionCollection when invoked with an objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("returns an ActionCollection when invoked with an objectPath and view", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("returns relevant actions when invoked with objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let action = actionCollection.getActionsObject()[mockObjectPathAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockObjectPathAction.key); | ||||
|             expect(action.name).toEqual(mockObjectPathAction.name); | ||||
|         }); | ||||
|  | ||||
|         it("returns relevant actions when invoked with objectPath and view", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -28,9 +28,10 @@ define([ | ||||
|     './telemetry/TelemetryAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './notifications/NotificationAPI', | ||||
|     './contextMenu/ContextMenuAPI', | ||||
|     './Editor' | ||||
|  | ||||
|     './Editor', | ||||
|     './menu/MenuAPI', | ||||
|     './actions/ActionsAPI', | ||||
|     './status/StatusAPI' | ||||
| ], function ( | ||||
|     TimeAPI, | ||||
|     ObjectAPI, | ||||
| @@ -39,8 +40,10 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     IndicatorAPI, | ||||
|     NotificationAPI, | ||||
|     ContextMenuAPI, | ||||
|     EditorAPI | ||||
|     EditorAPI, | ||||
|     MenuAPI, | ||||
|     ActionsAPI, | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
| @@ -51,6 +54,8 @@ define([ | ||||
|         IndicatorAPI: IndicatorAPI, | ||||
|         NotificationAPI: NotificationAPI.default, | ||||
|         EditorAPI: EditorAPI, | ||||
|         ContextMenuRegistry: ContextMenuAPI.default | ||||
|         MenuAPI: MenuAPI.default, | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         StatusAPI: StatusAPI.default | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -60,6 +60,17 @@ define([ | ||||
|         }; | ||||
|         this.onProviderAdd = this.onProviderAdd.bind(this); | ||||
|         this.onProviderRemove = this.onProviderRemove.bind(this); | ||||
|         this.mutables = {}; | ||||
|  | ||||
|         if (this.domainObject.isMutable) { | ||||
|             this.returnMutables = true; | ||||
|             let unobserve = this.domainObject.$on('$_destroy', () => { | ||||
|                 Object.values(this.mutables).forEach(mutable => { | ||||
|                     this.publicAPI.objects.destroyMutable(mutable); | ||||
|                 }); | ||||
|                 unobserve(); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -75,10 +86,6 @@ define([ | ||||
|             throw new Error('Event not supported by composition: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (!this.mutationListener) { | ||||
|             this._synchronize(); | ||||
|         } | ||||
|  | ||||
|         if (this.provider.on && this.provider.off) { | ||||
|             if (event === 'add') { | ||||
|                 this.provider.on( | ||||
| @@ -189,6 +196,13 @@ define([ | ||||
|  | ||||
|             this.provider.add(this.domainObject, child.identifier); | ||||
|         } else { | ||||
|             if (this.returnMutables && this.publicAPI.objects.supportsMutation(child)) { | ||||
|                 let keyString = this.publicAPI.objects.makeKeyString(child.identifier); | ||||
|  | ||||
|                 child = this.publicAPI.objects._toMutable(child); | ||||
|                 this.mutables[keyString] = child; | ||||
|             } | ||||
|  | ||||
|             this.emit('add', child); | ||||
|         } | ||||
|     }; | ||||
| @@ -202,6 +216,8 @@ define([ | ||||
|      * @name load | ||||
|      */ | ||||
|     CompositionCollection.prototype.load = function () { | ||||
|         this.cleanUpMutables(); | ||||
|  | ||||
|         return this.provider.load(this.domainObject) | ||||
|             .then(function (children) { | ||||
|                 return Promise.all(children.map((c) => this.publicAPI.objects.get(c))); | ||||
| @@ -234,6 +250,14 @@ define([ | ||||
|         if (!skipMutate) { | ||||
|             this.provider.remove(this.domainObject, child.identifier); | ||||
|         } else { | ||||
|             if (this.returnMutables) { | ||||
|                 let keyString = this.publicAPI.objects.makeKeyString(child); | ||||
|                 if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) { | ||||
|                     this.publicAPI.objects.destroyMutable(this.mutables[keyString]); | ||||
|                     delete this.mutables[keyString]; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.emit('remove', child); | ||||
|         } | ||||
|     }; | ||||
| @@ -281,12 +305,6 @@ define([ | ||||
|         this.remove(child, true); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype._synchronize = function () { | ||||
|         this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => { | ||||
|             this.domainObject = JSON.parse(JSON.stringify(newDomainObject)); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype._destroy = function () { | ||||
|         if (this.mutationListener) { | ||||
|             this.mutationListener(); | ||||
| @@ -308,5 +326,11 @@ define([ | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     CompositionCollection.prototype.cleanUpMutables = function () { | ||||
|         Object.values(this.mutables).forEach(mutable => { | ||||
|             this.publicAPI.objects.destroyMutable(mutable); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     return CompositionCollection; | ||||
| }); | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.invoke(objectPath)" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions', 'objectPath'] | ||||
| }; | ||||
| </script> | ||||
| @@ -1,159 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 ContextMenuComponent from './ContextMenu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| /** | ||||
|  * The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface ContextMenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
| class ContextMenuAPI { | ||||
|     constructor() { | ||||
|         this._allActions = []; | ||||
|         this._activeContextMenu = undefined; | ||||
|  | ||||
|         this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this); | ||||
|         this.registerAction = this.registerAction.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when | ||||
|      * selected. Applicabilioty can be restricted by specification of an `appliesTo` function. | ||||
|      * | ||||
|      * @interface ContextMenuAction | ||||
|      * @memberof module:openmct | ||||
|      * @property {string} name the human-readable name of this view | ||||
|      * @property {string} description a longer-form description (typically | ||||
|      *           a single sentence or short paragraph) of this kind of view | ||||
|      * @property {string} cssClass the CSS class to apply to labels for this | ||||
|      *           view (to add icons, for instance) | ||||
|      * @property {string} key unique key to identify the context menu action | ||||
|      *           (used in custom context menu eg table rows, to identify which actions to include) | ||||
|      * @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item) | ||||
|      */ | ||||
|     /** | ||||
|      * @method appliesTo | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on. | ||||
|      * @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false. | ||||
|      */ | ||||
|     /** | ||||
|      * Code to be executed when the action is selected from a context menu | ||||
|      * @method invoke | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object to invoke the action on. | ||||
|      */ | ||||
|     /** | ||||
|      * @param {ContextMenuAction} actionDefinition | ||||
|      */ | ||||
|     registerAction(actionDefinition) { | ||||
|         this._allActions.push(actionDefinition); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) { | ||||
|  | ||||
|         let applicableActions = this._allActions.filter((action) => { | ||||
|  | ||||
|             if (actionsToBeIncluded) { | ||||
|                 if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key); | ||||
|             } else { | ||||
|                 if (action.appliesTo === undefined) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath) && !action.hideInDefaultMenu; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (this._activeContextMenu) { | ||||
|             this._hideActiveContextMenu(); | ||||
|         } | ||||
|  | ||||
|         this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions); | ||||
|         this._activeContextMenu.$mount(); | ||||
|         document.body.appendChild(this._activeContextMenu.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$el.style.left = `${position.x}px`; | ||||
|         this._activeContextMenu.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this._hideActiveContextMenu); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _hideActiveContextMenu() { | ||||
|         document.removeEventListener('click', this._hideActiveContextMenu); | ||||
|         document.body.removeChild(this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$destroy(); | ||||
|         this._activeContextMenu = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _createContextMenuForObject(objectPath, actions) { | ||||
|         return new Vue({ | ||||
|             components: { | ||||
|                 ContextMenu: ContextMenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 actions: actions, | ||||
|                 objectPath: objectPath | ||||
|             }, | ||||
|             template: '<ContextMenu></ContextMenu>' | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| export default ContextMenuAPI; | ||||
							
								
								
									
										68
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 Menu from './menu.js'; | ||||
|  | ||||
| /** | ||||
|  * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface MenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
| class MenuAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.showMenu = this.showMenu.bind(this); | ||||
|         this._clearMenuComponent = this._clearMenuComponent.bind(this); | ||||
|         this._showObjectMenu = this._showObjectMenu.bind(this); | ||||
|     } | ||||
|  | ||||
|     showMenu(x, y, actions, onDestroy) { | ||||
|         if (this.menuComponent) { | ||||
|             this.menuComponent.dismiss(); | ||||
|         } | ||||
|  | ||||
|         let options = { | ||||
|             x, | ||||
|             y, | ||||
|             actions, | ||||
|             onDestroy | ||||
|         }; | ||||
|  | ||||
|         this.menuComponent = new Menu(options); | ||||
|         this.menuComponent.once('destroy', this._clearMenuComponent); | ||||
|     } | ||||
|  | ||||
|     _clearMenuComponent() { | ||||
|         this.menuComponent = undefined; | ||||
|         delete this.menuComponent; | ||||
|     } | ||||
|  | ||||
|     _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { | ||||
|         let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded); | ||||
|  | ||||
|         this.showMenu(x, y, applicableActions); | ||||
|     } | ||||
| } | ||||
| export default MenuAPI; | ||||
							
								
								
									
										134
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 MenuAPI from './MenuAPI'; | ||||
| import Menu from './menu'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe ('The Menu API', () => { | ||||
|     let openmct; | ||||
|     let menuAPI; | ||||
|     let actionsArray; | ||||
|     let x; | ||||
|     let y; | ||||
|     let result; | ||||
|     let onDestroy; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         menuAPI = new MenuAPI(openmct); | ||||
|         actionsArray = [ | ||||
|             { | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'test-css-class-1', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'test-css-class-2', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         x = 8; | ||||
|         y = 16; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("showMenu method", () => { | ||||
|         it("creates an instance of Menu when invoked", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             expect(menuAPI.menuComponent).toBeInstanceOf(Menu); | ||||
|         }); | ||||
|  | ||||
|         describe("creates a menu component", () => { | ||||
|             let menuComponent; | ||||
|             let vueComponent; | ||||
|  | ||||
|             beforeEach(() => { | ||||
|                 onDestroy = jasmine.createSpy('onDestroy'); | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, onDestroy); | ||||
|                 vueComponent = menuAPI.menuComponent.component; | ||||
|                 menuComponent = document.querySelector(".c-menu"); | ||||
|  | ||||
|                 spyOn(vueComponent, '$destroy'); | ||||
|             }); | ||||
|  | ||||
|             it("renders a menu component in the expected x and y coordinates", () => { | ||||
|                 let boundingClientRect = menuComponent.getBoundingClientRect(); | ||||
|                 let left = boundingClientRect.left; | ||||
|                 let top = boundingClientRect.top; | ||||
|  | ||||
|                 expect(left).toEqual(x); | ||||
|                 expect(top).toEqual(y); | ||||
|             }); | ||||
|  | ||||
|             it("with all the actions passed in", () => { | ||||
|                 expect(menuComponent).toBeDefined(); | ||||
|  | ||||
|                 let listItems = menuComponent.children[0].children; | ||||
|  | ||||
|                 expect(listItems.length).toEqual(actionsArray.length); | ||||
|             }); | ||||
|  | ||||
|             it("with click-able menu items, that will invoke the correct callBacks", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|  | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 expect(result).toEqual("Test Action 1 Invoked"); | ||||
|             }); | ||||
|  | ||||
|             it("dismisses the menu when action is clicked on", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|  | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 let menu = document.querySelector('.c-menu'); | ||||
|  | ||||
|                 expect(menu).toBeNull(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the destroy method when menu is dismissed", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(vueComponent.$destroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the onDestroy callback if passed in", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(onDestroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul v-if="actions.length && actions[0].length"> | ||||
|         <template | ||||
|             v-for="(actionGroups, index) in actions" | ||||
|         > | ||||
|             <li | ||||
|                 v-for="action in actionGroups" | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
|             </li> | ||||
|             <div | ||||
|                 v-if="index !== actions.length - 1" | ||||
|                 :key="index" | ||||
|                 class="c-menu__section-separator" | ||||
|             > | ||||
|             </div> | ||||
|             <li | ||||
|                 v-if="actionGroups.length === 0" | ||||
|                 :key="index" | ||||
|             > | ||||
|                 No actions defined. | ||||
|             </li> | ||||
|         </template> | ||||
|     </ul> | ||||
|  | ||||
|     <ul v-else> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions'] | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 EventEmitter from 'EventEmitter'; | ||||
| import MenuComponent from './components/Menu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| class Menu extends EventEmitter { | ||||
|     constructor(options) { | ||||
|         super(); | ||||
|  | ||||
|         this.options = options; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 actions: options.actions | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
|         if (options.onDestroy) { | ||||
|             this.once('destroy', options.onDestroy); | ||||
|         } | ||||
|  | ||||
|         this.dismiss = this.dismiss.bind(this); | ||||
|         this.show = this.show.bind(this); | ||||
|  | ||||
|         this.show(); | ||||
|     } | ||||
|  | ||||
|     dismiss() { | ||||
|         this.emit('destroy'); | ||||
|         document.body.removeChild(this.component.$el); | ||||
|         document.removeEventListener('click', this.dismiss); | ||||
|         this.component.$destroy(); | ||||
|     } | ||||
|  | ||||
|     show() { | ||||
|         this.component.$mount(); | ||||
|         document.body.appendChild(this.component.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el); | ||||
|  | ||||
|         this.component.$el.style.left = `${position.x}px`; | ||||
|         this.component.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this.dismiss); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default Menu; | ||||
| @@ -75,13 +75,20 @@ export default class NotificationAPI extends EventEmitter { | ||||
|      * Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief | ||||
|      * period of time. | ||||
|      * @param {string} message The message to display to the user | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {InfoNotification} | ||||
|      */ | ||||
|     info(message) { | ||||
|     info(message, options = {}) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             autoDismiss: true, | ||||
|             severity: "info" | ||||
|             severity: "info", | ||||
|             options | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -90,12 +97,19 @@ export default class NotificationAPI extends EventEmitter { | ||||
|     /** | ||||
|      * Present an alert to the user. | ||||
|      * @param {string} message The message to display to the user. | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {Notification} | ||||
|      */ | ||||
|     alert(message) { | ||||
|     alert(message, options = {}) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             severity: "alert" | ||||
|             severity: "alert", | ||||
|             options | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -104,12 +118,19 @@ export default class NotificationAPI extends EventEmitter { | ||||
|     /** | ||||
|      * Present an error message to the user | ||||
|      * @param {string} message | ||||
|      * @param {Object} [options] object with following properties | ||||
|      *      autoDismissTimeout: {number} in miliseconds to automatically dismisses notification | ||||
|      *      link: {Object} Add a link to notifications for navigation | ||||
|      *              onClick: callback function | ||||
|      *              cssClass: css class name to add style on link | ||||
|      *              text: text to display for link | ||||
|      * @returns {Notification} | ||||
|      */ | ||||
|     error(message) { | ||||
|     error(message, options = {}) { | ||||
|         let notificationModel = { | ||||
|             message: message, | ||||
|             severity: "error" | ||||
|             severity: "error", | ||||
|             options | ||||
|         }; | ||||
|  | ||||
|         return this._notify(notificationModel); | ||||
| @@ -325,9 +346,11 @@ export default class NotificationAPI extends EventEmitter { | ||||
|         this.emit('notification', notification); | ||||
|  | ||||
|         if (notification.model.autoDismiss || this._selectNextNotification()) { | ||||
|             const autoDismissTimeout = notification.model.options.autoDismissTimeout | ||||
|                 || DEFAULT_AUTO_DISMISS_TIMEOUT; | ||||
|             this.activeTimeout = setTimeout(() => { | ||||
|                 this._dismissOrMinimize(notification); | ||||
|             }, DEFAULT_AUTO_DISMISS_TIMEOUT); | ||||
|             }, autoDismissTimeout); | ||||
|         } else { | ||||
|             delete this.activeTimeout; | ||||
|         } | ||||
|   | ||||
							
								
								
									
										154
									
								
								src/api/notifications/NotificationAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/api/notifications/NotificationAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 NotificationAPI from './NotificationAPI'; | ||||
|  | ||||
| describe('The Notifiation API', () => { | ||||
|     let notificationAPIInstance; | ||||
|     let defaultTimeout = 4000; | ||||
|  | ||||
|     beforeAll(() => { | ||||
|         notificationAPIInstance = new NotificationAPI(); | ||||
|     }); | ||||
|  | ||||
|     describe('the info method', () => { | ||||
|         let message = 'Example Notification Message'; | ||||
|         let severity = 'info'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.info(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message with info severity', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('auto dismisses the notification after a brief timeout', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(0); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the alert method', () => { | ||||
|         let message = 'Example alert message'; | ||||
|         let severity = 'alert'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.alert(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message, with alert severity', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('does not auto dismiss the notification', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(1); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the error method', () => { | ||||
|         let message = 'Example error message'; | ||||
|         let severity = 'error'; | ||||
|         let notificationModel; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notificationModel = notificationAPIInstance.error(message).model; | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it('shows a string message, with severity error', () => { | ||||
|             expect(notificationModel.message).toEqual(message); | ||||
|             expect(notificationModel.severity).toEqual(severity); | ||||
|         }); | ||||
|  | ||||
|         it('does not auto dismiss the notification', (done) => { | ||||
|             window.setTimeout(() => { | ||||
|                 expect(notificationAPIInstance.notifications.length).toEqual(1); | ||||
|                 done(); | ||||
|             }, defaultTimeout); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('the progress method', () => { | ||||
|         let title = 'This is a progress notification'; | ||||
|         let message1 = 'Example progress message 1'; | ||||
|         let message2 = 'Example progress message 2'; | ||||
|         let percentage1 = 50; | ||||
|         let percentage2 = 99.9; | ||||
|         let severity = 'info'; | ||||
|         let notification; | ||||
|         let updatedPercentage; | ||||
|         let updatedMessage; | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             notification = notificationAPIInstance.progress(title, percentage1, message1); | ||||
|             notification.on('progress', (percentage, text) => { | ||||
|                 updatedPercentage = percentage; | ||||
|                 updatedMessage = text; | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             notificationAPIInstance.dismissAllNotifications(); | ||||
|         }); | ||||
|  | ||||
|         it ('shows a notification with a message, progress message, percentage and info severity', () => { | ||||
|             expect(notification.model.message).toEqual(title); | ||||
|             expect(notification.model.severity).toEqual(severity); | ||||
|             expect(notification.model.progressText).toEqual(message1); | ||||
|             expect(notification.model.progressPerc).toEqual(percentage1); | ||||
|         }); | ||||
|  | ||||
|         it ('allows dynamically updating the progress attributes', () => { | ||||
|             notification.progress(percentage2, message2); | ||||
|  | ||||
|             expect(updatedPercentage).toEqual(percentage2); | ||||
|             expect(updatedMessage).toEqual(message2); | ||||
|         }); | ||||
|  | ||||
|         it ('allows dynamically dismissing of progress notification', () => { | ||||
|             notification.dismiss(); | ||||
|  | ||||
|             expect(notificationAPIInstance.notifications.length).toEqual(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										66
									
								
								src/api/objects/InterceptorRegistry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/api/objects/InterceptorRegistry.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
| export default class InterceptorRegistry { | ||||
|     /** | ||||
|      * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects. | ||||
|      * @interface InterceptorRegistry | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     constructor() { | ||||
|         this.interceptors = []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @interface InterceptorDef | ||||
|      * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object | ||||
|      * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object | ||||
|      * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number | ||||
|      * @memberof module:openmct InterceptorRegistry# | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Register a new object interceptor. | ||||
|      * | ||||
|      * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add | ||||
|      * @method addInterceptor | ||||
|      * @memberof module:openmct.InterceptorRegistry# | ||||
|      */ | ||||
|     addInterceptor(interceptorDef) { | ||||
|         //TODO: sort by priority | ||||
|         this.interceptors.push(interceptorDef); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieve all interceptors applicable to a domain object. | ||||
|      * @method getInterceptors | ||||
|      * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object | ||||
|      * @memberof module:openmct.InterceptorRegistry# | ||||
|      */ | ||||
|     getInterceptors(identifier, object) { | ||||
|         return this.interceptors.filter(interceptor => { | ||||
|             return typeof interceptor.appliesTo === 'function' | ||||
|                 && interceptor.appliesTo(identifier, object); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
							
								
								
									
										0
									
								
								src/api/objects/InterceptorRegistrySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/api/objects/InterceptorRegistrySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										137
									
								
								src/api/objects/MutableDomainObject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/api/objects/MutableDomainObject.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 utils from './object-utils.js'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| const ANY_OBJECT_EVENT = 'mutation'; | ||||
|  | ||||
| /** | ||||
|  * Wraps a domain object to keep its model synchronized with other instances of the same object. | ||||
|  * | ||||
|  * Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers | ||||
|  * should be careful to destroy MutableDomainObject in order to avoid memory leaks. | ||||
|  * | ||||
|  * All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except | ||||
|  * `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()` | ||||
|  * will result in the creation of a new MutableDomainObject and you will be responsible for destroying it | ||||
|  * (via openmct.objects.destroy) when you're done with it. | ||||
|  * | ||||
|  * @typedef MutableDomainObject | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
| class MutableDomainObject { | ||||
|     constructor(eventEmitter) { | ||||
|         Object.defineProperties(this, { | ||||
|             _globalEventEmitter: { | ||||
|                 value: eventEmitter, | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             _instanceEventEmitter: { | ||||
|                 value: new EventEmitter(), | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             _observers: { | ||||
|                 value: [], | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             isMutable: { | ||||
|                 value: true, | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     $observe(path, callback) { | ||||
|         let fullPath = qualifiedEventName(this, path); | ||||
|         let eventOff = | ||||
|             this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback); | ||||
|  | ||||
|         this._globalEventEmitter.on(fullPath, callback); | ||||
|         this._observers.push(eventOff); | ||||
|  | ||||
|         return eventOff; | ||||
|     } | ||||
|     $set(path, value) { | ||||
|         _.set(this, path, value); | ||||
|         _.set(this, 'modified', Date.now()); | ||||
|  | ||||
|         //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired. | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this); | ||||
|  | ||||
|         //Emit a general "any object" event | ||||
|         this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this); | ||||
|         //Emit wildcard event, with path so that callback knows what changed | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value); | ||||
|  | ||||
|         //Emit events specific to properties affected | ||||
|         let parentPropertiesList = path.split('.'); | ||||
|         for (let index = parentPropertiesList.length; index > 0; index--) { | ||||
|             let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); | ||||
|             this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath)); | ||||
|         } | ||||
|  | ||||
|         //TODO: Emit events for listeners of child properties when parent changes. | ||||
|         // Do it at observer time - also register observers for parent attribute path. | ||||
|     } | ||||
|     $on(event, callback) { | ||||
|         this._instanceEventEmitter.on(event, callback); | ||||
|  | ||||
|         return () => this._instanceEventEmitter.off(event, callback); | ||||
|     } | ||||
|     $destroy() { | ||||
|         this._observers.forEach(observer => observer()); | ||||
|         delete this._globalEventEmitter; | ||||
|         delete this._observers; | ||||
|         this._instanceEventEmitter.emit('$_destroy'); | ||||
|     } | ||||
|  | ||||
|     static createMutable(object, mutationTopic) { | ||||
|         let mutable = Object.create(new MutableDomainObject(mutationTopic)); | ||||
|         Object.assign(mutable, object); | ||||
|  | ||||
|         mutable.$observe('$_synchronize_model', (updatedObject) => { | ||||
|             let clone = JSON.parse(JSON.stringify(updatedObject)); | ||||
|             let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject)); | ||||
|             deleted.forEach((propertyName) => delete mutable[propertyName]); | ||||
|             Object.assign(mutable, clone); | ||||
|         }); | ||||
|  | ||||
|         return mutable; | ||||
|     } | ||||
|  | ||||
|     static mutateObject(object, path, value) { | ||||
|         _.set(object, path, value); | ||||
|         _.set(object, 'modified', Date.now()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function qualifiedEventName(object, eventName) { | ||||
|     let keystring = utils.makeKeyString(object.identifier); | ||||
|  | ||||
|     return [keystring, eventName].join(':'); | ||||
| } | ||||
|  | ||||
| export default MutableDomainObject; | ||||
| @@ -1,102 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     utils, | ||||
|     _ | ||||
| ) { | ||||
|     const ANY_OBJECT_EVENT = "mutation"; | ||||
|  | ||||
|     /** | ||||
|      * The MutableObject wraps a DomainObject and provides getters and | ||||
|      * setters for | ||||
|      * @param eventEmitter | ||||
|      * @param object | ||||
|      * @interface MutableObject | ||||
|      */ | ||||
|     function MutableObject(eventEmitter, object) { | ||||
|         this.eventEmitter = eventEmitter; | ||||
|         this.object = object; | ||||
|         this.unlisteners = []; | ||||
|     } | ||||
|  | ||||
|     function qualifiedEventName(object, eventName) { | ||||
|         const keystring = utils.makeKeyString(object.identifier); | ||||
|  | ||||
|         return [keystring, eventName].join(':'); | ||||
|     } | ||||
|  | ||||
|     MutableObject.prototype.stopListening = function () { | ||||
|         this.unlisteners.forEach(function (unlisten) { | ||||
|             unlisten(); | ||||
|         }); | ||||
|         this.unlisteners = []; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Observe changes to this domain object. | ||||
|      * @param {string} path the property to observe | ||||
|      * @param {Function} callback a callback to invoke when new values for | ||||
|      *        this property are observed | ||||
|      * @method on | ||||
|      * @memberof module:openmct.MutableObject# | ||||
|      */ | ||||
|     MutableObject.prototype.on = function (path, callback) { | ||||
|         const fullPath = qualifiedEventName(this.object, path); | ||||
|         const eventOff = | ||||
|             this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback); | ||||
|  | ||||
|         this.eventEmitter.on(fullPath, callback); | ||||
|         this.unlisteners.push(eventOff); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Modify this domain object. | ||||
|      * @param {string} path the property to modify | ||||
|      * @param {*} value the new value for this property | ||||
|      * @method set | ||||
|      * @memberof module:openmct.MutableObject# | ||||
|      */ | ||||
|     MutableObject.prototype.set = function (path, value) { | ||||
|         _.set(this.object, path, value); | ||||
|         _.set(this.object, 'modified', Date.now()); | ||||
|  | ||||
|         const handleRecursiveMutation = function (newObject) { | ||||
|             this.object = newObject; | ||||
|         }.bind(this); | ||||
|  | ||||
|         //Emit wildcard event | ||||
|         this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object); | ||||
|         //Emit a general "any object" event | ||||
|         this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object); | ||||
|  | ||||
|         this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation); | ||||
|         //Emit event specific to property | ||||
|         this.eventEmitter.emit(qualifiedEventName(this.object, path), value); | ||||
|         this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation); | ||||
|     }; | ||||
|  | ||||
|     return MutableObject; | ||||
| }); | ||||
| @@ -20,337 +20,453 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'lodash', | ||||
|     'objectUtils', | ||||
|     './MutableObject', | ||||
|     './RootRegistry', | ||||
|     './RootObjectProvider', | ||||
|     'EventEmitter' | ||||
| ], function ( | ||||
|     _, | ||||
|     utils, | ||||
|     MutableObject, | ||||
|     RootRegistry, | ||||
|     RootObjectProvider, | ||||
|     EventEmitter | ||||
| ) { | ||||
| import utils from 'objectUtils'; | ||||
| import MutableDomainObject from './MutableDomainObject'; | ||||
| import RootRegistry from './RootRegistry'; | ||||
| import RootObjectProvider from './RootObjectProvider'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import InterceptorRegistry from './InterceptorRegistry'; | ||||
|  | ||||
|     /** | ||||
|      * Utilities for loading, saving, and manipulating domain objects. | ||||
|      * @interface ObjectAPI | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
| /** | ||||
|  * Utilities for loading, saving, and manipulating domain objects. | ||||
|  * @interface ObjectAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
|     function ObjectAPI() { | ||||
|         this.eventEmitter = new EventEmitter(); | ||||
|         this.providers = {}; | ||||
|         this.rootRegistry = new RootRegistry(); | ||||
|         this.rootProvider = new RootObjectProvider.default(this.rootRegistry); | ||||
|         this.cache = {}; | ||||
| function ObjectAPI(typeRegistry) { | ||||
|     this.typeRegistry = typeRegistry; | ||||
|     this.eventEmitter = new EventEmitter(); | ||||
|     this.providers = {}; | ||||
|     this.rootRegistry = new RootRegistry(); | ||||
|     this.rootProvider = new RootObjectProvider(this.rootRegistry); | ||||
|     this.cache = {}; | ||||
|     this.interceptorRegistry = new InterceptorRegistry(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set fallback provider, this is an internal API for legacy reasons. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { | ||||
|     this.fallbackProvider = p; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Retrieve the provider for a given identifier. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.getProvider = function (identifier) { | ||||
|     if (identifier.key === 'ROOT') { | ||||
|         return this.rootProvider; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set fallback provider, this is an internal API for legacy reasons. | ||||
|      * @private | ||||
|      */ | ||||
|     ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) { | ||||
|         this.fallbackProvider = p; | ||||
|     }; | ||||
|     return this.providers[identifier.namespace] || this.fallbackProvider; | ||||
| }; | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the provider for a given identifier. | ||||
|      * @private | ||||
|      */ | ||||
|     ObjectAPI.prototype.getProvider = function (identifier) { | ||||
|         if (identifier.key === 'ROOT') { | ||||
|             return this.rootProvider; | ||||
|         } | ||||
| /** | ||||
|  * Get the root-level object. | ||||
|  * @returns {Promise.<DomainObject>} a promise for the root object | ||||
|  */ | ||||
| ObjectAPI.prototype.getRoot = function () { | ||||
|     return this.rootProvider.get(); | ||||
| }; | ||||
|  | ||||
|         return this.providers[identifier.namespace] || this.fallbackProvider; | ||||
|     }; | ||||
| /** | ||||
|  * Register a new object provider for a particular namespace. | ||||
|  * | ||||
|  * @param {string} namespace the namespace for which to provide objects | ||||
|  * @param {module:openmct.ObjectProvider} provider the provider which | ||||
|  *        will handle loading domain objects from this namespace | ||||
|  * @memberof {module:openmct.ObjectAPI#} | ||||
|  * @name addProvider | ||||
|  */ | ||||
| ObjectAPI.prototype.addProvider = function (namespace, provider) { | ||||
|     this.providers[namespace] = provider; | ||||
| }; | ||||
|  | ||||
|     /** | ||||
|      * Get the root-level object. | ||||
|      * @returns {Promise.<DomainObject>} a promise for the root object | ||||
|      */ | ||||
|     ObjectAPI.prototype.getRoot = function () { | ||||
|         return this.rootProvider.get(); | ||||
|     }; | ||||
| /** | ||||
|  * Provides the ability to read, write, and delete domain objects. | ||||
|  * | ||||
|  * When registering a new object provider, all methods on this interface | ||||
|  * are optional. | ||||
|  * | ||||
|  * @interface ObjectProvider | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
|     /** | ||||
|      * Register a new object provider for a particular namespace. | ||||
|      * | ||||
|      * @param {string} namespace the namespace for which to provide objects | ||||
|      * @param {module:openmct.ObjectProvider} provider the provider which | ||||
|      *        will handle loading domain objects from this namespace | ||||
|      * @memberof {module:openmct.ObjectAPI#} | ||||
|      * @name addProvider | ||||
|      */ | ||||
|     ObjectAPI.prototype.addProvider = function (namespace, provider) { | ||||
|         this.providers[namespace] = provider; | ||||
|     }; | ||||
| /** | ||||
|  * Create the given domain object in the corresponding persistence store | ||||
|  * | ||||
|  * @method create | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        create | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been created, or be rejected if it cannot be saved | ||||
|  */ | ||||
|  | ||||
|     /** | ||||
|      * Provides the ability to read, write, and delete domain objects. | ||||
|      * | ||||
|      * When registering a new object provider, all methods on this interface | ||||
|      * are optional. | ||||
|      * | ||||
|      * @interface ObjectProvider | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
| /** | ||||
|  * Update this domain object in its persistence store | ||||
|  * | ||||
|  * @method update | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        update | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been updated, or be rejected if it cannot be saved | ||||
|  */ | ||||
|  | ||||
|     /** | ||||
|      * Create the given domain object in the corresponding persistence store | ||||
|      * | ||||
|      * @method create | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        create | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been created, or be rejected if it cannot be saved | ||||
|      */ | ||||
| /** | ||||
|  * Delete this domain object. | ||||
|  * | ||||
|  * @method delete | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        delete | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been deleted, or be rejected if it cannot be deleted | ||||
|  */ | ||||
|  | ||||
|     /** | ||||
|      * Update this domain object in its persistence store | ||||
|      * | ||||
|      * @method update | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        update | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been updated, or be rejected if it cannot be saved | ||||
|      */ | ||||
| /** | ||||
|  * Get a domain object. | ||||
|  * | ||||
|  * @method get | ||||
|  * @memberof module:openmct.ObjectProvider# | ||||
|  * @param {string} key the key for the domain object to load | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been saved, or be rejected if it cannot be saved | ||||
|  */ | ||||
|  | ||||
|     /** | ||||
|      * Delete this domain object. | ||||
|      * | ||||
|      * @method delete | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        delete | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been deleted, or be rejected if it cannot be deleted | ||||
|      */ | ||||
| ObjectAPI.prototype.get = function (identifier) { | ||||
|     let keystring = this.makeKeyString(identifier); | ||||
|     if (this.cache[keystring] !== undefined) { | ||||
|         return this.cache[keystring]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a domain object. | ||||
|      * | ||||
|      * @method get | ||||
|      * @memberof module:openmct.ObjectProvider# | ||||
|      * @param {string} key the key for the domain object to load | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     identifier = utils.parseKeyString(identifier); | ||||
|     const provider = this.getProvider(identifier); | ||||
|  | ||||
|     /** | ||||
|      * Get a domain object. | ||||
|      * | ||||
|      * @method get | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|      *        the identifier for the domain object to load | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     ObjectAPI.prototype.get = function (identifier) { | ||||
|         let keystring = this.makeKeyString(identifier); | ||||
|         if (this.cache[keystring] !== undefined) { | ||||
|             return this.cache[keystring]; | ||||
|         } | ||||
|     if (!provider) { | ||||
|         throw new Error('No Provider Matched'); | ||||
|     } | ||||
|  | ||||
|         identifier = utils.parseKeyString(identifier); | ||||
|         const provider = this.getProvider(identifier); | ||||
|     if (!provider.get) { | ||||
|         throw new Error('Provider does not support get!'); | ||||
|     } | ||||
|  | ||||
|         if (!provider) { | ||||
|             throw new Error('No Provider Matched'); | ||||
|         } | ||||
|     let objectPromise = provider.get(identifier); | ||||
|     this.cache[keystring] = objectPromise; | ||||
|  | ||||
|         if (!provider.get) { | ||||
|             throw new Error('Provider does not support get!'); | ||||
|         } | ||||
|  | ||||
|         let objectPromise = provider.get(identifier); | ||||
|  | ||||
|         this.cache[keystring] = objectPromise; | ||||
|  | ||||
|         return objectPromise.then(result => { | ||||
|             delete this.cache[keystring]; | ||||
|  | ||||
|             return result; | ||||
|     return objectPromise.then(result => { | ||||
|         delete this.cache[keystring]; | ||||
|         const interceptors = this.listGetInterceptors(identifier, result); | ||||
|         interceptors.forEach(interceptor => { | ||||
|             result = interceptor.invoke(identifier, result); | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     ObjectAPI.prototype.delete = function () { | ||||
|         throw new Error('Delete not implemented'); | ||||
|     }; | ||||
|  | ||||
|     ObjectAPI.prototype.isPersistable = function (domainObject) { | ||||
|         let provider = this.getProvider(domainObject.identifier); | ||||
|  | ||||
|         return provider !== undefined | ||||
|             && provider.create !== undefined | ||||
|             && provider.update !== undefined; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Save this domain object in its current state. EXPERIMENTAL | ||||
|      * | ||||
|      * @private | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|      *        save | ||||
|      * @returns {Promise} a promise which will resolve when the domain object | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     ObjectAPI.prototype.save = function (domainObject) { | ||||
|         let provider = this.getProvider(domainObject.identifier); | ||||
|         let savedResolve; | ||||
|         let result; | ||||
|  | ||||
|         if (!this.isPersistable(domainObject)) { | ||||
|             result = Promise.reject('Object provider does not support saving'); | ||||
|         } else if (hasAlreadyBeenPersisted(domainObject)) { | ||||
|             result = Promise.resolve(true); | ||||
|         } else { | ||||
|             const persistedTime = Date.now(); | ||||
|             if (domainObject.persisted === undefined) { | ||||
|                 result = new Promise((resolve) => { | ||||
|                     savedResolve = resolve; | ||||
|                 }); | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 provider.create(domainObject).then((response) => { | ||||
|                     this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                     savedResolve(response); | ||||
|                 }); | ||||
|             } else { | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 result = provider.update(domainObject); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     }; | ||||
|     }); | ||||
| }; | ||||
|  | ||||
|     /** | ||||
|      * Add a root-level object. | ||||
|      * @param {module:openmct.ObjectAPI~Identifier|function} an array of | ||||
|      *        identifiers for root level objects, or a function that returns a | ||||
|      *        promise for an identifier or an array of root level objects. | ||||
|      * @method addRoot | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.addRoot = function (key) { | ||||
|         this.rootRegistry.addRoot(key); | ||||
|     }; | ||||
| /** | ||||
|  * Search for domain objects. | ||||
|  * | ||||
|  * Object providersSearches and combines results of each object provider search. | ||||
|  * Objects without search provided will have been indexed | ||||
|  * and will be searched using the fallback indexed search. | ||||
|  * Search results are asynchronous and resolve in parallel. | ||||
|  * | ||||
|  * @method search | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  * @param {string} query the term to search for | ||||
|  * @param {Object} options search options | ||||
|  * @returns {Array.<Promise.<module:openmct.DomainObject>>} | ||||
|  *          an array of promises returned from each object provider's search function | ||||
|  *          each resolving to domain objects matching provided search query and options. | ||||
|  */ | ||||
| ObjectAPI.prototype.search = function (query, options) { | ||||
|     const searchPromises = Object.values(this.providers) | ||||
|         .filter(provider => provider.search !== undefined) | ||||
|         .map(provider => provider.search(query, options)); | ||||
|  | ||||
|     /** | ||||
|      * Modify a domain object. | ||||
|      * @param {module:openmct.DomainObject} object the object to mutate | ||||
|      * @param {string} path the property to modify | ||||
|      * @param {*} value the new value for this property | ||||
|      * @method mutate | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.mutate = function (domainObject, path, value) { | ||||
|         const mutableObject = | ||||
|             new MutableObject(this.eventEmitter, domainObject); | ||||
|     searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options) | ||||
|         .then(results => results.hits | ||||
|             .map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId())))); | ||||
|  | ||||
|         return mutableObject.set(path, value); | ||||
|     }; | ||||
|     return searchPromises; | ||||
| }; | ||||
|  | ||||
|     /** | ||||
|      * Observe changes to a domain object. | ||||
|      * @param {module:openmct.DomainObject} object the object to observe | ||||
|      * @param {string} path the property to observe | ||||
|      * @param {Function} callback a callback to invoke when new values for | ||||
|      *        this property are observed | ||||
|      * @method observe | ||||
|      * @memberof module:openmct.ObjectAPI# | ||||
|      */ | ||||
|     ObjectAPI.prototype.observe = function (domainObject, path, callback) { | ||||
|         const mutableObject = | ||||
|             new MutableObject(this.eventEmitter, domainObject); | ||||
|         mutableObject.on(path, callback); | ||||
|  | ||||
|         return mutableObject.stopListening.bind(mutableObject); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|      * @returns {string} A string representation of the given identifier, including namespace and key | ||||
|      */ | ||||
|     ObjectAPI.prototype.makeKeyString = function (identifier) { | ||||
|         return utils.makeKeyString(identifier); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Given any number of identifiers, will return true if they are all equal, otherwise false. | ||||
|      * @param {module:openmct.ObjectAPI~Identifier[]} identifiers | ||||
|      */ | ||||
|     ObjectAPI.prototype.areIdsEqual = function (...identifiers) { | ||||
|         return identifiers.map(utils.parseKeyString) | ||||
|             .every(identifier => { | ||||
|                 return identifier === identifiers[0] | ||||
|                     || (identifier.namespace === identifiers[0].namespace | ||||
|                         && identifier.key === identifiers[0].key); | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) { | ||||
|         return this.get(identifier).then((domainObject) => { | ||||
|             path.push(domainObject); | ||||
|             let location = domainObject.location; | ||||
|  | ||||
|             if (location) { | ||||
|                 return this.getOriginalPath(utils.parseKeyString(location), path); | ||||
|             } else { | ||||
|                 return path; | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Uniquely identifies a domain object. | ||||
|      * | ||||
|      * @typedef Identifier | ||||
|      * @memberof module:openmct.ObjectAPI~ | ||||
|      * @property {string} namespace the namespace to/from which this domain | ||||
|      *           object should be loaded/stored. | ||||
|      * @property {string} key a unique identifier for the domain object | ||||
|      *           within that namespace | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * A domain object is an entity of relevance to a user's workflow, that | ||||
|      * should appear as a distinct and meaningful object within the user | ||||
|      * interface. Examples of domain objects are folders, telemetry sensors, | ||||
|      * and so forth. | ||||
|      * | ||||
|      * A few common properties are defined for domain objects. Beyond these, | ||||
|      * individual types of domain objects may add more as they see fit. | ||||
|      * | ||||
|      * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which | ||||
|      *           uniquely identifies this domain object | ||||
|      * @property {string} type the type of domain object | ||||
|      * @property {string} name the human-readable name for this domain object | ||||
|      * @property {string} [creator] the user name of the creator of this domain | ||||
|      *           object | ||||
|      * @property {number} [modified] the time, in milliseconds since the UNIX | ||||
|      *           epoch, at which this domain object was last modified | ||||
|      * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if | ||||
|      *           present, this will be used by the default composition provider | ||||
|      *           to load domain objects | ||||
|      * @typedef DomainObject | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|  | ||||
|     function hasAlreadyBeenPersisted(domainObject) { | ||||
|         return domainObject.persisted !== undefined | ||||
|             && domainObject.persisted === domainObject.modified; | ||||
| /** | ||||
|  * Will fetch object for the given identifier, returning a version of the object that will automatically keep | ||||
|  * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it. | ||||
|  * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The | ||||
|  * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are | ||||
|  * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed. | ||||
|  * | ||||
|  * @memberof {module:openmct.ObjectAPI#} | ||||
|  * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if | ||||
|  * the object can be mutated. | ||||
|  */ | ||||
| ObjectAPI.prototype.getMutable = function (identifier) { | ||||
|     if (!this.supportsMutation(identifier)) { | ||||
|         throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`); | ||||
|     } | ||||
|  | ||||
|     return ObjectAPI; | ||||
| }); | ||||
|     return this.get(identifier).then((object) => { | ||||
|         return this._toMutable(object); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * This function is for cleaning up a mutable domain object when you're done with it. | ||||
|  * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the | ||||
|  * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle. | ||||
|  * @param {MutableDomainObject} domainObject | ||||
|  */ | ||||
| ObjectAPI.prototype.destroyMutable = function (domainObject) { | ||||
|     if (domainObject.isMutable) { | ||||
|         return domainObject.$destroy(); | ||||
|     } else { | ||||
|         throw new Error("Attempted to destroy non-mutable domain object"); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.delete = function () { | ||||
|     throw new Error('Delete not implemented'); | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.isPersistable = function (idOrKeyString) { | ||||
|     let identifier = utils.parseKeyString(idOrKeyString); | ||||
|     let provider = this.getProvider(identifier); | ||||
|  | ||||
|     return provider !== undefined | ||||
|         && provider.create !== undefined | ||||
|         && provider.update !== undefined; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Save this domain object in its current state. EXPERIMENTAL | ||||
|  * | ||||
|  * @private | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  * @param {module:openmct.DomainObject} domainObject the domain object to | ||||
|  *        save | ||||
|  * @returns {Promise} a promise which will resolve when the domain object | ||||
|  *          has been saved, or be rejected if it cannot be saved | ||||
|  */ | ||||
| ObjectAPI.prototype.save = function (domainObject) { | ||||
|     let provider = this.getProvider(domainObject.identifier); | ||||
|     let savedResolve; | ||||
|     let result; | ||||
|  | ||||
|     if (!this.isPersistable(domainObject.identifier)) { | ||||
|         result = Promise.reject('Object provider does not support saving'); | ||||
|     } else if (hasAlreadyBeenPersisted(domainObject)) { | ||||
|         result = Promise.resolve(true); | ||||
|     } else { | ||||
|         const persistedTime = Date.now(); | ||||
|         if (domainObject.persisted === undefined) { | ||||
|             result = new Promise((resolve) => { | ||||
|                 savedResolve = resolve; | ||||
|             }); | ||||
|             domainObject.persisted = persistedTime; | ||||
|             provider.create(domainObject).then((response) => { | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 savedResolve(response); | ||||
|             }); | ||||
|         } else { | ||||
|             domainObject.persisted = persistedTime; | ||||
|             this.mutate(domainObject, 'persisted', persistedTime); | ||||
|             result = provider.update(domainObject); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Add a root-level object. | ||||
|  * @param {module:openmct.ObjectAPI~Identifier|function} an array of | ||||
|  *        identifiers for root level objects, or a function that returns a | ||||
|  *        promise for an identifier or an array of root level objects. | ||||
|  * @method addRoot | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.addRoot = function (key) { | ||||
|     this.rootRegistry.addRoot(key); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get | ||||
|  * The domain object will be transformed after it is retrieved from the persistence store | ||||
|  * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef | ||||
|  * | ||||
|  * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add | ||||
|  * @method addGetInterceptor | ||||
|  * @memberof module:openmct.InterceptorRegistry# | ||||
|  */ | ||||
| ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) { | ||||
|     this.interceptorRegistry.addInterceptor(interceptorDef); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Retrieve the interceptors for a given domain object. | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype.listGetInterceptors = function (identifier, object) { | ||||
|     return this.interceptorRegistry.getInterceptors(identifier, object); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Modify a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to mutate | ||||
|  * @param {string} path the property to modify | ||||
|  * @param {*} value the new value for this property | ||||
|  * @method mutate | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.mutate = function (domainObject, path, value) { | ||||
|     if (!this.supportsMutation(domainObject.identifier)) { | ||||
|         throw `Error: Attempted to mutate immutable object ${domainObject.name}`; | ||||
|     } | ||||
|  | ||||
|     if (domainObject.isMutable) { | ||||
|         domainObject.$set(path, value); | ||||
|     } else { | ||||
|         //Creating a temporary mutable domain object allows other mutable instances of the | ||||
|         //object to be kept in sync. | ||||
|         let mutableDomainObject = this._toMutable(domainObject); | ||||
|  | ||||
|         //Mutate original object | ||||
|         MutableDomainObject.mutateObject(domainObject, path, value); | ||||
|  | ||||
|         //Mutate temporary mutable object, in the process informing any other mutable instances | ||||
|         mutableDomainObject.$set(path, value); | ||||
|  | ||||
|         //Destroy temporary mutable object | ||||
|         this.destroyMutable(mutableDomainObject); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @private | ||||
|  */ | ||||
| ObjectAPI.prototype._toMutable = function (object) { | ||||
|     if (object.isMutable) { | ||||
|         return object; | ||||
|     } else { | ||||
|         return MutableDomainObject.createMutable(object, this.eventEmitter); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param module:openmct.ObjectAPI~Identifier identifier An object identifier | ||||
|  * @returns {boolean} true if the object can be mutated, otherwise returns false | ||||
|  */ | ||||
| ObjectAPI.prototype.supportsMutation = function (identifier) { | ||||
|     return this.isPersistable(identifier); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Observe changes to a domain object. | ||||
|  * @param {module:openmct.DomainObject} object the object to observe | ||||
|  * @param {string} path the property to observe | ||||
|  * @param {Function} callback a callback to invoke when new values for | ||||
|  *        this property are observed | ||||
|  * @method observe | ||||
|  * @memberof module:openmct.ObjectAPI# | ||||
|  */ | ||||
| ObjectAPI.prototype.observe = function (domainObject, path, callback) { | ||||
|     if (domainObject.isMutable) { | ||||
|         return domainObject.$observe(path, callback); | ||||
|     } else { | ||||
|         let mutable = this._toMutable(domainObject); | ||||
|         mutable.$observe(path, callback); | ||||
|  | ||||
|         return () => mutable.$destroy(); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {module:openmct.ObjectAPI~Identifier} identifier | ||||
|  * @returns {string} A string representation of the given identifier, including namespace and key | ||||
|  */ | ||||
| ObjectAPI.prototype.makeKeyString = function (identifier) { | ||||
|     return utils.makeKeyString(identifier); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon. | ||||
|  * @returns {module:openmct.ObjectAPI~Identifier} An identifier object | ||||
|  */ | ||||
| ObjectAPI.prototype.parseKeyString = function (keyString) { | ||||
|     return utils.parseKeyString(keyString); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Given any number of identifiers, will return true if they are all equal, otherwise false. | ||||
|  * @param {module:openmct.ObjectAPI~Identifier[]} identifiers | ||||
|  */ | ||||
| ObjectAPI.prototype.areIdsEqual = function (...identifiers) { | ||||
|     return identifiers.map(utils.parseKeyString) | ||||
|         .every(identifier => { | ||||
|             return identifier === identifiers[0] | ||||
|                 || (identifier.namespace === identifiers[0].namespace | ||||
|                     && identifier.key === identifiers[0].key); | ||||
|         }); | ||||
| }; | ||||
|  | ||||
| ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) { | ||||
|     return this.get(identifier).then((domainObject) => { | ||||
|         path.push(domainObject); | ||||
|         let location = domainObject.location; | ||||
|  | ||||
|         if (location) { | ||||
|             return this.getOriginalPath(utils.parseKeyString(location), path); | ||||
|         } else { | ||||
|             return path; | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Uniquely identifies a domain object. | ||||
|  * | ||||
|  * @typedef Identifier | ||||
|  * @memberof module:openmct.ObjectAPI~ | ||||
|  * @property {string} namespace the namespace to/from which this domain | ||||
|  *           object should be loaded/stored. | ||||
|  * @property {string} key a unique identifier for the domain object | ||||
|  *           within that namespace | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * A domain object is an entity of relevance to a user's workflow, that | ||||
|  * should appear as a distinct and meaningful object within the user | ||||
|  * interface. Examples of domain objects are folders, telemetry sensors, | ||||
|  * and so forth. | ||||
|  * | ||||
|  * A few common properties are defined for domain objects. Beyond these, | ||||
|  * individual types of domain objects may add more as they see fit. | ||||
|  * | ||||
|  * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which | ||||
|  *           uniquely identifies this domain object | ||||
|  * @property {string} type the type of domain object | ||||
|  * @property {string} name the human-readable name for this domain object | ||||
|  * @property {string} [creator] the user name of the creator of this domain | ||||
|  *           object | ||||
|  * @property {number} [modified] the time, in milliseconds since the UNIX | ||||
|  *           epoch, at which this domain object was last modified | ||||
|  * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if | ||||
|  *           present, this will be used by the default composition provider | ||||
|  *           to load domain objects | ||||
|  * @typedef DomainObject | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
| function hasAlreadyBeenPersisted(domainObject) { | ||||
|     return domainObject.persisted !== undefined | ||||
|         && domainObject.persisted === domainObject.modified; | ||||
| } | ||||
|  | ||||
| export default ObjectAPI; | ||||
|   | ||||
							
								
								
									
										119
									
								
								src/api/objects/ObjectAPISearchSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/api/objects/ObjectAPISearchSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| import ObjectAPI from './ObjectAPI.js'; | ||||
|  | ||||
| describe("The Object API Search Function", () => { | ||||
|     const MOCK_PROVIDER_KEY = 'mockProvider'; | ||||
|     const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider'; | ||||
|     const MOCK_PROVIDER_SEARCH_DELAY = 15000; | ||||
|     const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000; | ||||
|     const TOTAL_TIME_ELAPSED = 21000; | ||||
|     const BASE_TIME = new Date(2021, 0, 1); | ||||
|  | ||||
|     let objectAPI; | ||||
|     let mockObjectProvider; | ||||
|     let anotherMockObjectProvider; | ||||
|     let mockFallbackProvider; | ||||
|     let fallbackProviderSearchResults; | ||||
|     let resultsPromises; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         jasmine.clock().install(); | ||||
|         jasmine.clock().mockDate(BASE_TIME); | ||||
|  | ||||
|         resultsPromises = []; | ||||
|         fallbackProviderSearchResults = { | ||||
|             hits: [] | ||||
|         }; | ||||
|  | ||||
|         objectAPI = new ObjectAPI(); | ||||
|  | ||||
|         mockObjectProvider = jasmine.createSpyObj("mock object provider", [ | ||||
|             "search" | ||||
|         ]); | ||||
|         anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ | ||||
|             "search" | ||||
|         ]); | ||||
|         mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [ | ||||
|             "superSecretFallbackSearch" | ||||
|         ]); | ||||
|         objectAPI.addProvider('objects', mockObjectProvider); | ||||
|         objectAPI.addProvider('other-objects', anotherMockObjectProvider); | ||||
|         objectAPI.supersecretSetFallbackProvider(mockFallbackProvider); | ||||
|  | ||||
|         mockObjectProvider.search.and.callFake(() => { | ||||
|             return new Promise(resolve => { | ||||
|                 const mockProviderSearch = { | ||||
|                     name: MOCK_PROVIDER_KEY, | ||||
|                     start: new Date() | ||||
|                 }; | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     mockProviderSearch.end = new Date(); | ||||
|  | ||||
|                     return resolve(mockProviderSearch); | ||||
|                 }, MOCK_PROVIDER_SEARCH_DELAY); | ||||
|             }); | ||||
|         }); | ||||
|         anotherMockObjectProvider.search.and.callFake(() => { | ||||
|             return new Promise(resolve => { | ||||
|                 const anotherMockProviderSearch = { | ||||
|                     name: ANOTHER_MOCK_PROVIDER_KEY, | ||||
|                     start: new Date() | ||||
|                 }; | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     anotherMockProviderSearch.end = new Date(); | ||||
|  | ||||
|                     return resolve(anotherMockProviderSearch); | ||||
|                 }, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY); | ||||
|             }); | ||||
|         }); | ||||
|         mockFallbackProvider.superSecretFallbackSearch.and.callFake( | ||||
|             () => new Promise( | ||||
|                 resolve => setTimeout( | ||||
|                     () => resolve(fallbackProviderSearchResults), | ||||
|                     50 | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|         resultsPromises = objectAPI.search('foo'); | ||||
|  | ||||
|         jasmine.clock().tick(TOTAL_TIME_ELAPSED); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         jasmine.clock().uninstall(); | ||||
|     }); | ||||
|  | ||||
|     it("uses each objects given provider's search function", () => { | ||||
|         expect(mockObjectProvider.search).toHaveBeenCalled(); | ||||
|         expect(anotherMockObjectProvider.search).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("uses the fallback indexed search for objects without a search function provided", () => { | ||||
|         expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("provides each providers results as promises that resolve in parallel", async () => { | ||||
|         const results = await Promise.all(resultsPromises); | ||||
|         const mockProviderResults = results.find( | ||||
|             result => result.name === MOCK_PROVIDER_KEY | ||||
|         ); | ||||
|         const anotherMockProviderResults = results.find( | ||||
|             result => result.name === ANOTHER_MOCK_PROVIDER_KEY | ||||
|         ); | ||||
|         const mockProviderStart = mockProviderResults.start.getTime(); | ||||
|         const mockProviderEnd = mockProviderResults.end.getTime(); | ||||
|         const anotherMockProviderStart = anotherMockProviderResults.start.getTime(); | ||||
|         const anotherMockProviderEnd = anotherMockProviderResults.end.getTime(); | ||||
|         const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd) | ||||
|             - Math.min(mockProviderEnd, anotherMockProviderEnd); | ||||
|  | ||||
|         expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd); | ||||
|         expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd); | ||||
|         expect(searchElapsedTime).toBeLessThan( | ||||
|             MOCK_PROVIDER_SEARCH_DELAY | ||||
|             + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY | ||||
|         ); | ||||
|     }); | ||||
| }); | ||||
| @@ -2,12 +2,16 @@ import ObjectAPI from './ObjectAPI.js'; | ||||
|  | ||||
| describe("The Object API", () => { | ||||
|     let objectAPI; | ||||
|     let typeRegistry; | ||||
|     let mockDomainObject; | ||||
|     const TEST_NAMESPACE = "test-namespace"; | ||||
|     const FIFTEEN_MINUTES = 15 * 60 * 1000; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         objectAPI = new ObjectAPI(); | ||||
|         typeRegistry = jasmine.createSpyObj('typeRegistry', [ | ||||
|             'get' | ||||
|         ]); | ||||
|         objectAPI = new ObjectAPI(typeRegistry); | ||||
|         mockDomainObject = { | ||||
|             identifier: { | ||||
|                 namespace: TEST_NAMESPACE, | ||||
| @@ -33,6 +37,7 @@ describe("The Object API", () => { | ||||
|                     "update" | ||||
|                 ]); | ||||
|                 mockProvider.create.and.returnValue(Promise.resolve(true)); | ||||
|                 mockProvider.update.and.returnValue(Promise.resolve(true)); | ||||
|                 objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|             }); | ||||
|             it("Calls 'create' on provider if object is new", () => { | ||||
| @@ -63,12 +68,51 @@ describe("The Object API", () => { | ||||
|     describe("The get function", () => { | ||||
|         describe("when a provider is available", () => { | ||||
|             let mockProvider; | ||||
|             let mockInterceptor; | ||||
|             let anotherMockInterceptor; | ||||
|             let notApplicableMockInterceptor; | ||||
|             beforeEach(() => { | ||||
|                 mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                     "get" | ||||
|                 ]); | ||||
|                 mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); | ||||
|  | ||||
|                 mockInterceptor = jasmine.createSpyObj("mock interceptor", [ | ||||
|                     "appliesTo", | ||||
|                     "invoke" | ||||
|                 ]); | ||||
|                 mockInterceptor.appliesTo.and.returnValue(true); | ||||
|                 mockInterceptor.invoke.and.callFake((identifier, object) => { | ||||
|                     return Object.assign({ | ||||
|                         changed: true | ||||
|                     }, object); | ||||
|                 }); | ||||
|  | ||||
|                 anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [ | ||||
|                     "appliesTo", | ||||
|                     "invoke" | ||||
|                 ]); | ||||
|                 anotherMockInterceptor.appliesTo.and.returnValue(true); | ||||
|                 anotherMockInterceptor.invoke.and.callFake((identifier, object) => { | ||||
|                     return Object.assign({ | ||||
|                         alsoChanged: true | ||||
|                     }, object); | ||||
|                 }); | ||||
|  | ||||
|                 notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [ | ||||
|                     "appliesTo", | ||||
|                     "invoke" | ||||
|                 ]); | ||||
|                 notApplicableMockInterceptor.appliesTo.and.returnValue(false); | ||||
|                 notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => { | ||||
|                     return Object.assign({ | ||||
|                         shouldNotBeChanged: true | ||||
|                     }, object); | ||||
|                 }); | ||||
|                 objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|                 objectAPI.addGetInterceptor(mockInterceptor); | ||||
|                 objectAPI.addGetInterceptor(anotherMockInterceptor); | ||||
|                 objectAPI.addGetInterceptor(notApplicableMockInterceptor); | ||||
|             }); | ||||
|  | ||||
|             it("Caches multiple requests for the same object", () => { | ||||
| @@ -78,6 +122,142 @@ describe("The Object API", () => { | ||||
|                 objectAPI.get(mockDomainObject.identifier); | ||||
|                 expect(mockProvider.get.calls.count()).toBe(1); | ||||
|             }); | ||||
|  | ||||
|             it("applies any applicable interceptors", () => { | ||||
|                 expect(mockDomainObject.changed).toBeUndefined(); | ||||
|                 objectAPI.get(mockDomainObject.identifier).then((object) => { | ||||
|                     expect(object.changed).toBeTrue(); | ||||
|                     expect(object.alsoChanged).toBeTrue(); | ||||
|                     expect(object.shouldNotBeChanged).toBeUndefined(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the mutation API", () => { | ||||
|         let testObject; | ||||
|         let mutable; | ||||
|         let mockProvider; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             objectAPI = new ObjectAPI(typeRegistry); | ||||
|             testObject = { | ||||
|                 identifier: { | ||||
|                     namespace: TEST_NAMESPACE, | ||||
|                     key: 'test-key' | ||||
|                 }, | ||||
|                 name: 'test object', | ||||
|                 otherAttribute: 'other-attribute-value', | ||||
|                 objectAttribute: { | ||||
|                     embeddedObject: { | ||||
|                         embeddedKey: 'embedded-value' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                 "get", | ||||
|                 "create", | ||||
|                 "update" | ||||
|             ]); | ||||
|             mockProvider.get.and.returnValue(Promise.resolve(testObject)); | ||||
|             objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|  | ||||
|             return objectAPI.getMutable(testObject.identifier) | ||||
|                 .then(object => { | ||||
|                     mutable = object; | ||||
|  | ||||
|                     return mutable; | ||||
|                 }); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             mutable.$destroy(); | ||||
|         }); | ||||
|  | ||||
|         it('mutates the original object', () => { | ||||
|             const MUTATED_NAME = 'mutated name'; | ||||
|             objectAPI.mutate(testObject, 'name', MUTATED_NAME); | ||||
|             expect(testObject.name).toBe(MUTATED_NAME); | ||||
|         }); | ||||
|  | ||||
|         describe ('uses a MutableDomainObject', () => { | ||||
|             it('and retains properties of original object ', function () { | ||||
|                 expect(hasOwnProperty(mutable, 'identifier')).toBe(true); | ||||
|                 expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true); | ||||
|                 expect(mutable.identifier).toEqual(testObject.identifier); | ||||
|                 expect(mutable.otherAttribute).toEqual(testObject.otherAttribute); | ||||
|             }); | ||||
|  | ||||
|             it('that is identical to original object when serialized', function () { | ||||
|                 expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject)); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('uses events', function () { | ||||
|             let testObjectDuplicate; | ||||
|             let mutableSecondInstance; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 // Duplicate object to guarantee we are not sharing object instance, which would invalidate test | ||||
|                 testObjectDuplicate = JSON.parse(JSON.stringify(testObject)); | ||||
|                 mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate); | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 mutableSecondInstance.$destroy(); | ||||
|             }); | ||||
|  | ||||
|             it('to stay synchronized when mutated', function () { | ||||
|                 objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); | ||||
|                 expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a property changes', function () { | ||||
|                 let mutationCallback = jasmine.createSpy('mutation-callback'); | ||||
|                 let unlisten; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     mutationCallback.and.callFake(resolve); | ||||
|                     unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); | ||||
|                     objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); | ||||
|                     unlisten(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a child property has changed', function () { | ||||
|                 let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                 let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                 let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                 let listeners = []; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                     objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                     expect(embeddedObjectCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedKey: 'updated-embedded-value' | ||||
|                     }); | ||||
|                     expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedObject: { | ||||
|                             embeddedKey: 'updated-embedded-value' | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
|                     listeners.forEach(listener => listener()); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| function hasOwnProperty(object, property) { | ||||
|     return Object.prototype.hasOwnProperty.call(object, property); | ||||
| } | ||||
|   | ||||
| @@ -48,7 +48,7 @@ define([ | ||||
|             this.providers.push(function () { | ||||
|                 return key; | ||||
|             }); | ||||
|         } else if (_.isFunction(key)) { | ||||
|         } else if (typeof key === "function") { | ||||
|             this.providers.push(key); | ||||
|         } | ||||
|     }; | ||||
|   | ||||
| @@ -6,6 +6,9 @@ class Dialog extends Overlay { | ||||
|     constructor({iconClass, message, title, hint, timestamp, ...options}) { | ||||
|  | ||||
|         let component = new Vue({ | ||||
|             components: { | ||||
|                 DialogComponent: DialogComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 iconClass, | ||||
|                 message, | ||||
| @@ -13,9 +16,6 @@ class Dialog extends Overlay { | ||||
|                 hint, | ||||
|                 timestamp | ||||
|             }, | ||||
|             components: { | ||||
|                 DialogComponent: DialogComponent | ||||
|             }, | ||||
|             template: '<dialog-component></dialog-component>' | ||||
|         }).$mount(); | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class OverlayAPI { | ||||
|                 this.dismissLastOverlay(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -127,6 +128,7 @@ class OverlayAPI { | ||||
|  | ||||
|         return progressDialog; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default OverlayAPI; | ||||
|   | ||||
| @@ -7,6 +7,9 @@ let component; | ||||
| class ProgressDialog extends Overlay { | ||||
|     constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) { | ||||
|         component = new Vue({ | ||||
|             components: { | ||||
|                 ProgressDialogComponent: ProgressDialogComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 iconClass, | ||||
|                 message, | ||||
| @@ -14,9 +17,6 @@ class ProgressDialog extends Overlay { | ||||
|                 hint, | ||||
|                 timestamp | ||||
|             }, | ||||
|             components: { | ||||
|                 ProgressDialogComponent: ProgressDialogComponent | ||||
|             }, | ||||
|             data() { | ||||
|                 return { | ||||
|                     model: { | ||||
|   | ||||
| @@ -38,12 +38,12 @@ | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['dismiss', 'element', 'buttons', 'dismissable'], | ||||
|     data: function () { | ||||
|         return { | ||||
|             focusIndex: -1 | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['dismiss', 'element', 'buttons', 'dismissable'], | ||||
|     mounted() { | ||||
|         const element = this.$refs.element; | ||||
|         element.appendChild(this.element); | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| export default class StatusAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._statusCache = {}; | ||||
|  | ||||
|         this.get = this.get.bind(this); | ||||
|         this.set = this.set.bind(this); | ||||
|         this.observe = this.observe.bind(this); | ||||
|     } | ||||
|  | ||||
|     get(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         return this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     set(identifier, value) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = value; | ||||
|         this.emit(keyString, value); | ||||
|     } | ||||
|  | ||||
|     delete(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = undefined; | ||||
|         this.emit(keyString, undefined); | ||||
|         delete this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     observe(identifier, callback) { | ||||
|         let key = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this.on(key, callback); | ||||
|  | ||||
|         return () => { | ||||
|             this.off(key, callback); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import StatusAPI from './StatusAPI.js'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe("The Status API", () => { | ||||
|     let statusAPI; | ||||
|     let openmct; | ||||
|     let identifier; | ||||
|     let status; | ||||
|     let status2; | ||||
|     let callback; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         statusAPI = new StatusAPI(openmct); | ||||
|         identifier = { | ||||
|             namespace: "test-namespace", | ||||
|             key: "test-key" | ||||
|         }; | ||||
|         status = "test-status"; | ||||
|         status2 = 'test-status-deux'; | ||||
|         callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("set function", () => { | ||||
|         it("sets status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("get function", () => { | ||||
|         it("returns status for identifier", () => { | ||||
|             statusAPI.set(identifier, status2); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("delete function", () => { | ||||
|         it("deletes status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|             resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toBeUndefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("observe function", () => { | ||||
|  | ||||
|         it("allows callbacks to be attached to status set and delete events", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(undefined); | ||||
|             unsubscribe(); | ||||
|         }); | ||||
|  | ||||
|         it("returns a unsubscribe function", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             unsubscribe(); | ||||
|  | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledTimes(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -21,12 +21,14 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
|     './TelemetryValueFormatter', | ||||
|     './DefaultMetadataProvider', | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     CustomStringFormatter, | ||||
|     TelemetryMetadataManager, | ||||
|     TelemetryValueFormatter, | ||||
|     DefaultMetadataProvider, | ||||
| @@ -142,6 +144,17 @@ define([ | ||||
|         this.valueFormatterCache = new WeakMap(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return Custom String Formatter | ||||
|      * | ||||
|      * @param {Object} valueMetadata valueMetadata for given telemetry object | ||||
|      * @param {string} format custom formatter string (eg: %.4f, <s etc.) | ||||
|      * @returns {CustomStringFormatter} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { | ||||
|         return new CustomStringFormatter.default(this.openmct, valueMetadata, format); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return true if the given domainObject is a telemetry object.  A telemetry | ||||
|      * object is any object which has telemetry metadata-- regardless of whether | ||||
| @@ -400,6 +413,17 @@ define([ | ||||
|         return _.sortBy(options, sortKeys); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatService = function () { | ||||
|         if (!this.formatService) { | ||||
|             this.formatService = this.openmct.$injector.get('formatService'); | ||||
|         } | ||||
|  | ||||
|         return this.formatService; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given valueMetadata. | ||||
|      * | ||||
| @@ -407,19 +431,27 @@ define([ | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { | ||||
|         if (!this.valueFormatterCache.has(valueMetadata)) { | ||||
|             if (!this.formatService) { | ||||
|                 this.formatService = this.openmct.$injector.get('formatService'); | ||||
|             } | ||||
|  | ||||
|             this.valueFormatterCache.set( | ||||
|                 valueMetadata, | ||||
|                 new TelemetryValueFormatter(valueMetadata, this.formatService) | ||||
|                 new TelemetryValueFormatter(valueMetadata, this.getFormatService()) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return this.valueFormatterCache.get(valueMetadata); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given key. | ||||
|      * @param {string} key | ||||
|      * | ||||
|      * @returns {Format} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatter = function (key) { | ||||
|         const formatMap = this.getFormatService().formatMap; | ||||
|  | ||||
|         return formatMap[key]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a format map of all value formatters for a given piece of telemetry | ||||
|      * metadata. | ||||
|   | ||||
| @@ -44,11 +44,15 @@ export default function LADTableViewProvider(openmct) { | ||||
|                             LadTableComponent: LadTable | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject, | ||||
|                             objectPath | ||||
|                             openmct | ||||
|                         }, | ||||
|                         template: '<lad-table-component></lad-table-component>' | ||||
|                         data: () => { | ||||
|                             return { | ||||
|                                 domainObject, | ||||
|                                 objectPath | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|     class="js-lad-table__body__row" | ||||
|     @contextmenu.prevent="showContextMenu" | ||||
| > | ||||
|     <td class="js-first-data">{{ name }}</td> | ||||
|     <td class="js-first-data">{{ domainObject.name }}</td> | ||||
|     <td class="js-second-data">{{ formattedTimestamp }}</td> | ||||
|     <td | ||||
|         class="js-third-data" | ||||
| @@ -44,17 +44,22 @@ | ||||
| <script> | ||||
|  | ||||
| const CONTEXT_MENU_ACTIONS = [ | ||||
|     'viewDatumAction', | ||||
|     'viewHistoricalData', | ||||
|     'remove' | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         objectPath: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         }, | ||||
|         hasUnits: { | ||||
|             type: Boolean, | ||||
|             requred: true | ||||
| @@ -65,7 +70,6 @@ export default { | ||||
|         currentObjectPath.unshift(this.domainObject); | ||||
|  | ||||
|         return { | ||||
|             name: this.domainObject.name, | ||||
|             timestamp: undefined, | ||||
|             value: '---', | ||||
|             valueClass: '', | ||||
| @@ -88,14 +92,6 @@ export default { | ||||
|             .telemetry | ||||
|             .limitEvaluator(this.domainObject); | ||||
|  | ||||
|         this.stopWatchingMutation = this.openmct | ||||
|             .objects | ||||
|             .observe( | ||||
|                 this.domainObject, | ||||
|                 '*', | ||||
|                 this.updateName | ||||
|             ); | ||||
|  | ||||
|         this.openmct.time.on('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.on('bounds', this.updateBounds); | ||||
|  | ||||
| @@ -118,7 +114,6 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.stopWatchingMutation(); | ||||
|         this.unsubscribe(); | ||||
|         this.openmct.time.off('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.off('bounds', this.updateBounds); | ||||
| @@ -129,6 +124,7 @@ export default { | ||||
|             let limit; | ||||
|  | ||||
|             if (this.shouldUpdate(newTimestamp)) { | ||||
|                 this.datum = datum; | ||||
|                 this.timestamp = newTimestamp; | ||||
|                 this.value = this.formats[this.valueKey].format(datum); | ||||
|                 limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); | ||||
| @@ -158,9 +154,6 @@ export default { | ||||
|                 }) | ||||
|                 .then((array) => this.updateValues(array[array.length - 1])); | ||||
|         }, | ||||
|         updateName(name) { | ||||
|             this.name = name; | ||||
|         }, | ||||
|         updateBounds(bounds, isTick) { | ||||
|             this.bounds = bounds; | ||||
|             if (!isTick) { | ||||
| @@ -175,8 +168,25 @@ export default { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: () => { | ||||
|                             return this.datum; | ||||
|                         } | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|             let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             let allActions = actionCollection.getActionsObject(); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|         }, | ||||
|         resetValues() { | ||||
|             this.value = '---'; | ||||
|   | ||||
| @@ -36,6 +36,7 @@ | ||||
|                 v-for="item in items" | ||||
|                 :key="item.key" | ||||
|                 :domain-object="item.domainObject" | ||||
|                 :object-path="objectPath" | ||||
|                 :has-units="hasUnits" | ||||
|             /> | ||||
|         </tbody> | ||||
| @@ -47,10 +48,20 @@ | ||||
| import LadRow from './LADRow.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'objectPath'], | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         objectPath: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             items: [] | ||||
|   | ||||
| @@ -57,10 +57,10 @@ | ||||
| import LadRow from './LADRow.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     components: { | ||||
|         LadRow | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             ladTableObjects: [], | ||||
|   | ||||
| @@ -19,342 +19,352 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import AutoflowTabularPlugin from './AutoflowTabularPlugin'; | ||||
| import AutoflowTabularConstants from './AutoflowTabularConstants'; | ||||
| import $ from 'zepto'; | ||||
| import DOMObserver from './dom-observer'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     spyOnBuiltins | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularPlugin', | ||||
|     './AutoflowTabularConstants', | ||||
|     '../../MCT', | ||||
|     'zepto', | ||||
|     './dom-observer' | ||||
| ], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) { | ||||
|     describe("AutoflowTabularPlugin", function () { | ||||
|         let testType; | ||||
|         let testObject; | ||||
|         let mockmct; | ||||
| describe("AutoflowTabularPlugin", () => { | ||||
|     let testType; | ||||
|     let testObject; | ||||
|     let mockmct; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testType = "some-type"; | ||||
|             testObject = { type: testType }; | ||||
|             mockmct = new MCT(); | ||||
|             spyOn(mockmct.composition, 'get'); | ||||
|             spyOn(mockmct.objectViews, 'addProvider'); | ||||
|             spyOn(mockmct.telemetry, 'getMetadata'); | ||||
|             spyOn(mockmct.telemetry, 'getValueFormatter'); | ||||
|             spyOn(mockmct.telemetry, 'limitEvaluator'); | ||||
|             spyOn(mockmct.telemetry, 'request'); | ||||
|             spyOn(mockmct.telemetry, 'subscribe'); | ||||
|     beforeEach(() => { | ||||
|         testType = "some-type"; | ||||
|         testObject = { type: testType }; | ||||
|         mockmct = createOpenMct(); | ||||
|         spyOn(mockmct.composition, 'get'); | ||||
|         spyOn(mockmct.objectViews, 'addProvider'); | ||||
|         spyOn(mockmct.telemetry, 'getMetadata'); | ||||
|         spyOn(mockmct.telemetry, 'getValueFormatter'); | ||||
|         spyOn(mockmct.telemetry, 'limitEvaluator'); | ||||
|         spyOn(mockmct.telemetry, 'request'); | ||||
|         spyOn(mockmct.telemetry, 'subscribe'); | ||||
|  | ||||
|             const plugin = new AutoflowTabularPlugin({ type: testType }); | ||||
|             plugin(mockmct); | ||||
|         const plugin = new AutoflowTabularPlugin({ type: testType }); | ||||
|         plugin(mockmct); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(mockmct); | ||||
|     }); | ||||
|  | ||||
|     it("installs a view provider", () => { | ||||
|         expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     describe("installs a view provider which", () => { | ||||
|         let provider; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             provider = | ||||
|                 mockmct.objectViews.addProvider.calls.mostRecent().args[0]; | ||||
|         }); | ||||
|  | ||||
|         it("installs a view provider", function () { | ||||
|             expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); | ||||
|         it("applies its view to the type from options", () => { | ||||
|             expect(provider.canView(testObject)).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         describe("installs a view provider which", function () { | ||||
|             let provider; | ||||
|         it("does not apply to other types", () => { | ||||
|             expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|         }); | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 provider = | ||||
|                     mockmct.objectViews.addProvider.calls.mostRecent().args[0]; | ||||
|             }); | ||||
|         describe("provides a view which", () => { | ||||
|             let testKeys; | ||||
|             let testChildren; | ||||
|             let testContainer; | ||||
|             let testHistories; | ||||
|             let mockComposition; | ||||
|             let mockMetadata; | ||||
|             let mockEvaluator; | ||||
|             let mockUnsubscribes; | ||||
|             let callbacks; | ||||
|             let view; | ||||
|             let domObserver; | ||||
|  | ||||
|             it("applies its view to the type from options", function () { | ||||
|                 expect(provider.canView(testObject)).toBe(true); | ||||
|             }); | ||||
|             function waitsForChange() { | ||||
|                 return new Promise(function (resolve) { | ||||
|                     window.requestAnimationFrame(resolve); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             it("does not apply to other types", function () { | ||||
|                 expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|             }); | ||||
|             function emitEvent(mockEmitter, type, event) { | ||||
|                 mockEmitter.on.calls.all().forEach((call) => { | ||||
|                     if (call.args[0] === type) { | ||||
|                         call.args[1](event); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             describe("provides a view which", function () { | ||||
|                 let testKeys; | ||||
|                 let testChildren; | ||||
|                 let testContainer; | ||||
|                 let testHistories; | ||||
|                 let mockComposition; | ||||
|                 let mockMetadata; | ||||
|                 let mockEvaluator; | ||||
|                 let mockUnsubscribes; | ||||
|                 let callbacks; | ||||
|                 let view; | ||||
|                 let domObserver; | ||||
|             beforeEach((done) => { | ||||
|                 callbacks = {}; | ||||
|  | ||||
|                 function waitsForChange() { | ||||
|                     return new Promise(function (resolve) { | ||||
|                         window.requestAnimationFrame(resolve); | ||||
|                 spyOnBuiltins(['requestAnimationFrame']); | ||||
|                 window.requestAnimationFrame.and.callFake((callBack) => { | ||||
|                     callBack(); | ||||
|                 }); | ||||
|  | ||||
|                 testObject = { type: 'some-type' }; | ||||
|                 testKeys = ['abc', 'def', 'xyz']; | ||||
|                 testChildren = testKeys.map((key) => { | ||||
|                     return { | ||||
|                         identifier: { | ||||
|                             namespace: "test", | ||||
|                             key: key | ||||
|                         }, | ||||
|                         name: "Object " + key | ||||
|                     }; | ||||
|                 }); | ||||
|                 testContainer = $('<div>')[0]; | ||||
|                 domObserver = new DOMObserver(testContainer); | ||||
|  | ||||
|                 testHistories = testKeys.reduce((histories, key, index) => { | ||||
|                     histories[key] = { | ||||
|                         key: key, | ||||
|                         range: index + 10, | ||||
|                         domain: key + index | ||||
|                     }; | ||||
|  | ||||
|                     return histories; | ||||
|                 }, {}); | ||||
|  | ||||
|                 mockComposition = | ||||
|                     jasmine.createSpyObj('composition', ['load', 'on', 'off']); | ||||
|                 mockMetadata = | ||||
|                     jasmine.createSpyObj('metadata', ['valuesForHints']); | ||||
|  | ||||
|                 mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); | ||||
|                 mockUnsubscribes = testKeys.reduce((map, key) => { | ||||
|                     map[key] = jasmine.createSpy('unsubscribe-' + key); | ||||
|  | ||||
|                     return map; | ||||
|                 }, {}); | ||||
|  | ||||
|                 mockmct.composition.get.and.returnValue(mockComposition); | ||||
|                 mockComposition.load.and.callFake(() => { | ||||
|                     testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); | ||||
|  | ||||
|                     return Promise.resolve(testChildren); | ||||
|                 }); | ||||
|  | ||||
|                 mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); | ||||
|                 mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => { | ||||
|                     const mockFormatter = jasmine.createSpyObj('formatter', ['format']); | ||||
|                     mockFormatter.format.and.callFake((datum) => { | ||||
|                         return datum[metadatum.hint]; | ||||
|                     }); | ||||
|  | ||||
|                     return mockFormatter; | ||||
|                 }); | ||||
|                 mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); | ||||
|                 mockmct.telemetry.subscribe.and.callFake((obj, callback) => { | ||||
|                     const key = obj.identifier.key; | ||||
|                     callbacks[key] = callback; | ||||
|  | ||||
|                     return mockUnsubscribes[key]; | ||||
|                 }); | ||||
|                 mockmct.telemetry.request.and.callFake((obj, request) => { | ||||
|                     const key = obj.identifier.key; | ||||
|  | ||||
|                     return Promise.resolve([testHistories[key]]); | ||||
|                 }); | ||||
|                 mockMetadata.valuesForHints.and.callFake((hints) => { | ||||
|                     return [{ hint: hints[0] }]; | ||||
|                 }); | ||||
|  | ||||
|                 view = provider.view(testObject); | ||||
|                 view.show(testContainer); | ||||
|  | ||||
|                 return done(); | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 domObserver.destroy(); | ||||
|             }); | ||||
|  | ||||
|             it("populates its container", () => { | ||||
|                 expect(testContainer.children.length > 0).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             describe("when rows have been populated", () => { | ||||
|                 function rowsMatch() { | ||||
|                     const rows = $(testContainer).find(".l-autoflow-row").length; | ||||
|  | ||||
|                     return rows === testChildren.length; | ||||
|                 } | ||||
|  | ||||
|                 function emitEvent(mockEmitter, type, event) { | ||||
|                     mockEmitter.on.calls.all().forEach(function (call) { | ||||
|                         if (call.args[0] === type) { | ||||
|                             call.args[1](event); | ||||
|                         } | ||||
|                     }); | ||||
|                 it("shows one row per child object", () => { | ||||
|                     return domObserver.when(rowsMatch); | ||||
|                 }); | ||||
|  | ||||
|                 // it("adds rows on composition change", () => { | ||||
|                 //     const child = { | ||||
|                 //         identifier: { | ||||
|                 //             namespace: "test", | ||||
|                 //             key: "123" | ||||
|                 //         }, | ||||
|                 //         name: "Object 123" | ||||
|                 //     }; | ||||
|                 //     testChildren.push(child); | ||||
|                 //     emitEvent(mockComposition, 'add', child); | ||||
|  | ||||
|                 //     return domObserver.when(rowsMatch); | ||||
|                 // }); | ||||
|  | ||||
|                 it("removes rows on composition change", () => { | ||||
|                     const child = testChildren.pop(); | ||||
|                     emitEvent(mockComposition, 'remove', child.identifier); | ||||
|  | ||||
|                     return domObserver.when(rowsMatch); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("removes subscriptions when destroyed", () => { | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|                 view.destroy(); | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(mockUnsubscribes[key]).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("provides a button to change column width", () => { | ||||
|                 const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|                 const nextWidth = | ||||
|                     initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|  | ||||
|                 expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                     .toEqual(initialWidth + 'px'); | ||||
|  | ||||
|                 $(testContainer).find('.change-column-width').click(); | ||||
|  | ||||
|                 function widthHasChanged() { | ||||
|                     const width = $(testContainer).find('.l-autoflow-col').css('width'); | ||||
|  | ||||
|                     return width !== initialWidth + 'px'; | ||||
|                 } | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     callbacks = {}; | ||||
|  | ||||
|                     testObject = { type: 'some-type' }; | ||||
|                     testKeys = ['abc', 'def', 'xyz']; | ||||
|                     testChildren = testKeys.map(function (key) { | ||||
|                         return { | ||||
|                             identifier: { | ||||
|                                 namespace: "test", | ||||
|                                 key: key | ||||
|                             }, | ||||
|                             name: "Object " + key | ||||
|                         }; | ||||
|                 return domObserver.when(widthHasChanged) | ||||
|                     .then(() => { | ||||
|                         expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                             .toEqual(nextWidth + 'px'); | ||||
|                     }); | ||||
|                     testContainer = $('<div>')[0]; | ||||
|                     domObserver = new DOMObserver(testContainer); | ||||
|             }); | ||||
|  | ||||
|                     testHistories = testKeys.reduce(function (histories, key, index) { | ||||
|                         histories[key] = { | ||||
|                             key: key, | ||||
|                             range: index + 10, | ||||
|                             domain: key + index | ||||
|                         }; | ||||
|             it("subscribes to all child objects", () => { | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(callbacks[key]).toEqual(jasmine.any(Function)); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                         return histories; | ||||
|                     }, {}); | ||||
|             it("displays historical telemetry", () => { | ||||
|                 function rowTextDefined() { | ||||
|                     return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== ""; | ||||
|                 } | ||||
|  | ||||
|                     mockComposition = | ||||
|                         jasmine.createSpyObj('composition', ['load', 'on', 'off']); | ||||
|                     mockMetadata = | ||||
|                         jasmine.createSpyObj('metadata', ['valuesForHints']); | ||||
|  | ||||
|                     mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); | ||||
|                     mockUnsubscribes = testKeys.reduce(function (map, key) { | ||||
|                         map[key] = jasmine.createSpy('unsubscribe-' + key); | ||||
|  | ||||
|                         return map; | ||||
|                     }, {}); | ||||
|  | ||||
|                     mockmct.composition.get.and.returnValue(mockComposition); | ||||
|                     mockComposition.load.and.callFake(function () { | ||||
|                         testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); | ||||
|  | ||||
|                         return Promise.resolve(testChildren); | ||||
|                 return domObserver.when(rowTextDefined).then(() => { | ||||
|                     testKeys.forEach((key, index) => { | ||||
|                         const datum = testHistories[key]; | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.text()).toEqual(String(datum.range)); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                     mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); | ||||
|                     mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) { | ||||
|                         const mockFormatter = jasmine.createSpyObj('formatter', ['format']); | ||||
|                         mockFormatter.format.and.callFake(function (datum) { | ||||
|                             return datum[metadatum.hint]; | ||||
|                         }); | ||||
|  | ||||
|                         return mockFormatter; | ||||
|                     }); | ||||
|                     mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); | ||||
|                     mockmct.telemetry.subscribe.and.callFake(function (obj, callback) { | ||||
|                         const key = obj.identifier.key; | ||||
|                         callbacks[key] = callback; | ||||
|  | ||||
|                         return mockUnsubscribes[key]; | ||||
|                     }); | ||||
|                     mockmct.telemetry.request.and.callFake(function (obj, request) { | ||||
|                         const key = obj.identifier.key; | ||||
|  | ||||
|                         return Promise.resolve([testHistories[key]]); | ||||
|                     }); | ||||
|                     mockMetadata.valuesForHints.and.callFake(function (hints) { | ||||
|                         return [{ hint: hints[0] }]; | ||||
|                     }); | ||||
|  | ||||
|                     view = provider.view(testObject); | ||||
|                     view.show(testContainer); | ||||
|  | ||||
|                     return waitsForChange(); | ||||
|             it("displays incoming telemetry", () => { | ||||
|                 const testData = testKeys.map((key, index) => { | ||||
|                     return { | ||||
|                         key: key, | ||||
|                         range: index * 100, | ||||
|                         domain: key + index | ||||
|                     }; | ||||
|                 }); | ||||
|  | ||||
|                 afterEach(function () { | ||||
|                     domObserver.destroy(); | ||||
|                 testData.forEach((datum) => { | ||||
|                     callbacks[datum.key](datum); | ||||
|                 }); | ||||
|  | ||||
|                 it("populates its container", function () { | ||||
|                     expect(testContainer.children.length > 0).toBe(true); | ||||
|                 return waitsForChange().then(() => { | ||||
|                     testData.forEach((datum, index) => { | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.text()).toEqual(String(datum.range)); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 describe("when rows have been populated", function () { | ||||
|                     function rowsMatch() { | ||||
|                         const rows = $(testContainer).find(".l-autoflow-row").length; | ||||
|  | ||||
|                         return rows === testChildren.length; | ||||
|                     } | ||||
|  | ||||
|                     it("shows one row per child object", function () { | ||||
|                         return domObserver.when(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("adds rows on composition change", function () { | ||||
|                         const child = { | ||||
|                             identifier: { | ||||
|                                 namespace: "test", | ||||
|                                 key: "123" | ||||
|                             }, | ||||
|                             name: "Object 123" | ||||
|                         }; | ||||
|                         testChildren.push(child); | ||||
|                         emitEvent(mockComposition, 'add', child); | ||||
|  | ||||
|                         return domObserver.when(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("removes rows on composition change", function () { | ||||
|                         const child = testChildren.pop(); | ||||
|                         emitEvent(mockComposition, 'remove', child.identifier); | ||||
|  | ||||
|                         return domObserver.when(rowsMatch); | ||||
|             it("updates classes for limit violations", () => { | ||||
|                 const testClass = "some-limit-violation"; | ||||
|                 mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); | ||||
|                 testKeys.forEach((key) => { | ||||
|                     callbacks[key]({ | ||||
|                         range: 'foo', | ||||
|                         domain: 'bar' | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("removes subscriptions when destroyed", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); | ||||
|                     }); | ||||
|                     view.destroy(); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).toHaveBeenCalled(); | ||||
|                 return waitsForChange().then(() => { | ||||
|                     testKeys.forEach((datum, index) => { | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.hasClass(testClass)).toBe(true); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 it("provides a button to change column width", function () { | ||||
|                     const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|                     const nextWidth = | ||||
|                         initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|             it("automatically flows to new columns", () => { | ||||
|                 const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|                 const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|                 const count = testKeys.length; | ||||
|                 const $container = $(testContainer); | ||||
|                 let promiseChain = Promise.resolve(); | ||||
|  | ||||
|                     expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                         .toEqual(initialWidth + 'px'); | ||||
|                 function columnsHaveAutoflowed() { | ||||
|                     const itemsHeight = $container.find('.l-autoflow-items').height(); | ||||
|                     const availableHeight = itemsHeight - sliderHeight; | ||||
|                     const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); | ||||
|                     const columns = Math.ceil(count / availableRows); | ||||
|  | ||||
|                     $(testContainer).find('.change-column-width').click(); | ||||
|                     return $container.find('.l-autoflow-col').length === columns; | ||||
|                 } | ||||
|  | ||||
|                     function widthHasChanged() { | ||||
|                         const width = $(testContainer).find('.l-autoflow-col').css('width'); | ||||
|  | ||||
|                         return width !== initialWidth + 'px'; | ||||
|                     } | ||||
|  | ||||
|                     return domObserver.when(widthHasChanged) | ||||
|                         .then(function () { | ||||
|                             expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                                 .toEqual(nextWidth + 'px'); | ||||
|                         }); | ||||
|                 $container.find('.abs').css({ | ||||
|                     position: 'absolute', | ||||
|                     left: '0px', | ||||
|                     right: '0px', | ||||
|                     top: '0px', | ||||
|                     bottom: '0px' | ||||
|                 }); | ||||
|                 $container.css({ position: 'absolute' }); | ||||
|  | ||||
|                 it("subscribes to all child objects", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(callbacks[key]).toEqual(jasmine.any(Function)); | ||||
|                     }); | ||||
|                 $container.appendTo(document.body); | ||||
|  | ||||
|                 function setHeight(height) { | ||||
|                     $container.css('height', height + 'px'); | ||||
|  | ||||
|                     return domObserver.when(columnsHaveAutoflowed); | ||||
|                 } | ||||
|  | ||||
|                 for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { | ||||
|                     // eslint-disable-next-line no-invalid-this | ||||
|                     promiseChain = promiseChain.then(setHeight.bind(this, height)); | ||||
|                 } | ||||
|  | ||||
|                 return promiseChain.then(() => { | ||||
|                     $container.remove(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 it("displays historical telemetry", function () { | ||||
|                     function rowTextDefined() { | ||||
|                         return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== ""; | ||||
|                     } | ||||
|  | ||||
|                     return domObserver.when(rowTextDefined).then(function () { | ||||
|                         testKeys.forEach(function (key, index) { | ||||
|                             const datum = testHistories[key]; | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("displays incoming telemetry", function () { | ||||
|                     const testData = testKeys.map(function (key, index) { | ||||
|                         return { | ||||
|                             key: key, | ||||
|                             range: index * 100, | ||||
|                             domain: key + index | ||||
|                         }; | ||||
|                     }); | ||||
|  | ||||
|                     testData.forEach(function (datum) { | ||||
|                         callbacks[datum.key](datum); | ||||
|                     }); | ||||
|  | ||||
|                     return waitsForChange().then(function () { | ||||
|                         testData.forEach(function (datum, index) { | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates classes for limit violations", function () { | ||||
|                     const testClass = "some-limit-violation"; | ||||
|                     mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         callbacks[key]({ | ||||
|                             range: 'foo', | ||||
|                             domain: 'bar' | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     return waitsForChange().then(function () { | ||||
|                         testKeys.forEach(function (datum, index) { | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.hasClass(testClass)).toBe(true); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("automatically flows to new columns", function () { | ||||
|                     const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|                     const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|                     const count = testKeys.length; | ||||
|                     const $container = $(testContainer); | ||||
|                     let promiseChain = Promise.resolve(); | ||||
|  | ||||
|                     function columnsHaveAutoflowed() { | ||||
|                         const itemsHeight = $container.find('.l-autoflow-items').height(); | ||||
|                         const availableHeight = itemsHeight - sliderHeight; | ||||
|                         const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); | ||||
|                         const columns = Math.ceil(count / availableRows); | ||||
|  | ||||
|                         return $container.find('.l-autoflow-col').length === columns; | ||||
|                     } | ||||
|  | ||||
|                     $container.find('.abs').css({ | ||||
|                         position: 'absolute', | ||||
|                         left: '0px', | ||||
|                         right: '0px', | ||||
|                         top: '0px', | ||||
|                         bottom: '0px' | ||||
|                     }); | ||||
|                     $container.css({ position: 'absolute' }); | ||||
|  | ||||
|                     $container.appendTo(document.body); | ||||
|  | ||||
|                     function setHeight(height) { | ||||
|                         $container.css('height', height + 'px'); | ||||
|  | ||||
|                         return domObserver.when(columnsHaveAutoflowed); | ||||
|                     } | ||||
|  | ||||
|                     for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { | ||||
|                         // eslint-disable-next-line no-invalid-this | ||||
|                         promiseChain = promiseChain.then(setHeight.bind(this, height)); | ||||
|                     } | ||||
|  | ||||
|                     return promiseChain.then(function () { | ||||
|                         $container.remove(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("loads composition exactly once", function () { | ||||
|                     const testObj = testChildren.pop(); | ||||
|                     emitEvent(mockComposition, 'remove', testObj.identifier); | ||||
|                     testChildren.push(testObj); | ||||
|                     emitEvent(mockComposition, 'add', testObj); | ||||
|                     expect(mockComposition.load.calls.count()).toEqual(1); | ||||
|                 }); | ||||
|             it("loads composition exactly once", () => { | ||||
|                 const testObj = testChildren.pop(); | ||||
|                 emitEvent(mockComposition, 'remove', testObj.identifier); | ||||
|                 testChildren.push(testObj); | ||||
|                 emitEvent(mockComposition, 'add', testObj); | ||||
|                 expect(mockComposition.load.calls.count()).toEqual(1); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| export default class ClearDataAction { | ||||
|     constructor(openmct, appliesToObjects) { | ||||
|         this.name = 'Clear Data for Object'; | ||||
|         this.key = 'clear-data-action'; | ||||
|         this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; | ||||
|         this.cssClass = 'icon-clear-data'; | ||||
|  | ||||
|   | ||||
| @@ -37,12 +37,12 @@ define([ | ||||
|         return function install(openmct) { | ||||
|             if (installIndicator) { | ||||
|                 let component = new Vue ({ | ||||
|                     provide: { | ||||
|                         openmct | ||||
|                     }, | ||||
|                     components: { | ||||
|                         GlobalClearIndicator: GlobaClearIndicator.default | ||||
|                     }, | ||||
|                     provide: { | ||||
|                         openmct | ||||
|                     }, | ||||
|                     template: '<GlobalClearIndicator></GlobalClearIndicator>' | ||||
|                 }); | ||||
|  | ||||
| @@ -53,7 +53,7 @@ define([ | ||||
|                 openmct.indicators.add(indicator); | ||||
|             } | ||||
|  | ||||
|             openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|             openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js'; | ||||
| describe('When the Clear Data Plugin is installed,', function () { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         contextMenu: mockContextMenuProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         } | ||||
| @@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () { | ||||
|     it('Clear Data context menu action is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockContextMenuProvider.registerAction).toHaveBeenCalled(); | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', function () { | ||||
|   | ||||
| @@ -75,7 +75,8 @@ export default class Condition extends EventEmitter { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (this.isTelemetryUsed(datum.id)) { | ||||
|         // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate | ||||
|         if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) { | ||||
|  | ||||
|             this.criteria.forEach(criterion => { | ||||
|                 if (this.isAnyOrAllTelemetry(criterion)) { | ||||
| @@ -93,6 +94,12 @@ export default class Condition extends EventEmitter { | ||||
|         return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')); | ||||
|     } | ||||
|  | ||||
|     hasNoTelemetry() { | ||||
|         return this.criteria.every((criterion) => { | ||||
|             return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === ''; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     isTelemetryUsed(id) { | ||||
|         return this.criteria.some(criterion => { | ||||
|             return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id; | ||||
| @@ -250,10 +257,17 @@ export default class Condition extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     getTriggerDescription() { | ||||
|         return { | ||||
|             conjunction: TRIGGER_CONJUNCTION[this.trigger], | ||||
|             prefix: `${TRIGGER_LABEL[this.trigger]}: ` | ||||
|         }; | ||||
|         if (this.trigger) { | ||||
|             return { | ||||
|                 conjunction: TRIGGER_CONJUNCTION[this.trigger], | ||||
|                 prefix: `${TRIGGER_LABEL[this.trigger]}: ` | ||||
|             }; | ||||
|         } else { | ||||
|             return { | ||||
|                 conjunction: '', | ||||
|                 prefix: '' | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     requestLADConditionResult() { | ||||
|   | ||||
| @@ -79,6 +79,17 @@ export default class ConditionManager extends EventEmitter { | ||||
|         delete this.subscriptions[id]; | ||||
|         delete this.telemetryObjects[id]; | ||||
|         this.removeConditionTelemetryObjects(); | ||||
|  | ||||
|         //force re-computation of condition set result as we might be in a state where | ||||
|         // there is no telemetry datum coming in for a while or at all. | ||||
|         let latestTimestamp = getLatestTimestamp( | ||||
|             {}, | ||||
|             {}, | ||||
|             this.timeSystems, | ||||
|             this.openmct.time.timeSystem() | ||||
|         ); | ||||
|         this.updateConditionResults({id: id}); | ||||
|         this.updateCurrentCondition(latestTimestamp); | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
| @@ -336,14 +347,17 @@ export default class ConditionManager extends EventEmitter { | ||||
|         let timestamp = {}; | ||||
|         timestamp[timeSystemKey] = normalizedDatum[timeSystemKey]; | ||||
|  | ||||
|         this.updateConditionResults(normalizedDatum); | ||||
|         this.updateCurrentCondition(timestamp); | ||||
|     } | ||||
|  | ||||
|     updateConditionResults(normalizedDatum) { | ||||
|         //We want to stop when the first condition evaluates to true. | ||||
|         this.conditions.some((condition) => { | ||||
|             condition.updateResult(normalizedDatum); | ||||
|  | ||||
|             return condition.result === true; | ||||
|         }); | ||||
|  | ||||
|         this.updateCurrentCondition(timestamp); | ||||
|     } | ||||
|  | ||||
|     updateCurrentCondition(timestamp) { | ||||
|   | ||||
| @@ -86,6 +86,7 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|     updateObjectStyleConfig(styleConfiguration) { | ||||
|         if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { | ||||
|             this.initialize(styleConfiguration || {}); | ||||
|             this.applyStaticStyle(); | ||||
|             this.destroy(); | ||||
|         } else { | ||||
|             let isNewConditionSet = !this.conditionSetIdentifier | ||||
| @@ -158,7 +159,6 @@ export default class StyleRuleManager extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.applyStaticStyle(); | ||||
|         if (this.stopProvidingTelemetry) { | ||||
|             this.stopProvidingTelemetry(); | ||||
|             delete this.stopProvidingTelemetry; | ||||
|   | ||||
| @@ -195,11 +195,11 @@ import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants"; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         Criterion, | ||||
|         ConditionDescription | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         currentConditionId: { | ||||
|             type: String, | ||||
|   | ||||
| @@ -81,10 +81,10 @@ import Condition from './Condition.vue'; | ||||
| import ConditionManager from '../ConditionManager'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     components: { | ||||
|         Condition | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         isEditing: Boolean, | ||||
|         testData: { | ||||
|   | ||||
| @@ -58,11 +58,11 @@ import TestData from './TestData.vue'; | ||||
| import ConditionCollection from './ConditionCollection.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ["openmct", "domainObject"], | ||||
|     components: { | ||||
|         TestData, | ||||
|         ConditionCollection | ||||
|     }, | ||||
|     inject: ["openmct", "domainObject"], | ||||
|     props: { | ||||
|         isEditing: Boolean | ||||
|     }, | ||||
|   | ||||
| @@ -31,7 +31,6 @@ | ||||
|             v-model="expanded" | ||||
|             class="c-tree__item__view-control" | ||||
|             :enabled="hasChildren" | ||||
|             :propagate="false" | ||||
|         /> | ||||
|         <div class="c-tree__item__label c-object-label"> | ||||
|             <div | ||||
| @@ -42,7 +41,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <ul | ||||
|         v-if="expanded" | ||||
|         v-if="expanded && !isLoading" | ||||
|         class="c-tree" | ||||
|     > | ||||
|         <li | ||||
| @@ -69,10 +68,10 @@ import viewControl from '@/ui/components/viewControl.vue'; | ||||
|  | ||||
| export default { | ||||
|     name: 'ConditionSetDialogTreeItem', | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         viewControl | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         node: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|         ></div> | ||||
|         <!-- end loading --> | ||||
|  | ||||
|         <div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)" | ||||
|         <div v-if="shouldDisplayNoResultsText" | ||||
|              class="c-tree-and-search__no-results" | ||||
|         > | ||||
|             No results found | ||||
| @@ -63,7 +63,7 @@ | ||||
|         <!-- end main tree --> | ||||
|  | ||||
|         <!-- search tree --> | ||||
|         <ul v-if="searchValue" | ||||
|         <ul v-if="searchValue && !isLoading" | ||||
|             class="c-tree-and-search__tree c-tree" | ||||
|         > | ||||
|             <condition-set-dialog-tree-item | ||||
| @@ -80,16 +80,17 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import debounce from 'lodash/debounce'; | ||||
| import search from '@/ui/components/search.vue'; | ||||
| import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     name: 'ConditionSetSelectorDialog', | ||||
|     components: { | ||||
|         search, | ||||
|         ConditionSetDialogTreeItem | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             expanded: false, | ||||
| @@ -100,8 +101,20 @@ export default { | ||||
|             selectedItem: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         shouldDisplayNoResultsText() { | ||||
|             if (this.isLoading) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return this.allTreeItems.length === 0 | ||||
|                 || (this.searchValue && this.filteredTreeItems.length === 0); | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|         this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.searchService = this.openmct.$injector.get('searchService'); | ||||
|         this.getAllChildren(); | ||||
|     }, | ||||
|     methods: { | ||||
| @@ -124,37 +137,44 @@ export default { | ||||
|                 }); | ||||
|         }, | ||||
|         getFilteredChildren() { | ||||
|             this.searchService.query(this.searchValue).then(children => { | ||||
|                 this.filteredTreeItems = children.hits.map(child => { | ||||
|             // clear any previous search results | ||||
|             this.filteredTreeItems = []; | ||||
|  | ||||
|                     let context = child.object.getCapability('context'); | ||||
|                     let object = child.object.useCapability('adapter'); | ||||
|                     let objectPath = []; | ||||
|                     let navigateToParent; | ||||
|             const promises = this.openmct.objects.search(this.searchValue) | ||||
|                 .map(promise => promise | ||||
|                     .then(results => this.aggregateFilteredChildren(results))); | ||||
|  | ||||
|                     if (context) { | ||||
|                         objectPath = context.getPath().slice(1) | ||||
|                             .map(oldObject => oldObject.useCapability('adapter')) | ||||
|                             .reverse(); | ||||
|                         navigateToParent = '/browse/' + objectPath.slice(1) | ||||
|                             .map((parent) => this.openmct.objects.makeKeyString(parent.identifier)) | ||||
|                             .join('/'); | ||||
|                     } | ||||
|  | ||||
|                     return { | ||||
|                         id: this.openmct.objects.makeKeyString(object.identifier), | ||||
|                         object, | ||||
|                         objectPath, | ||||
|                         navigateToParent | ||||
|                     }; | ||||
|                 }); | ||||
|             Promise.all(promises).then(() => { | ||||
|                 this.isLoading = false; | ||||
|             }); | ||||
|         }, | ||||
|         async aggregateFilteredChildren(results) { | ||||
|             for (const object of results) { | ||||
|                 const objectPath = await this.openmct.objects.getOriginalPath(object.identifier); | ||||
|  | ||||
|                 const navigateToParent = '/browse/' | ||||
|                     + objectPath.slice(1) | ||||
|                         .map(parent => this.openmct.objects.makeKeyString(parent.identifier)) | ||||
|                         .join('/'); | ||||
|  | ||||
|                 const filteredChild = { | ||||
|                     id: this.openmct.objects.makeKeyString(object.identifier), | ||||
|                     object, | ||||
|                     objectPath, | ||||
|                     navigateToParent | ||||
|                 }; | ||||
|  | ||||
|                 this.filteredTreeItems.push(filteredChild); | ||||
|             } | ||||
|         }, | ||||
|         searchTree(value) { | ||||
|             this.searchValue = value; | ||||
|             this.isLoading = true; | ||||
|  | ||||
|             if (this.searchValue !== '') { | ||||
|                 this.getFilteredChildren(); | ||||
|                 this.getDebouncedFilteredChildren(); | ||||
|             } else { | ||||
|                 this.isLoading = false; | ||||
|             } | ||||
|         }, | ||||
|         handleItemSelection(item, node) { | ||||
|   | ||||
| @@ -22,6 +22,7 @@ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from "utils/testing"; | ||||
| import ConditionPlugin from "./plugin"; | ||||
| import stylesManager from '@/ui/inspector/styles/StylesManager'; | ||||
| import StylesView from "./components/inspector/StylesView.vue"; | ||||
| import Vue from 'vue'; | ||||
| import {getApplicableStylesForItem} from "./utils/styleUtils"; | ||||
| @@ -400,14 +401,15 @@ describe('the plugin', function () { | ||||
|             let viewContainer = document.createElement('div'); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     selection: selection | ||||
|                 }, | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     StylesView | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     selection: selection, | ||||
|                     stylesManager | ||||
|                 }, | ||||
|                 template: '<styles-view/>' | ||||
|             }); | ||||
|  | ||||
|   | ||||
| @@ -56,17 +56,24 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new Vue({ | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view></alphanumeric-format-view>' | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.alphanumericFormatView.getViewContext(); | ||||
|                         } else { | ||||
|                             return {}; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function () { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|   | ||||
							
								
								
									
										38
									
								
								src/plugins/displayLayout/CustomStringFormatter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/plugins/displayLayout/CustomStringFormatter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import printj from 'printj'; | ||||
|  | ||||
| export default class CustomStringFormatter { | ||||
|     constructor(openmct, valueMetadata, itemFormat) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.itemFormat = itemFormat; | ||||
|         this.valueMetadata = valueMetadata; | ||||
|     } | ||||
|  | ||||
|     format(datum) { | ||||
|         if (!this.itemFormat) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!this.itemFormat.startsWith('&')) { | ||||
|             return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             const key = this.itemFormat.slice(1); | ||||
|             const customFormatter = this.openmct.telemetry.getFormatter(key); | ||||
|             if (!customFormatter) { | ||||
|                 throw new Error('Custom Formatter not found'); | ||||
|             } | ||||
|  | ||||
|             return customFormatter.format(datum[this.valueMetadata.key]); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|  | ||||
|             return datum[this.valueMetadata.key]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     setFormat(itemFormat) { | ||||
|         this.itemFormat = itemFormat; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										82
									
								
								src/plugins/displayLayout/CustomStringFormatterSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/plugins/displayLayout/CustomStringFormatterSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import CustomStringFormatter from './CustomStringFormatter'; | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
|  | ||||
| const CUSTOM_FORMATS = [ | ||||
|     { | ||||
|         key: 'sclk', | ||||
|         format: (value) => 2 * value | ||||
|     }, | ||||
|     { | ||||
|         key: 'lts', | ||||
|         format: (value) => 3 * value | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| const valueMetadata = { | ||||
|     key: "sin", | ||||
|     name: "Sine", | ||||
|     unit: "Hz", | ||||
|     formatString: "%0.2f", | ||||
|     hints: { | ||||
|         range: 1, | ||||
|         priority: 3 | ||||
|     }, | ||||
|     source: "sin" | ||||
| }; | ||||
|  | ||||
| const datum = { | ||||
|     name: "1 Sine Wave Generator", | ||||
|     utc: 1603930354000, | ||||
|     yesterday: 1603843954000, | ||||
|     sin: 0.587785209686822, | ||||
|     cos: -0.8090170253297632 | ||||
| }; | ||||
|  | ||||
| describe('CustomStringFormatter', function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let customStringFormatter; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         element.appendChild(child); | ||||
|         CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct})); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'getFormatter'); | ||||
|         openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key)); | ||||
|  | ||||
|         customStringFormatter = new CustomStringFormatter(openmct, valueMetadata); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('adds custom format sclk', () => { | ||||
|         const format = openmct.telemetry.getFormatter('sclk'); | ||||
|         expect(format.key).toEqual('sclk'); | ||||
|     }); | ||||
|  | ||||
|     it('adds custom format lts', () => { | ||||
|         const format = openmct.telemetry.getFormatter('lts'); | ||||
|         expect(format.key).toEqual('lts'); | ||||
|     }); | ||||
|  | ||||
|     it('returns correct value for custom format sclk', () => { | ||||
|         customStringFormatter.setFormat('&sclk'); | ||||
|         const value = customStringFormatter.format(datum, valueMetadata); | ||||
|         expect(datum.sin * 2).toEqual(value); | ||||
|     }); | ||||
|  | ||||
|     it('returns correct value for custom format lts', () => { | ||||
|         customStringFormatter.setFormat('<s'); | ||||
|         const value = customStringFormatter.format(datum, valueMetadata); | ||||
|         expect(datum.sin * 3).toEqual(value); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										34
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import clipboard from '@/utils/clipboard'; | ||||
|  | ||||
| export default class CopyToClipboardAction { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy value to clipboard'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToClipboard'; | ||||
|         this.name = 'Copy to Clipboard'; | ||||
|         this.priority = 1; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|  | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
|                 this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|                 this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
| @@ -51,11 +51,11 @@ export default { | ||||
|             height: 5 | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -152,10 +152,7 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         let domainObject = JSON.parse(JSON.stringify(this.domainObject)); | ||||
|  | ||||
|         return { | ||||
|             internalDomainObject: domainObject, | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
| @@ -163,10 +160,10 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         gridSize() { | ||||
|             return this.internalDomainObject.configuration.layoutGrid; | ||||
|             return this.domainObject.configuration.layoutGrid; | ||||
|         }, | ||||
|         layoutItems() { | ||||
|             return this.internalDomainObject.configuration.items; | ||||
|             return this.domainObject.configuration.items; | ||||
|         }, | ||||
|         selectedLayoutItems() { | ||||
|             return this.layoutItems.filter(item => { | ||||
| @@ -174,7 +171,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         layoutDimensions() { | ||||
|             return this.internalDomainObject.configuration.layoutDimensions; | ||||
|             return this.domainObject.configuration.layoutDimensions; | ||||
|         }, | ||||
|         shouldDisplayLayoutDimensions() { | ||||
|             return this.layoutDimensions | ||||
| @@ -206,12 +203,9 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) { | ||||
|             this.internalDomainObject = JSON.parse(JSON.stringify(obj)); | ||||
|         }.bind(this)); | ||||
|         this.openmct.selection.on('change', this.setSelection); | ||||
|         this.initializeItems(); | ||||
|         this.composition = this.openmct.composition.get(this.internalDomainObject); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.composition.on('add', this.addChild); | ||||
|         this.composition.on('remove', this.removeChild); | ||||
|         this.composition.load(); | ||||
| @@ -220,7 +214,6 @@ export default { | ||||
|         this.openmct.selection.off('change', this.setSelection); | ||||
|         this.composition.off('add', this.addChild); | ||||
|         this.composition.off('remove', this.removeChild); | ||||
|         this.unlisten(); | ||||
|     }, | ||||
|     methods: { | ||||
|         addElement(itemType, element) { | ||||
| @@ -347,7 +340,7 @@ export default { | ||||
|             this.startingMinY2 = undefined; | ||||
|         }, | ||||
|         mutate(path, value) { | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, path, value); | ||||
|             this.openmct.objects.mutate(this.domainObject, path, value); | ||||
|         }, | ||||
|         handleDrop($event) { | ||||
|             if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) { | ||||
| @@ -387,11 +380,11 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         containsObject(identifier) { | ||||
|             return _.get(this.internalDomainObject, 'composition') | ||||
|             return _.get(this.domainObject, 'composition') | ||||
|                 .some(childId => this.openmct.objects.areIdsEqual(childId, identifier)); | ||||
|         }, | ||||
|         handleDragOver($event) { | ||||
|             if (this.internalDomainObject.locked) { | ||||
|             if (this.domainObject.locked) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -420,7 +413,7 @@ export default { | ||||
|             item.id = uuid(); | ||||
|             this.trackItem(item); | ||||
|             this.layoutItems.push(item); | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems); | ||||
|             this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems); | ||||
|             this.initSelectIndex = this.layoutItems.length - 1; | ||||
|         }, | ||||
|         trackItem(item) { | ||||
| @@ -477,7 +470,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         removeFromComposition(keyString) { | ||||
|             let composition = _.get(this.internalDomainObject, 'composition'); | ||||
|             let composition = _.get(this.domainObject, 'composition'); | ||||
|             composition = composition.filter(identifier => { | ||||
|                 return this.openmct.objects.makeKeyString(identifier) !== keyString; | ||||
|             }); | ||||
| @@ -629,10 +622,10 @@ export default { | ||||
|         createNewDomainObject(domainObject, composition, viewType, nameExtension, model) { | ||||
|             let identifier = { | ||||
|                 key: uuid(), | ||||
|                 namespace: this.internalDomainObject.identifier.namespace | ||||
|                 namespace: this.domainObject.identifier.namespace | ||||
|             }; | ||||
|             let type = this.openmct.types.get(viewType); | ||||
|             let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier); | ||||
|             let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name; | ||||
|             let object = {}; | ||||
|  | ||||
| @@ -689,7 +682,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         duplicateItem(selectedItems) { | ||||
|             let objectStyles = this.internalDomainObject.configuration.objectStyles || {}; | ||||
|             let objectStyles = this.domainObject.configuration.objectStyles || {}; | ||||
|             let selectItemsArray = []; | ||||
|             let newDomainObjectsArray = []; | ||||
|  | ||||
| @@ -728,8 +721,8 @@ export default { | ||||
|             }); | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems); | ||||
|                 this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles); | ||||
|                 this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems); | ||||
|                 this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles); | ||||
|                 this.$el.click(); //clear selection; | ||||
|  | ||||
|                 newDomainObjectsArray.forEach(domainObject => { | ||||
| @@ -768,13 +761,13 @@ export default { | ||||
|             }; | ||||
|             this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => { | ||||
|                 let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier); | ||||
|                 let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier); | ||||
|                 let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|  | ||||
|                 this.composition.add(newDomainObject); | ||||
|                 this.addItem('subobject-view', newDomainObject, position); | ||||
|  | ||||
|                 overlayPlots.forEach(overlayPlot => { | ||||
|                     if (overlayPlot.location === internalDomainObjectKeyString) { | ||||
|                     if (overlayPlot.location === domainObjectKeyString) { | ||||
|                         this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString); | ||||
|                     } | ||||
|                 }); | ||||
|   | ||||
| @@ -51,11 +51,11 @@ export default { | ||||
|             url: element.url | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -99,8 +99,8 @@ export default { | ||||
|             stroke: '#717171' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -80,11 +80,11 @@ export default { | ||||
|             viewKey | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     components: { | ||||
|         ObjectFrame, | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -129,27 +129,40 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.objects.get(this.item.identifier) | ||||
|             .then(this.setObject); | ||||
|         if (this.openmct.objects.supportsMutation(this.item.identifier)) { | ||||
|             this.openmct.objects.getMutable(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } else { | ||||
|             this.openmct.objects.get(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|     beforeDestroy() { | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
|         } | ||||
|  | ||||
|         if (this.domainObject.isMutable) { | ||||
|             this.openmct.objects.destroyMutable(this.domainObject); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); | ||||
|             this.$nextTick(() => { | ||||
|                 let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                 childContext.item = domainObject; | ||||
|                 childContext.layoutItem = this.item; | ||||
|                 childContext.index = this.index; | ||||
|                 this.context = childContext; | ||||
|                 this.removeSelectable = this.openmct.selection.selectable( | ||||
|                     this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                 delete this.immediatelySelect; | ||||
|                 let reference = this.$refs.objectFrame; | ||||
|  | ||||
|                 if (reference) { | ||||
|                     let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                     childContext.item = domainObject; | ||||
|                     childContext.layoutItem = this.item; | ||||
|                     childContext.index = this.index; | ||||
|                     this.context = childContext; | ||||
|                     this.removeSelectable = this.openmct.selection.selectable( | ||||
|                         this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                     delete this.immediatelySelect; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -30,18 +30,15 @@ | ||||
| > | ||||
|     <div | ||||
|         v-if="domainObject" | ||||
|         class="u-style-receiver c-telemetry-view" | ||||
|         :class="{ | ||||
|             styleClass, | ||||
|             'is-missing': domainObject.status === 'missing' | ||||
|         }" | ||||
|         class="c-telemetry-view u-style-receiver" | ||||
|         :class="[statusClass]" | ||||
|         :style="styleObject" | ||||
|         :data-font-size="item.fontSize" | ||||
|         :data-font="item.font" | ||||
|         @contextmenu.prevent="showContextMenu" | ||||
|     > | ||||
|         <div class="is-missing__indicator" | ||||
|              title="This item is missing" | ||||
|         <div class="is-status__indicator" | ||||
|              :title="`This item is ${status}`" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="showLabel" | ||||
| @@ -74,12 +71,12 @@ | ||||
|  | ||||
| <script> | ||||
| import LayoutFrame from './LayoutFrame.vue'; | ||||
| import printj from 'printj'; | ||||
| import conditionalStylesMixin from "../mixins/objectStyles-mixin"; | ||||
| import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js'; | ||||
|  | ||||
| const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; | ||||
| const DEFAULT_POSITION = [1, 1]; | ||||
| const CONTEXT_MENU_ACTIONS = ['viewHistoricalData']; | ||||
| const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData']; | ||||
|  | ||||
| export default { | ||||
|     makeDefinition(openmct, gridSize, domainObject, position) { | ||||
| @@ -101,11 +98,11 @@ export default { | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct', 'objectPath'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -129,13 +126,18 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentObjectPath: undefined, | ||||
|             datum: undefined, | ||||
|             formats: undefined, | ||||
|             domainObject: undefined, | ||||
|             currentObjectPath: undefined | ||||
|             formats: undefined, | ||||
|             viewKey: `alphanumeric-format-${Math.random()}`, | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         statusClass() { | ||||
|             return (this.status) ? `is-status--${this.status}` : ''; | ||||
|         }, | ||||
|         showLabel() { | ||||
|             let displayMode = this.item.displayMode; | ||||
|  | ||||
| @@ -169,7 +171,11 @@ export default { | ||||
|         valueMetadata() { | ||||
|             return this.datum && this.metadata.value(this.item.value); | ||||
|         }, | ||||
|         valueFormatter() { | ||||
|         formatter() { | ||||
|             if (this.item.format) { | ||||
|                 return this.customStringformatter; | ||||
|             } | ||||
|  | ||||
|             return this.formats[this.item.value]; | ||||
|         }, | ||||
|         telemetryValue() { | ||||
| @@ -177,11 +183,7 @@ export default { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.item.format) { | ||||
|                 return printj.sprintf(this.item.format, this.datum[this.valueMetadata.key]); | ||||
|             } | ||||
|  | ||||
|             return this.valueFormatter && this.valueFormatter.format(this.datum); | ||||
|             return this.formatter && this.formatter.format(this.datum); | ||||
|         }, | ||||
|         telemetryClass() { | ||||
|             if (!this.datum) { | ||||
| @@ -210,20 +212,41 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.objects.get(this.item.identifier) | ||||
|             .then(this.setObject); | ||||
|         if (this.openmct.objects.supportsMutation(this.item.identifier)) { | ||||
|             this.openmct.objects.getMutable(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } else { | ||||
|             this.openmct.objects.get(this.item.identifier) | ||||
|                 .then(this.setObject); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.on("bounds", this.refreshData); | ||||
|  | ||||
|         this.status = this.openmct.status.get(this.item.identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus); | ||||
|     }, | ||||
|     destroyed() { | ||||
|     beforeDestroy() { | ||||
|         this.removeSubscription(); | ||||
|         this.removeStatusListener(); | ||||
|  | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|  | ||||
|         if (this.domainObject.isMutable) { | ||||
|             this.openmct.objects.destroyMutable(this.domainObject); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         formattedValueForCopy() { | ||||
|             const timeFormatterKey = this.openmct.time.timeSystem().key; | ||||
|             const timeFormatter = this.formats[timeFormatterKey]; | ||||
|             const unit = this.unit ? ` ${this.unit}` : ''; | ||||
|  | ||||
|             return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`; | ||||
|         }, | ||||
|         requestHistoricalData() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             let options = { | ||||
| @@ -261,12 +284,26 @@ export default { | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         formattedValueForCopy: this.formattedValueForCopy | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|             this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); | ||||
|             this.formats = this.openmct.telemetry.getFormatMap(this.metadata); | ||||
|  | ||||
|             const valueMetadata = this.metadata.value(this.item.value); | ||||
|             this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format); | ||||
|  | ||||
|             this.requestHistoricalData(); | ||||
|             this.subscribeToObject(); | ||||
|  | ||||
| @@ -286,10 +323,37 @@ export default { | ||||
|             delete this.immediatelySelect; | ||||
|         }, | ||||
|         updateTelemetryFormat(format) { | ||||
|             this.customStringformatter.setFormat(format); | ||||
|  | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|         async getContextMenuActions() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); | ||||
|             const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             const actionsObject = actionCollection.getActionsObject(); | ||||
|  | ||||
|             let copyToNotebookAction = actionsObject.copyToNotebook; | ||||
|  | ||||
|             if (defaultNotebook) { | ||||
|                 const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                 copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`; | ||||
|             } else { | ||||
|                 actionsObject.copyToNotebook = undefined; | ||||
|                 delete actionsObject.copyToNotebook; | ||||
|             } | ||||
|  | ||||
|             return CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return actionsObject[actionKey]; | ||||
|             }).filter(action => action !== undefined); | ||||
|         }, | ||||
|         async showContextMenu(event) { | ||||
|             const contextMenuActions = await this.getContextMenuActions(); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -59,11 +59,11 @@ export default { | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         LayoutFrame | ||||
|     }, | ||||
|     mixins: [conditionalStylesMixin], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|         flex: 1 1 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|        // justify-content: center; | ||||
|         align-items: center; | ||||
|         overflow: hidden; | ||||
|         padding: $interiorMargin; | ||||
| @@ -27,14 +26,13 @@ | ||||
|         border: 1px solid transparent; | ||||
|     } | ||||
|  | ||||
|     @include isMissing($absPos: true); | ||||
|  | ||||
|     .is-missing__indicator { | ||||
|     .is-status__indicator { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|     } | ||||
|  | ||||
|     &.is-missing { | ||||
|     &[class*='is-status'] { | ||||
|         border: $borderMissing; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,9 +26,12 @@ import objectUtils from 'objectUtils'; | ||||
| import DisplayLayoutType from './DisplayLayoutType.js'; | ||||
| import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; | ||||
| import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; | ||||
| import CopyToClipboardAction from './actions/CopyToClipboardAction'; | ||||
|  | ||||
| export default function DisplayLayoutPlugin(options) { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new CopyToClipboardAction(openmct)); | ||||
|  | ||||
|         openmct.objectViews.addProvider({ | ||||
|             key: 'layout.view', | ||||
|             canView: function (domainObject) { | ||||
|   | ||||
							
								
								
									
										159
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 DuplicateTask from './DuplicateTask'; | ||||
|  | ||||
| export default class DuplicateAction { | ||||
|     constructor(openmct) { | ||||
|         this.name = 'Duplicate'; | ||||
|         this.key = 'duplicate'; | ||||
|         this.description = 'Duplicate this object.'; | ||||
|         this.cssClass = "icon-duplicate"; | ||||
|         this.group = "action"; | ||||
|         this.priority = 7; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     async invoke(objectPath) { | ||||
|         let duplicationTask = new DuplicateTask(this.openmct); | ||||
|         let originalObject = objectPath[0]; | ||||
|         let parent = objectPath[1]; | ||||
|         let userInput = await this.getUserInput(originalObject, parent); | ||||
|         let newParent = userInput.location; | ||||
|         let inNavigationPath = this.inNavigationPath(originalObject); | ||||
|  | ||||
|         // legacy check | ||||
|         if (this.isLegacyDomainObject(newParent)) { | ||||
|             newParent = await this.convertFromLegacy(newParent); | ||||
|         } | ||||
|  | ||||
|         // if editing, save | ||||
|         if (inNavigationPath && this.openmct.editor.isEditing()) { | ||||
|             this.openmct.editor.save(); | ||||
|         } | ||||
|  | ||||
|         // duplicate | ||||
|         let newObject = await duplicationTask.duplicate(originalObject, newParent); | ||||
|         this.updateNameCheck(newObject, userInput.name); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     async getUserInput(originalObject, parent) { | ||||
|         let dialogService = this.openmct.$injector.get('dialogService'); | ||||
|         let dialogForm = this.getDialogForm(originalObject, parent); | ||||
|         let formState = { | ||||
|             name: originalObject.name | ||||
|         }; | ||||
|         let userInput = await dialogService.getUserInput(dialogForm, formState); | ||||
|  | ||||
|         return userInput; | ||||
|     } | ||||
|  | ||||
|     updateNameCheck(object, name) { | ||||
|         if (object.name !== name) { | ||||
|             this.openmct.objects.mutate(object, 'name', name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     inNavigationPath(object) { | ||||
|         return this.openmct.router.path | ||||
|             .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)); | ||||
|     } | ||||
|  | ||||
|     getDialogForm(object, parent) { | ||||
|         return { | ||||
|             name: "Duplicate Item", | ||||
|             sections: [ | ||||
|                 { | ||||
|                     rows: [ | ||||
|                         { | ||||
|                             key: "name", | ||||
|                             control: "textfield", | ||||
|                             name: "Name", | ||||
|                             pattern: "\\S+", | ||||
|                             required: true, | ||||
|                             cssClass: "l-input-lg" | ||||
|                         }, | ||||
|                         { | ||||
|                             name: "location", | ||||
|                             cssClass: "grows", | ||||
|                             control: "locator", | ||||
|                             validate: this.validate(object, parent), | ||||
|                             key: 'location' | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     validate(object, currentParent) { | ||||
|         return (parentCandidate) => { | ||||
|             let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier); | ||||
|             let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId()); | ||||
|             let objectKeystring = this.openmct.objects.makeKeyString(object.identifier); | ||||
|  | ||||
|             if (!parentCandidate || !currentParentKeystring) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidateKeystring === objectKeystring) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return this.openmct.composition.checkPolicy( | ||||
|                 parentCandidate.useCapability('adapter'), | ||||
|                 object | ||||
|             ); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     isLegacyDomainObject(domainObject) { | ||||
|         return domainObject.getCapability !== undefined; | ||||
|     } | ||||
|  | ||||
|     async convertFromLegacy(legacyDomainObject) { | ||||
|         let objectContext = legacyDomainObject.getCapability('context'); | ||||
|         let domainObject = await this.openmct.objects.get(objectContext.domainObject.id); | ||||
|  | ||||
|         return domainObject; | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath) { | ||||
|         let parent = objectPath[1]; | ||||
|         let parentType = parent && this.openmct.types.get(parent.type); | ||||
|         let child = objectPath[0]; | ||||
|         let childType = child && this.openmct.types.get(child.type); | ||||
|         let locked = child.locked ? child.locked : parent && parent.locked; | ||||
|  | ||||
|         if (locked) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return childType | ||||
|             && childType.definition.creatable | ||||
|             && parentType | ||||
|             && parentType.definition.creatable | ||||
|             && Array.isArray(parent.composition); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										273
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 uuid from 'uuid'; | ||||
|  | ||||
| /** | ||||
|  * This class encapsulates the process of  duplicating/copying a domain object | ||||
|  * and all of its children. | ||||
|  * | ||||
|  * @param {DomainObject} domainObject The object to duplicate | ||||
|  * @param {DomainObject} parent The new location of the cloned object tree | ||||
|  * @param {src/plugins/duplicate.DuplicateService~filter} filter | ||||
|  *        a function used to filter out objects from | ||||
|  *        the cloning process | ||||
|  * @constructor | ||||
|  */ | ||||
| export default class DuplicateTask { | ||||
|  | ||||
|     constructor(openmct) { | ||||
|         this.domainObject = undefined; | ||||
|         this.parent = undefined; | ||||
|         this.firstClone = undefined; | ||||
|         this.filter = undefined; | ||||
|         this.persisted = 0; | ||||
|         this.clones = []; | ||||
|         this.idMap = {}; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Execute the duplicate/copy task with the objects provided. | ||||
|      * @returns {promise} Which will resolve with a clone of the object | ||||
|      * once complete. | ||||
|      */ | ||||
|     async duplicate(domainObject, parent, filter) { | ||||
|         this.domainObject = domainObject; | ||||
|         this.parent = parent; | ||||
|         this.namespace = parent.identifier.namespace; | ||||
|         this.filter = filter || this.isCreatable; | ||||
|  | ||||
|         await this.buildDuplicationPlan(); | ||||
|         await this.persistObjects(); | ||||
|         await this.addClonesToParent(); | ||||
|  | ||||
|         return this.firstClone; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will build a graph of an object and all of its child objects in | ||||
|      * memory | ||||
|      * @private | ||||
|      * @param domainObject The original object to be copied | ||||
|      * @param parent The parent of the original object to be copied | ||||
|      * @returns {Promise} resolved with an array of clones of the models | ||||
|      * of the object tree being copied. Duplicating is done in a bottom-up | ||||
|      * fashion, so that the last member in the array is a clone of the model | ||||
|      * object being copied. The clones are all full composed with | ||||
|      * references to their own children. | ||||
|      */ | ||||
|     async buildDuplicationPlan() { | ||||
|         let domainObjectClone = await this.duplicateObject(this.domainObject); | ||||
|  | ||||
|         if (domainObjectClone !== this.domainObject) { | ||||
|             domainObjectClone.location = this.getKeyString(this.parent); | ||||
|         } | ||||
|  | ||||
|         this.firstClone = domainObjectClone; | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will persist a list of {@link objectClones}. It will persist all | ||||
|      * simultaneously, irrespective of order in the list. This may | ||||
|      * result in automatic request batching by the browser. | ||||
|      */ | ||||
|     async persistObjects() { | ||||
|         let initialCount = this.clones.length; | ||||
|         let dialog = this.openmct.overlays.progressDialog({ | ||||
|             progressPerc: 0, | ||||
|             message: `Duplicating ${initialCount} objects.`, | ||||
|             iconClass: 'info', | ||||
|             title: 'Duplicating' | ||||
|         }); | ||||
|  | ||||
|         let clonesDone = Promise.all(this.clones.map((clone) => { | ||||
|             let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount)); | ||||
|             let message = `Duplicating ${initialCount - this.persisted} objects.`; | ||||
|  | ||||
|             dialog.updateProgress(percentPersisted, message); | ||||
|  | ||||
|             return this.openmct.objects.save(clone); | ||||
|         })); | ||||
|  | ||||
|         await clonesDone; | ||||
|  | ||||
|         dialog.dismiss(); | ||||
|         this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will add a list of clones to the specified parent's composition | ||||
|      */ | ||||
|     async addClonesToParent() { | ||||
|         let parentComposition = this.openmct.composition.get(this.parent); | ||||
|         await parentComposition.load(); | ||||
|         parentComposition.add(this.firstClone); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A recursive function that will perform a bottom-up duplicate of | ||||
|      * the object tree with originalObject at the root. Recurses to | ||||
|      * the farthest leaf, then works its way back up again, | ||||
|      * cloning objects, and composing them with their child clones | ||||
|      * as it goes | ||||
|      * @private | ||||
|      * @returns {DomainObject} If the type of the original object allows for | ||||
|      * duplication, then a duplicate of the object, otherwise the object | ||||
|      * itself (to allow linking to non duplicatable objects). | ||||
|      */ | ||||
|     async duplicateObject(originalObject) { | ||||
|         // Check if the creatable (or other passed in filter). | ||||
|         if (this.filter(originalObject)) { | ||||
|             let clone = this.cloneObjectModel(originalObject); | ||||
|             let composeesCollection = this.openmct.composition.get(originalObject); | ||||
|             let composees; | ||||
|  | ||||
|             if (composeesCollection) { | ||||
|                 composees = await composeesCollection.load(); | ||||
|             } | ||||
|  | ||||
|             return this.duplicateComposees(clone, composees); | ||||
|         } | ||||
|  | ||||
|         // Not creatable, creating a link, no need to iterate children | ||||
|         return originalObject; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given an array of objects composed by a parent, clone them, then | ||||
|      * add them to the parent. | ||||
|      * @private | ||||
|      * @returns {*} | ||||
|      */ | ||||
|     async duplicateComposees(clonedParent, composees = []) { | ||||
|         let idMappings = []; | ||||
|         let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => { | ||||
|             await previousPromise; | ||||
|  | ||||
|             let clonedComposee = await this.duplicateObject(nextComposee); | ||||
|  | ||||
|             if (clonedComposee) { | ||||
|                 idMappings.push({ | ||||
|                     newId: clonedComposee.identifier, | ||||
|                     oldId: nextComposee.identifier | ||||
|                 }); | ||||
|                 this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee); | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         }, Promise.resolve()); | ||||
|  | ||||
|         await allComposeesDuplicated; | ||||
|  | ||||
|         clonedParent = this.rewriteIdentifiers(clonedParent, idMappings); | ||||
|         this.clones.push(clonedParent); | ||||
|  | ||||
|         return clonedParent; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update identifiers in a cloned object model (or part of | ||||
|      * a cloned object model) to reflect new identifiers after | ||||
|      * duplicating. | ||||
|      * @private | ||||
|      */ | ||||
|     rewriteIdentifiers(clonedParent, childIdMappings) { | ||||
|         for (let { newId, oldId } of childIdMappings) { | ||||
|             let newIdKeyString = this.openmct.objects.makeKeyString(newId); | ||||
|             let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); | ||||
|  | ||||
|             // regex replace keystrings | ||||
|             clonedParent = JSON.stringify(clonedParent).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString); | ||||
|  | ||||
|             // parse reviver to replace identifiers | ||||
|             clonedParent = JSON.parse(clonedParent, (key, value) => { | ||||
|                 if (Object.prototype.hasOwnProperty.call(value, 'key') | ||||
|                     && Object.prototype.hasOwnProperty.call(value, 'namespace') | ||||
|                     && value.key === oldId.key | ||||
|                     && value.namespace === oldId.namespace) { | ||||
|                     return newId; | ||||
|                 } else { | ||||
|                     return value; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return clonedParent; | ||||
|     } | ||||
|  | ||||
|     composeChild(child, parent, setLocation) { | ||||
|         parent.composition.push(child.identifier); | ||||
|  | ||||
|         //If a location is not specified, set it. | ||||
|         if (setLocation && child.location === undefined) { | ||||
|             let parentKeyString = this.getKeyString(parent); | ||||
|             child.location = parentKeyString; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getTypeDefinition(domainObject, definition) { | ||||
|         let typeDefinitions = this.openmct.types.get(domainObject.type).definition; | ||||
|  | ||||
|         return typeDefinitions[definition] || false; | ||||
|     } | ||||
|  | ||||
|     cloneObjectModel(domainObject) { | ||||
|         let clone = JSON.parse(JSON.stringify(domainObject)); | ||||
|         let identifier = { | ||||
|             key: uuid(), | ||||
|             namespace: this.namespace // set to NEW parent's namespace | ||||
|         }; | ||||
|  | ||||
|         if (clone.modified || clone.persisted || clone.location) { | ||||
|             clone.modified = undefined; | ||||
|             clone.persisted = undefined; | ||||
|             clone.location = undefined; | ||||
|             delete clone.modified; | ||||
|             delete clone.persisted; | ||||
|             delete clone.location; | ||||
|         } | ||||
|  | ||||
|         if (clone.composition) { | ||||
|             clone.composition = []; | ||||
|         } | ||||
|  | ||||
|         clone.identifier = identifier; | ||||
|  | ||||
|         return clone; | ||||
|     } | ||||
|  | ||||
|     getKeyString(domainObject) { | ||||
|         return this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|     } | ||||
|  | ||||
|     isCreatable(domainObject) { | ||||
|         return this.getTypeDefinition(domainObject, 'creatable'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2019, 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 DuplicateAction from "./DuplicateAction"; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new DuplicateAction(openmct)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										159
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, 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 DuplicateActionPlugin from './plugin.js'; | ||||
| import DuplicateAction from './DuplicateAction.js'; | ||||
| import DuplicateTask from './DuplicateTask.js'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     getMockObjects | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The Duplicate Action plugin", () => { | ||||
|  | ||||
|     let openmct; | ||||
|     let duplicateTask; | ||||
|     let childObject; | ||||
|     let parentObject; | ||||
|     let anotherParentObject; | ||||
|  | ||||
|     // this setups up the app | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         childObject = getMockObjects({ | ||||
|             objectKeyStrings: ['folder'], | ||||
|             overwrite: { | ||||
|                 folder: { | ||||
|                     name: "Child Folder", | ||||
|                     identifier: { | ||||
|                         namespace: "", | ||||
|                         key: "child-folder-object" | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }).folder; | ||||
|         parentObject = getMockObjects({ | ||||
|             objectKeyStrings: ['folder'], | ||||
|             overwrite: { | ||||
|                 folder: { | ||||
|                     name: "Parent Folder", | ||||
|                     type: "folder", | ||||
|                     composition: [childObject.identifier] | ||||
|                 } | ||||
|             } | ||||
|         }).folder; | ||||
|         anotherParentObject = getMockObjects({ | ||||
|             objectKeyStrings: ['folder'], | ||||
|             overwrite: { | ||||
|                 folder: { | ||||
|                     name: "Another Parent Folder" | ||||
|                 } | ||||
|             } | ||||
|         }).folder; | ||||
|  | ||||
|         let objectGet = openmct.objects.get.bind(openmct.objects); | ||||
|         spyOn(openmct.objects, 'get').and.callFake((identifier) => { | ||||
|             let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key); | ||||
|  | ||||
|             if (!obj) { | ||||
|                 // not one of the mocked objs, callthrough basically | ||||
|                 return objectGet(identifier); | ||||
|             } | ||||
|  | ||||
|             return Promise.resolve(obj); | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.composition, 'get').and.callFake((domainObject) => { | ||||
|             return { | ||||
|                 load: async () => { | ||||
|                     let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key); | ||||
|                     let children = []; | ||||
|  | ||||
|                     if (obj) { | ||||
|                         for (let i = 0; i < obj.composition.length; i++) { | ||||
|                             children.push(await openmct.objects.get(obj.composition[i])); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     return Promise.resolve(children); | ||||
|                 }, | ||||
|                 add: (child) => { | ||||
|                     domainObject.composition.push(child.identifier); | ||||
|                 } | ||||
|             }; | ||||
|         }); | ||||
|  | ||||
|         // already installed by default, but never hurts, just adds to context menu | ||||
|         openmct.install(DuplicateActionPlugin()); | ||||
|         openmct.types.addType('folder', {creatable: true}); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|         expect(DuplicateActionPlugin).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("when moving an object to a new parent", () => { | ||||
|  | ||||
|         beforeEach(async (done) => { | ||||
|             duplicateTask = new DuplicateTask(openmct); | ||||
|             await duplicateTask.duplicate(parentObject, anotherParentObject); | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         it("the duplicate child object's name (when not changing) should be the same as the original object", async () => { | ||||
|             let duplicatedObjectIdentifier = anotherParentObject.composition[0]; | ||||
|             let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier); | ||||
|             let duplicateObjectName = duplicatedObject.name; | ||||
|  | ||||
|             expect(duplicateObjectName).toEqual(parentObject.name); | ||||
|         }); | ||||
|  | ||||
|         it("the duplicate child object's identifier should be new", () => { | ||||
|             let duplicatedObjectIdentifier = anotherParentObject.composition[0]; | ||||
|  | ||||
|             expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("when a new name is provided for the duplicated object", () => { | ||||
|         const NEW_NAME = 'New Name'; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             duplicateTask = new DuplicateAction(openmct); | ||||
|             duplicateTask.updateNameCheck(parentObject, NEW_NAME); | ||||
|         }); | ||||
|  | ||||
|         it("the name is updated", () => { | ||||
|             let childName = parentObject.name; | ||||
|             expect(childName).toEqual(NEW_NAME); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -47,13 +47,13 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new Vue({ | ||||
|                             provide: { | ||||
|                                 openmct | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FiltersView: FiltersView.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct | ||||
|                             }, | ||||
|                             template: '<filters-view></filters-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|   | ||||
| @@ -65,11 +65,11 @@ import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; | ||||
| import isEmpty from 'lodash/isEmpty'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FilterField, | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         filterObject: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -41,10 +41,10 @@ | ||||
| import FilterField from './FilterField.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FilterField | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         globalMetadata: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -87,12 +87,12 @@ import DropHint from './dropHint.vue'; | ||||
| const MIN_FRAME_SIZE = 5; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         FrameComponent, | ||||
|         ResizeHandle, | ||||
|         DropHint | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         container: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -28,7 +28,7 @@ | ||||
|     ></div> | ||||
|  | ||||
|     <div | ||||
|         v-if="areAllContainersEmpty()" | ||||
|         v-if="allContainersAreEmpty" | ||||
|         class="c-fl__empty" | ||||
|     > | ||||
|         <span class="c-fl__empty-message">This Flexible Layout is currently empty</span> | ||||
| @@ -94,7 +94,6 @@ import Container from '../utils/container'; | ||||
| import Frame from '../utils/frame'; | ||||
| import ResizeHandle from './resizeHandle.vue'; | ||||
| import DropHint from './dropHint.vue'; | ||||
| import RemoveAction from '../../remove/RemoveAction.js'; | ||||
|  | ||||
| const MIN_CONTAINER_SIZE = 5; | ||||
|  | ||||
| @@ -140,19 +139,20 @@ function sizeToFill(items) { | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'objectPath', 'layoutObject'], | ||||
|     components: { | ||||
|         ContainerComponent, | ||||
|         ResizeHandle, | ||||
|         DropHint | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath', 'layoutObject'], | ||||
|     props: { | ||||
|         isEditing: Boolean | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: this.layoutObject, | ||||
|             newFrameLocation: [] | ||||
|             newFrameLocation: [], | ||||
|             identifierMap: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -168,26 +168,30 @@ export default { | ||||
|         }, | ||||
|         rowsLayout() { | ||||
|             return this.domainObject.configuration.rowsLayout; | ||||
|         }, | ||||
|         allContainersAreEmpty() { | ||||
|             return this.containers.every(container => container.frames.length === 0); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.buildIdentifierMap(); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.composition.on('remove', this.removeChildObject); | ||||
|         this.composition.on('add', this.addFrame); | ||||
|  | ||||
|         this.RemoveAction = new RemoveAction(this.openmct); | ||||
|  | ||||
|         this.unobserve = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject); | ||||
|         this.composition.load(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.composition.off('remove', this.removeChildObject); | ||||
|         this.composition.off('add', this.addFrame); | ||||
|  | ||||
|         this.unobserve(); | ||||
|     }, | ||||
|     methods: { | ||||
|         areAllContainersEmpty() { | ||||
|             return !this.containers.filter(container => container.frames.length).length; | ||||
|         buildIdentifierMap() { | ||||
|             this.containers.forEach(container => { | ||||
|                 container.frames.forEach(frame => { | ||||
|                     let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); | ||||
|                     this.identifierMap[keystring] = true; | ||||
|                 }); | ||||
|             }); | ||||
|         }, | ||||
|         addContainer() { | ||||
|             let container = new Container(); | ||||
| @@ -236,16 +240,21 @@ export default { | ||||
|             this.newFrameLocation = [containerIndex, insertFrameIndex]; | ||||
|         }, | ||||
|         addFrame(domainObject) { | ||||
|             let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0; | ||||
|             let container = this.containers[containerIndex]; | ||||
|             let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length; | ||||
|             let frame = new Frame(domainObject.identifier); | ||||
|             let keystring = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|  | ||||
|             container.frames.splice(frameIndex + 1, 0, frame); | ||||
|             sizeItems(container.frames, frame); | ||||
|             if (!this.identifierMap[keystring]) { | ||||
|                 let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0; | ||||
|                 let container = this.containers[containerIndex]; | ||||
|                 let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length; | ||||
|                 let frame = new Frame(domainObject.identifier); | ||||
|  | ||||
|             this.newFrameLocation = []; | ||||
|             this.persist(containerIndex); | ||||
|                 container.frames.splice(frameIndex + 1, 0, frame); | ||||
|                 sizeItems(container.frames, frame); | ||||
|  | ||||
|                 this.newFrameLocation = []; | ||||
|                 this.persist(containerIndex); | ||||
|                 this.identifierMap[keystring] = true; | ||||
|             } | ||||
|         }, | ||||
|         deleteFrame(frameId) { | ||||
|             let container = this.containers | ||||
| @@ -254,16 +263,20 @@ export default { | ||||
|                 .frames | ||||
|                 .filter((f => f.id === frameId))[0]; | ||||
|  | ||||
|             this.removeFromComposition(frame.domainObjectIdentifier) | ||||
|                 .then(() => { | ||||
|                     sizeToFill(container.frames); | ||||
|                     this.setSelectionToParent(); | ||||
|                 }); | ||||
|             this.removeFromComposition(frame.domainObjectIdentifier); | ||||
|  | ||||
|             this.$nextTick().then(() => { | ||||
|                 sizeToFill(container.frames); | ||||
|                 this.setSelectionToParent(); | ||||
|             }); | ||||
|         }, | ||||
|         removeFromComposition(identifier) { | ||||
|             return this.openmct.objects.get(identifier).then((childDomainObject) => { | ||||
|                 this.RemoveAction.removeFromComposition(this.domainObject, childDomainObject); | ||||
|             }); | ||||
|             let keystring = this.openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|             this.identifierMap[keystring] = undefined; | ||||
|             delete this.identifierMap[keystring]; | ||||
|  | ||||
|             this.composition.remove({identifier}); | ||||
|         }, | ||||
|         setSelectionToParent() { | ||||
|             this.$el.click(); | ||||
|   | ||||
| @@ -58,10 +58,10 @@ | ||||
| import ObjectFrame from '../../../ui/components/ObjectFrame.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         ObjectFrame | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         frame: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -44,15 +44,15 @@ define([ | ||||
|                 return { | ||||
|                     show: function (element, isEditing) { | ||||
|                         component = new Vue({ | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FlexibleLayoutComponent: FlexibleLayoutComponent.default | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 objectPath, | ||||
|                                 layoutObject: domainObject | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             components: { | ||||
|                                 FlexibleLayoutComponent: FlexibleLayoutComponent.default | ||||
|                             }, | ||||
|                             data() { | ||||
|                                 return { | ||||
|                                     isEditing: isEditing | ||||
|   | ||||
| @@ -22,18 +22,22 @@ | ||||
|  | ||||
| define([ | ||||
|     './components/GridView.vue', | ||||
|     './constants.js', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     GridViewComponent, | ||||
|     constants, | ||||
|     Vue | ||||
| ) { | ||||
|     function FolderGridView(openmct) { | ||||
|         const ALLOWED_FOLDER_TYPES = constants.ALLOWED_FOLDER_TYPES; | ||||
|  | ||||
|         return { | ||||
|             key: 'grid', | ||||
|             name: 'Grid View', | ||||
|             cssClass: 'icon-thumbs-strip', | ||||
|             canView: function (domainObject) { | ||||
|                 return domainObject.type === 'folder'; | ||||
|                 return ALLOWED_FOLDER_TYPES.includes(domainObject.type); | ||||
|             }, | ||||
|             view: function (domainObject) { | ||||
|                 let component; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user