Compare commits
	
		
			57 Commits
		
	
	
		
			status-api
			...
			tree-searc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6b719259e3 | ||
|   | 9fc31809f6 | ||
|   | ccd4bbd279 | ||
|   | 351848ad56 | ||
|   | cbac495f93 | ||
|   | 15ef5b7623 | ||
|   | 46c7ac944f | ||
|   | aa4bfab462 | ||
|   | f5cbb37e5a | ||
|   | 8d9079984a | ||
|   | 72848849dd | ||
|   | 41783d8939 | ||
|   | 441ad58fe7 | ||
|   | 66130ba542 | ||
|   | 06a6a3f773 | ||
|   | 52fab78625 | ||
|   | 5eb6c15959 | ||
|   | 895bdc164f | ||
|   | ce8c31cfa4 | ||
|   | c9728144a5 | ||
|   | d80c0eef8e | ||
|   | 55829dcf05 | ||
|   | d78956327c | ||
|   | 4a0728a55b | ||
|   | 2e1d57aa8c | ||
|   | 1c2b0678be | ||
|   | 76fec7f3bc | ||
|   | 6f810add43 | ||
|   | 12727adb16 | ||
|   | 1b4717065a | ||
|   | 9da750c3bb | ||
|   | 176226ddef | ||
|   | acea18fa70 | ||
|   | d1656f8561 | ||
|   | 87751e882c | ||
|   | e24542c1a6 | ||
|   | b08f3106ed | ||
|   | dff393a714 | ||
|   | fd9c9aee03 | ||
|   | 59bf981fb0 | ||
|   | 4bbdac759f | ||
|   | 13fe7509de | ||
|   | 6fd8f6cd43 | ||
|   | 700bc7616d | ||
|   | 6375ecda34 | ||
|   | d232dacc65 | ||
|   | 59946e89ef | ||
|   | d75c4b4049 | ||
|   | 3436e976cf | ||
|   | 30ca4b707d | ||
|   | 27704c9a48 | ||
|   | b8e232831e | ||
|   | f6bc49fc82 | ||
|   | 7018c217c4 | ||
|   | 18c230c0f7 | ||
|   | b8c2f3f49a | ||
|   | e5e27ea498 | 
| @@ -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 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.4.1-SNAPSHOT", | ||||
|   "version": "1.5.0-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|   | ||||
| @@ -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,39 +52,6 @@ define([ | ||||
|             "configuration": {}, | ||||
|             "extensions": { | ||||
|                 "actions": [ | ||||
|                     { | ||||
|                         "key": "move", | ||||
|                         "name": "Move", | ||||
|                         "description": "Move object to another location.", | ||||
|                         "cssClass": "icon-move", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 9, | ||||
|                         "implementation": MoveAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
|                             "locationService", | ||||
|                             "moveService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "copy", | ||||
|                         "name": "Duplicate", | ||||
|                         "description": "Duplicate object to another location.", | ||||
|                         "cssClass": "icon-duplicate", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 8, | ||||
|                         "implementation": CopyAction, | ||||
|                         "depends": [ | ||||
|                             "$log", | ||||
|                             "policyService", | ||||
|                             "locationService", | ||||
|                             "copyService", | ||||
|                             "dialogService", | ||||
|                             "notificationService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "link", | ||||
|                         "name": "Create Link", | ||||
| @@ -141,10 +100,6 @@ define([ | ||||
|                     { | ||||
|                         "category": "action", | ||||
|                         "implementation": CopyPolicy | ||||
|                     }, | ||||
|                     { | ||||
|                         "category": "action", | ||||
|                         "implementation": MovePolicy | ||||
|                     } | ||||
|                 ], | ||||
|                 "capabilities": [ | ||||
| @@ -160,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); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -32,7 +32,8 @@ | ||||
|     function indexItem(id, model) { | ||||
|         indexedItems.push({ | ||||
|             id: id, | ||||
|             name: model.name.toLowerCase() | ||||
|             name: model.name.toLowerCase(), | ||||
|             type: model.type | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -125,15 +125,17 @@ 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( | ||||
|                 let mutatedObjectModel = mutatedObject.getModel(); | ||||
|                 this.index( | ||||
|                     mutatedObject.getId(), | ||||
|                     mutatedObject.getModel() | ||||
|                     mutatedObjectModel, | ||||
|                     mutatedObjectModel.type | ||||
|  | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
| @@ -178,14 +180,15 @@ define([ | ||||
|      * @param id a model id | ||||
|      * @param model a model | ||||
|      */ | ||||
|     GenericSearchProvider.prototype.index = function (id, model) { | ||||
|     GenericSearchProvider.prototype.index = function (id, model, type) { | ||||
|         var provider = this; | ||||
|  | ||||
|         if (id !== 'ROOT') { | ||||
|             this.worker.postMessage({ | ||||
|                 request: 'index', | ||||
|                 model: model, | ||||
|                 id: id | ||||
|                 id: id, | ||||
|                 type: type | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @@ -223,7 +226,7 @@ define([ | ||||
|             .then(function (objects) { | ||||
|                 delete provider.pendingIndex[idToIndex]; | ||||
|                 if (objects[idToIndex]) { | ||||
|                     provider.index(idToIndex, objects[idToIndex].model); | ||||
|                     provider.index(idToIndex, objects[idToIndex].model, objects[idToIndex].model.type); | ||||
|                 } | ||||
|             }, function () { | ||||
|                 provider | ||||
| @@ -262,6 +265,7 @@ define([ | ||||
|                 return { | ||||
|                     id: hit.item.id, | ||||
|                     model: hit.item.model, | ||||
|                     type: hit.item.type, | ||||
|                     score: hit.matchCount | ||||
|                 }; | ||||
|             }); | ||||
| @@ -273,7 +277,9 @@ define([ | ||||
|  | ||||
|             modelResults.hits = event.data.results.map(function (hit) { | ||||
|                 return { | ||||
|                     id: hit.id | ||||
|                     id: hit.id, | ||||
|                     name: hit.name, | ||||
|                     type: hit.type | ||||
|                 }; | ||||
|             }); | ||||
|         } | ||||
|   | ||||
| @@ -41,7 +41,8 @@ | ||||
|         indexedItems.push({ | ||||
|             id: id, | ||||
|             vector: vector, | ||||
|             model: model | ||||
|             model: model, | ||||
|             type: model.type | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -85,7 +85,8 @@ define([ | ||||
|     SearchAggregator.prototype.query = function ( | ||||
|         inputText, | ||||
|         maxResults, | ||||
|         filter | ||||
|         filter, | ||||
|         indexOnly | ||||
|     ) { | ||||
|  | ||||
|         var aggregator = this, | ||||
| @@ -120,10 +121,24 @@ define([ | ||||
|                 modelResults = aggregator.applyFilter(modelResults, filter); | ||||
|                 modelResults = aggregator.removeDuplicates(modelResults); | ||||
|  | ||||
|                 if (indexOnly) { | ||||
|                     return Promise.resolve(modelResults); | ||||
|                 } | ||||
|  | ||||
|                 return aggregator.asObjectResults(modelResults); | ||||
|             }); | ||||
|     }; | ||||
|  | ||||
|     SearchAggregator.prototype.queryLite = function ( | ||||
|         inputText, | ||||
|         maxResults, | ||||
|         filter | ||||
|     ) { | ||||
|         const INDEX_ONLY = true; | ||||
|  | ||||
|         return this.query(inputText, maxResults, filter, INDEX_ONLY); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Order model results by score descending and return them. | ||||
|      */ | ||||
|   | ||||
| @@ -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 | ||||
| ) { | ||||
|     /** | ||||
| @@ -263,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()); | ||||
| @@ -276,6 +282,7 @@ define([ | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
| @@ -24,13 +24,14 @@ import EventEmitter from 'EventEmitter'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionCollection extends EventEmitter { | ||||
|     constructor(applicableActions, objectPath, view, openmct) { | ||||
|     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 = { | ||||
| @@ -41,10 +42,12 @@ class ActionCollection extends EventEmitter { | ||||
|         this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions); | ||||
|         this._update = _.debounce(this._update.bind(this), 150, debounceOptions); | ||||
|  | ||||
|         this._observeObjectPath(); | ||||
|         this._initializeActions(); | ||||
|         if (!skipEnvironmentObservers) { | ||||
|             this._observeObjectPath(); | ||||
|             this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         this._initializeActions(); | ||||
|     } | ||||
|  | ||||
|     disable(actionKeys) { | ||||
| @@ -84,11 +87,15 @@ class ActionCollection extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|             unsubscribe(); | ||||
|         }); | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         this.openmct.editor.off('isEditing', this._updateActions); | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
|             }); | ||||
|  | ||||
|             this.openmct.editor.off('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|     } | ||||
| @@ -123,6 +130,10 @@ class ActionCollection extends EventEmitter { | ||||
|         return statusBarActions; | ||||
|     } | ||||
|  | ||||
|     getActionsObject() { | ||||
|         return this.applicableActions; | ||||
|     } | ||||
|  | ||||
|     _update() { | ||||
|         this.emit('update', this.applicableActions); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										225
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         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); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -44,34 +44,12 @@ class ActionsAPI extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         let viewContext = view && view.getViewContext && view.getViewContext() || {}; | ||||
|         if (view) { | ||||
|  | ||||
|         if (view && !viewContext.skipCache) { | ||||
|             let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
|             if (cachedActionCollection) { | ||||
|                 return cachedActionCollection; | ||||
|             } else { | ||||
|                 let applicableActions = this._applicableActions(objectPath, view); | ||||
|                 let actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct); | ||||
|  | ||||
|                 this._actionCollections.set(view, actionCollection); | ||||
|                 actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|                 return actionCollection; | ||||
|             } | ||||
|             return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); | ||||
|         } else { | ||||
|             let applicableActions = this._applicableActions(objectPath, view); | ||||
|  | ||||
|             Object.keys(applicableActions).forEach(key => { | ||||
|                 let action = applicableActions[key]; | ||||
|  | ||||
|                 action.callBack = () => { | ||||
|                     return action.invoke(objectPath, view); | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return applicableActions; | ||||
|             return this._newActionCollection(objectPath, view, true); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -79,6 +57,27 @@ class ActionsAPI extends EventEmitter { | ||||
|         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); | ||||
|   | ||||
| @@ -22,22 +22,41 @@ | ||||
|  | ||||
| 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', | ||||
|             key: 'test-action', | ||||
|             cssClass: 'test-action', | ||||
|             description: 'This is a test action', | ||||
|             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 = {}) => { | ||||
| @@ -45,8 +64,6 @@ describe('The Actions API', () => { | ||||
|                     let viewContext = view.getViewContext(); | ||||
|  | ||||
|                     return viewContext.onlyAppliesToTestCase; | ||||
|                 } else if (objectPath.length) { | ||||
|                     return objectPath[0].type === 'fake-folder'; | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
| @@ -75,8 +92,7 @@ describe('The Actions API', () => { | ||||
|         mockViewContext1 = { | ||||
|             getViewContext: () => { | ||||
|                 return { | ||||
|                     onlyAppliesToTestCase: true, | ||||
|                     skipCache: true | ||||
|                     onlyAppliesToTestCase: true | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
| @@ -90,7 +106,8 @@ describe('The Actions API', () => { | ||||
|         it("adds action to ActionsAPI", () => { | ||||
|             actionsAPI.register(mockAction); | ||||
|  | ||||
|             let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key]; | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
| @@ -100,17 +117,34 @@ describe('The Actions API', () => { | ||||
|     describe("get method", () => { | ||||
|         beforeEach(() => { | ||||
|             actionsAPI.register(mockAction); | ||||
|             actionsAPI.register(mockObjectPathAction); | ||||
|         }); | ||||
|  | ||||
|         it("returns an object with relevant actions when invoked with objectPath only", () => { | ||||
|             let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key]; | ||||
|         it("returns an ActionCollection when invoked with an objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath); | ||||
|             let instanceOfActionCollection = actionCollection instanceof ActionCollection; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
|             expect(instanceOfActionCollection).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("returns an object with relevant actions when invoked with viewContext and skipCache", () => { | ||||
|             let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key]; | ||||
|         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); | ||||
|   | ||||
							
								
								
									
										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
									
								
							| @@ -26,6 +26,7 @@ define([ | ||||
|     './MutableObject', | ||||
|     './RootRegistry', | ||||
|     './RootObjectProvider', | ||||
|     './InterceptorRegistry', | ||||
|     'EventEmitter' | ||||
| ], function ( | ||||
|     _, | ||||
| @@ -33,6 +34,7 @@ define([ | ||||
|     MutableObject, | ||||
|     RootRegistry, | ||||
|     RootObjectProvider, | ||||
|     InterceptorRegistry, | ||||
|     EventEmitter | ||||
| ) { | ||||
|  | ||||
| @@ -48,6 +50,7 @@ define([ | ||||
|         this.rootRegistry = new RootRegistry(); | ||||
|         this.rootProvider = new RootObjectProvider.default(this.rootRegistry); | ||||
|         this.cache = {}; | ||||
|         this.interceptorRegistry = new InterceptorRegistry.default(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -177,6 +180,10 @@ define([ | ||||
|  | ||||
|         return objectPromise.then(result => { | ||||
|             delete this.cache[keystring]; | ||||
|             const interceptors = this.listGetInterceptors(identifier, result); | ||||
|             interceptors.forEach(interceptor => { | ||||
|                 result = interceptor.invoke(identifier, result); | ||||
|             }); | ||||
|  | ||||
|             return result; | ||||
|         }); | ||||
| @@ -312,6 +319,27 @@ define([ | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * 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); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Uniquely identifies a domain object. | ||||
|      * | ||||
|   | ||||
| @@ -63,12 +63,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 +117,15 @@ 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(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -20,13 +20,17 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
|  | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
|     './TelemetryValueFormatter', | ||||
|     './DefaultMetadataProvider', | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     CustomStringFormatter, | ||||
|     TelemetryMetadataManager, | ||||
|     TelemetryValueFormatter, | ||||
|     DefaultMetadataProvider, | ||||
| @@ -142,6 +146,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 | ||||
| @@ -260,6 +275,48 @@ define([ | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request telemetry collection for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
|      * (start, end, etc.), sort order, and strategies for retrieving | ||||
|      * telemetry (aggregation, latest available, etc.). | ||||
|      * | ||||
|      * @method request | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      * @param {module:openmct.DomainObject} domainObject the object | ||||
|      *        which has associated telemetry | ||||
|      * @param {module:openmct.TelemetryAPI~TelemetryRequest} options | ||||
|      *        options for this historical request | ||||
|      * @returns {Promise.<object[]>} a promise for an array of | ||||
|      *          telemetry data | ||||
|      */ | ||||
|     TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject) { | ||||
|         if (arguments.length === 1) { | ||||
|             arguments.length = 2; | ||||
|             arguments[1] = {}; | ||||
|         } | ||||
|  | ||||
|         // historical setup | ||||
|         this.standardizeRequestOptions(arguments[1]); | ||||
|         const historicalProvider = this.findRequestProvider(domainObject, arguments); | ||||
|  | ||||
|         // subscription setup | ||||
|         const subscriptionProvider = this.findSubscriptionProvider(domainObject); | ||||
|  | ||||
|         // check for no providers | ||||
|         if (!historicalProvider && !subscriptionProvider) { | ||||
|             return Promise.reject('No providers found'); | ||||
|         } | ||||
|  | ||||
|         let telemetryCollectionOptions = { | ||||
|             historicalProvider, | ||||
|             subscriptionProvider, | ||||
|             arguments: arguments | ||||
|         }; | ||||
|  | ||||
|         return Promise.resolve(new TelemetryCollection(this.openmct, domainObject, telemetryCollectionOptions)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request historical telemetry for a domain object. | ||||
|      * The `options` argument allows you to specify filters | ||||
| @@ -400,6 +457,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 +475,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. | ||||
|   | ||||
							
								
								
									
										315
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/api/telemetry/TelemetryCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,315 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 TelemetrySubscriptionService from './TelemetrySubscriptionService'; | ||||
|  | ||||
| function bindUs() { | ||||
|     return [ | ||||
|         'trackHistoricalTelemetry', | ||||
|         'trackSubscriptionTelemetry', | ||||
|         'addPage', | ||||
|         'processNewTelemetry', | ||||
|         'hasMorePages', | ||||
|         'nextPage', | ||||
|         'bounds', | ||||
|         'timeSystem', | ||||
|         'on', | ||||
|         'off', | ||||
|         'emit', | ||||
|         'subscribeToBounds', | ||||
|         'unsubscribeFromBounds', | ||||
|         'subscribeToTimeSystem', | ||||
|         'unsubscribeFromTimeSystem', | ||||
|         'destroy' | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| export class TelemetryCollection { | ||||
|  | ||||
|     constructor(openmct, domainObject, options) { | ||||
|         bindUs().forEach(method => this[method] = this[method].bind(this)); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.domainObject = domainObject; | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|         this.parseTime = undefined; | ||||
|         this.timeSystem(openmct.time.timeSystem()); | ||||
|         this.lastBounds = openmct.time.bounds(); | ||||
|  | ||||
|         this.historicalProvider = options.historicalProvider; | ||||
|         this.subscriptionProvider = options.subscriptionProvider; | ||||
|  | ||||
|         this.arguments = options.arguments; | ||||
|  | ||||
|         this.listeners = { | ||||
|             add: [], | ||||
|             remove: [] | ||||
|         }; | ||||
|  | ||||
|         this.trackHistoricalTelemetry(); | ||||
|         this.trackSubscriptionTelemetry(); | ||||
|  | ||||
|         this.subscribeToBounds(); | ||||
|         this.subscribeToTimeSystem(); | ||||
|     } | ||||
|  | ||||
|     // should we wait to track history until an 'add' listener is added? | ||||
|     async trackHistoricalTelemetry() { | ||||
|         if (!this.historicalProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // remove for reset | ||||
|         if (this.boundedTelemetry.length !== 0) { | ||||
|             this.emit('remove', this.boundedTelemetry); | ||||
|             this.boundedTelemetry = []; | ||||
|         } | ||||
|  | ||||
|         let historicalData = await this.historicalProvider.request.apply(this.domainObject, this.arguments).catch((rejected) => { | ||||
|             this.openmct.notifications.error('Error requesting telemetry data, see console for details'); | ||||
|             console.error(rejected); | ||||
|  | ||||
|             return Promise.reject(rejected); | ||||
|         }); | ||||
|  | ||||
|         // make sure it wasn't rejected | ||||
|         if (Array.isArray(historicalData)) { | ||||
|             // reset on requests, should only happen on initial load, | ||||
|             // bounds manually changed and time system changes | ||||
|             this.boundedTelemetry = historicalData; | ||||
|             this.emit('add', [...this.boundedTelemetry]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     trackSubscriptionTelemetry() { | ||||
|         if (!this.subscriptionProvider) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.subscriptionService = new TelemetrySubscriptionService(this.openmct); | ||||
|         this.unsubscribe = this.subscriptionService.subscribe( | ||||
|             this.domainObject, | ||||
|             this.processNewTelemetry, | ||||
|             this.subscriptionProvider, | ||||
|             this.arguments | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // utilized by telemetry provider to add more data | ||||
|     addPage(telemetryData) { | ||||
|         this.processNewTelemetry(telemetryData); | ||||
|     } | ||||
|  | ||||
|     // used to sort any new telemetry (add/page, historical, subscription) | ||||
|     processNewTelemetry(telemetryData) { | ||||
|  | ||||
|         let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; | ||||
|         let parsedValue; | ||||
|         let beforeStartOfBounds; | ||||
|         let afterEndOfBounds; | ||||
|         let added = []; | ||||
|  | ||||
|         for (let datum of data) { | ||||
|             parsedValue = this.parseTime(datum); | ||||
|             beforeStartOfBounds = parsedValue < this.lastBounds.start; | ||||
|             afterEndOfBounds = parsedValue > this.lastBounds.end; | ||||
|  | ||||
|             if (!afterEndOfBounds && !beforeStartOfBounds) { | ||||
|                 if (!this.boundedTelemetry.includes(datum)) { | ||||
|                     this.boundedTelemetry.push(datum); | ||||
|                     added.push(datum); | ||||
|                 } | ||||
|             } else if (afterEndOfBounds) { | ||||
|                 this.futureBuffer.push(datum); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (added.length) { | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // returns a boolean if there is more telemetry within the time bounds  | ||||
|     // if the provider supports it | ||||
|     hasMorePages() { | ||||
|         return this.historicalProvider | ||||
|             && this.historicalProvider.supportsPaging() | ||||
|             && this.historicalProvider.hasMorePages(this); | ||||
|     } | ||||
|  | ||||
|     // will return the next "page" of telemetry if the provider supports it | ||||
|     nextPage() { | ||||
|         if (!this.historicalProvider || !this.historicalProvider.supportsPaging()) { | ||||
|             throw new Error('Provider does not support paging'); | ||||
|         } | ||||
|  | ||||
|         this.historicalProvider.nextPage(this.arguments, this); | ||||
|     } | ||||
|  | ||||
|     // when user changes bounds, or when bounds increment from a tick | ||||
|     bounds(bounds, isTick) { | ||||
|  | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (isTick) { | ||||
|             // need to check futureBuffer and need to check | ||||
|             // if anything has fallen out of bounds | ||||
|         } else { | ||||
|             // TODO: also reset right? | ||||
|             // need to reset and request history again | ||||
|             // no need to mess with subscription | ||||
|         } | ||||
|  | ||||
|         let startChanged = this.lastBounds.start !== bounds.start; | ||||
|         let endChanged = this.lastBounds.end !== bounds.end; | ||||
|  | ||||
|         let startIndex = 0; | ||||
|         let endIndex = 0; | ||||
|  | ||||
|         let discarded = []; | ||||
|         let added = []; | ||||
|         let testValue = { | ||||
|             datum: {} | ||||
|         }; | ||||
|  | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (startChanged) { | ||||
|             testValue.datum[this.sortOptions.key] = bounds.start; | ||||
|             // Calculate the new index of the first item within the bounds | ||||
|             startIndex = this.sortedIndex(this.rows, testValue); | ||||
|             discarded = this.rows.splice(0, startIndex); | ||||
|         } | ||||
|  | ||||
|         if (endChanged) { | ||||
|             testValue.datum[this.sortOptions.key] = bounds.end; | ||||
|             // Calculate the new index of the last item in bounds | ||||
|             endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue); | ||||
|             added = this.futureBuffer.rows.splice(0, endIndex); | ||||
|             added.forEach((datum) => this.rows.push(datum)); | ||||
|         } | ||||
|  | ||||
|         if (discarded.length > 0) { | ||||
|             /** | ||||
|              * A `discarded` event is emitted when telemetry data fall out of | ||||
|              * bounds due to a bounds change event | ||||
|              * @type {object[]} discarded the telemetry data | ||||
|              * discarded as a result of the bounds change | ||||
|              */ | ||||
|             this.emit('remove', discarded); | ||||
|         } | ||||
|  | ||||
|         if (added.length > 0) { | ||||
|             /** | ||||
|              * An `added` event is emitted when a bounds change results in | ||||
|              * received telemetry falling within the new bounds. | ||||
|              * @type {object[]} added the telemetry data that is now within bounds | ||||
|              */ | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     timeSystem(timeSystem) { | ||||
|         let timeKey = timeSystem.key; | ||||
|         let formatter = this.openmct.telemetry.getValueFormatter({ | ||||
|             key: timeKey, | ||||
|             source: timeKey, | ||||
|             format: timeKey | ||||
|         }); | ||||
|  | ||||
|         this.parseTime = formatter.parse; | ||||
|  | ||||
|         // TODO: Reset right? | ||||
|     } | ||||
|  | ||||
|     on(event, callback, context) { | ||||
|         if (!this.listeners[event]) { | ||||
|             throw new Error('Event not supported by Telemetry Collections: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (this.listeners[event].includes(callback)) { | ||||
|             throw new Error('Tried to add a listener that is already registered'); | ||||
|         } | ||||
|  | ||||
|         this.listeners[event].push({ | ||||
|             callback: callback, | ||||
|             context: context | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Unregister TelemetryCollection events. | ||||
|     off(event, callback) { | ||||
|         if (!this.listeners[event]) { | ||||
|             throw new Error('Event not supported by Telemetry Collections: ' + event); | ||||
|         } | ||||
|  | ||||
|         if (!this.listeners[event].includes(callback)) { | ||||
|             throw new Error('Tried to remove a listener that does not exist'); | ||||
|         } | ||||
|  | ||||
|         this.listeners[event].remove(callback); | ||||
|     } | ||||
|  | ||||
|     emit(event, payload) { | ||||
|         if (!this.listeners[event].length) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         payload = [...payload]; | ||||
|  | ||||
|         this.listeners[event].forEach((listener) => { | ||||
|             if (listener.context) { | ||||
|                 listener.callback.apply(listener.context, payload); | ||||
|             } else { | ||||
|                 listener.callback(payload); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     subscribeToBounds() { | ||||
|         this.openmct.time.on('bounds', this.bounds); | ||||
|     } | ||||
|  | ||||
|     unsubscribeFromBounds() { | ||||
|         this.openmct.time.off('bounds', this.bounds); | ||||
|     } | ||||
|  | ||||
|     subscribeToTimeSystem() { | ||||
|         this.openmct.time.on('timeSystem', this.timeSystem); | ||||
|     } | ||||
|  | ||||
|     unsubscribeFromTimeSystem() { | ||||
|         this.openmct.time.off('bounds', this.timeSystem); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.unsubscribeFromBounds(); | ||||
|         this.unsubscribeFromTimeSystem(); | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/api/telemetry/TelemetrySubscriptionService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/api/telemetry/TelemetrySubscriptionService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 objectUtils from 'objectUtils'; | ||||
| class TelemetrySubscriptionService { | ||||
|     constructor(openmct) { | ||||
|         if (!TelemetrySubscriptionService.instance) { | ||||
|             this.openmct = openmct; | ||||
|             this.subscriptionCache = {}; | ||||
|  | ||||
|             TelemetrySubscriptionService.instance = this; | ||||
|         } | ||||
|  | ||||
|         return TelemetrySubscriptionService.instance; // eslint-disable-line no-constructor-return | ||||
|     } | ||||
|  | ||||
|     subscribe(domainObject, callback, provider, options) { | ||||
|         const keyString = objectUtils.makeKeyString(domainObject.identifier); | ||||
|         let subscriber = this.subscriptionCache[keyString]; | ||||
|  | ||||
|         if (!subscriber) { | ||||
|             subscriber = this.subscriptionCache[keyString] = { | ||||
|                 callbacks: [callback] | ||||
|             }; | ||||
|  | ||||
|             subscriber.unsubscribe = provider | ||||
|                 .subscribe(domainObject, function (value) { | ||||
|                     subscriber.callbacks.forEach(function (cb) { | ||||
|                         cb(value); | ||||
|                     }); | ||||
|                 }, options); | ||||
|         } else { | ||||
|             subscriber.callbacks.push(callback); | ||||
|         } | ||||
|  | ||||
|         return () => { | ||||
|             subscriber.callbacks = subscriber.callbacks.filter((cb) => { | ||||
|                 return cb !== callback; | ||||
|             }); | ||||
|             if (subscriber.callbacks.length === 0) { | ||||
|                 subscriber.unsubscribe(); | ||||
|                 delete this.subscriptionCache[keyString]; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| function instance(openmct) { | ||||
|     return new TelemetrySubscriptionService(openmct); | ||||
| } | ||||
|  | ||||
| export default instance; | ||||
| @@ -44,6 +44,7 @@ | ||||
| <script> | ||||
|  | ||||
| const CONTEXT_MENU_ACTIONS = [ | ||||
|     'viewDatumAction', | ||||
|     'viewHistoricalData', | ||||
|     'remove' | ||||
| ]; | ||||
| @@ -129,6 +130,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); | ||||
| @@ -175,8 +177,22 @@ export default { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: () => { | ||||
|                             return this.datum; | ||||
|                         } | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true}); | ||||
|             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); | ||||
|   | ||||
| @@ -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); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -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"; | ||||
| @@ -402,7 +403,8 @@ describe('the plugin', function () { | ||||
|             component = new Vue({ | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     selection: selection | ||||
|                     selection: selection, | ||||
|                     stylesManager | ||||
|                 }, | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|   | ||||
							
								
								
									
										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); | ||||
|     }); | ||||
| }); | ||||
| @@ -5,29 +5,30 @@ export default class CopyToClipboardAction { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy to Clipboard action'; | ||||
|         this.description = 'Copy value to clipboard'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToClipboard'; | ||||
|         this.name = 'Copy to Clipboard'; | ||||
|         this.priority = 9; | ||||
|         this.priority = 1; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, viewContext) { | ||||
|     invoke(objectPath, view = {}) { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|  | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
|                 this.openmct.notifications.info(`Success : copied to clipboard '${formattedValue}'`); | ||||
|                 this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|                 this.openmct.notifications.error(`Failed : to copy to clipboard '${formattedValue}'`); | ||||
|                 this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, viewContext) { | ||||
|         if (viewContext && viewContext.getViewKey) { | ||||
|             return viewContext.getViewKey().includes('alphanumeric-format'); | ||||
|         } | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         return false; | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ | ||||
| > | ||||
|     <div | ||||
|         v-if="domainObject" | ||||
|         class="c-telemetry-view" | ||||
|         class="c-telemetry-view u-style-receiver" | ||||
|         :class="[statusClass]" | ||||
|         :style="styleObject" | ||||
|         :data-font-size="item.fontSize" | ||||
| @@ -38,7 +38,7 @@ | ||||
|         @contextmenu.prevent="showContextMenu" | ||||
|     > | ||||
|         <div class="is-status__indicator" | ||||
|              :title="`This item is ${this.status}`" | ||||
|              :title="`This item is ${status}`" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="showLabel" | ||||
| @@ -71,7 +71,6 @@ | ||||
|  | ||||
| <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'; | ||||
|  | ||||
| @@ -172,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() { | ||||
| @@ -180,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) { | ||||
| @@ -231,17 +230,12 @@ export default { | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|     }, | ||||
|     methods: { | ||||
|         getViewContext() { | ||||
|             return { | ||||
|                 getViewKey: () => this.viewKey, | ||||
|                 formattedValueForCopy: this.formattedValueForCopy | ||||
|             }; | ||||
|         }, | ||||
|         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} ${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(); | ||||
| @@ -282,10 +276,10 @@ export default { | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext() { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         skipCache: true | ||||
|                         formattedValueForCopy: this.formattedValueForCopy | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
| @@ -296,6 +290,10 @@ export default { | ||||
|             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(); | ||||
|  | ||||
| @@ -313,36 +311,35 @@ export default { | ||||
|             this.removeSelectable = this.openmct.selection.selectable( | ||||
|                 this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|             delete this.immediatelySelect; | ||||
|  | ||||
|             let allActions = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|  | ||||
|             this.applicableActions = CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return allActions[actionKey]; | ||||
|             }); | ||||
|         }, | ||||
|         updateTelemetryFormat(format) { | ||||
|             this.customStringformatter.setFormat(format); | ||||
|  | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         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(); | ||||
|  | ||||
|             const actionsObject = this.openmct.actions.get(this.currentObjectPath, this.getViewContext(), { viewHistoricalData: true }).applicableActions; | ||||
|             let applicableActionKeys = Object.keys(actionsObject) | ||||
|                 .filter(key => { | ||||
|                     const isCopyToNotebook = actionsObject[key].key === 'copyToNotebook'; | ||||
|                     if (defaultNotebook && isCopyToNotebook) { | ||||
|                         const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                         actionsObject[key].name = `Copy to Notebook ${defaultPath}`; | ||||
|                     } | ||||
|             let copyToNotebookAction = actionsObject.copyToNotebook; | ||||
|  | ||||
|                     return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key); | ||||
|                 }); | ||||
|             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 applicableActionKeys.map(key => actionsObject[key]); | ||||
|             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) { | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|         flex: 1 1 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|        // justify-content: center; | ||||
|         align-items: center; | ||||
|         overflow: hidden; | ||||
|         padding: $interiorMargin; | ||||
| @@ -28,19 +27,12 @@ | ||||
|     } | ||||
|  | ||||
|     .is-status__indicator { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|     } | ||||
|  | ||||
|     &.is-status--missing { | ||||
|         @include isMissing($absPos: true); | ||||
|  | ||||
|         border: $borderMissing; | ||||
|     } | ||||
|  | ||||
|     &.is-status--suspect { | ||||
|         @include isSuspect($absPos: true); | ||||
|  | ||||
|     &[class*='is-status'] { | ||||
|         border: $borderMissing; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										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'); | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +1,9 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * 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 Web is licensed under the Apache License, Version 2.0 (the | ||||
|  * 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.
 | ||||
| @@ -14,31 +14,15 @@ | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT Web includes source code licensed under additional open source | ||||
|  * 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"; | ||||
| 
 | ||||
| define(["./LocalClock"], function (LocalClock) { | ||||
|     describe("The LocalClock class", function () { | ||||
|         let clock; | ||||
|         let mockTimeout; | ||||
|         const timeoutHandle = {}; | ||||
| 
 | ||||
|         beforeEach(function () { | ||||
|             mockTimeout = jasmine.createSpy("timeout"); | ||||
|             mockTimeout.and.returnValue(timeoutHandle); | ||||
| 
 | ||||
|             clock = new LocalClock(0); | ||||
|             clock.start(); | ||||
|         }); | ||||
| 
 | ||||
|         it("calls listeners on tick with current time", function () { | ||||
|             const mockListener = jasmine.createSpy("listener"); | ||||
|             clock.on('tick', mockListener); | ||||
|             clock.tick(); | ||||
|             expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new DuplicateAction(openmct)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										157
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| /***************************************************************************** | ||||
|  * 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", | ||||
|                     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.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); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -27,7 +27,7 @@ | ||||
|     </div> | ||||
|     <div class="c-grid-item__controls"> | ||||
|         <div class="is-status__indicator" | ||||
|              title="This item is missing or suspect" | ||||
|              :title="`This item is ${status}`" | ||||
|         ></div> | ||||
|         <div | ||||
|             class="icon-people" | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|                 :class="item.type.cssClass" | ||||
|             > | ||||
|                 <span class="is-status__indicator" | ||||
|                       title="This item is missing or suspect" | ||||
|                       :title="`This item is ${status}`" | ||||
|                 ></span> | ||||
|             </div> | ||||
|             <div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div> | ||||
|   | ||||
| @@ -1,121 +0,0 @@ | ||||
| /******************************* GRID ITEMS */ | ||||
| .c-grid-item { | ||||
|     // Mobile-first | ||||
|     @include button($bg: $colorItemBg, $fg: $colorItemFg); | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     padding: $interiorMarginLg; | ||||
|  | ||||
|     &__type-icon { | ||||
|         filter: $colorKeyFilter; | ||||
|         flex: 0 0 $gridItemMobile; | ||||
|         font-size: floor($gridItemMobile / 2); | ||||
|         margin-right: $interiorMarginLg; | ||||
|     } | ||||
|  | ||||
|     &.is-alias { | ||||
|         // Object is an alias to an original. | ||||
|         [class*='__type-icon'] { | ||||
|             @include isAlias(); | ||||
|             color: $colorIconAliasForKeyFilter; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__details { | ||||
|         display: flex; | ||||
|         flex-flow: column nowrap; | ||||
|         flex: 1 1 auto; | ||||
|     } | ||||
|  | ||||
|     &__name { | ||||
|         @include ellipsize(); | ||||
|         color: $colorItemFg; | ||||
|         @include headerFont(1.2em); | ||||
|         margin-bottom: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     &__metadata { | ||||
|         color: $colorItemFgDetails; | ||||
|         font-size: 0.9em; | ||||
|  | ||||
|         body.mobile & { | ||||
|             [class*='__item-count'] { | ||||
|                 &:before { | ||||
|                     content: ' - '; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__controls { | ||||
|         color: $colorItemFgDetails; | ||||
|         flex: 0 0 64px; | ||||
|         font-size: 1.2em; | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: flex-end; | ||||
|  | ||||
|         > * + * { | ||||
|             margin-left: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     body.desktop & { | ||||
|         $transOutMs: 300ms; | ||||
|         flex-flow: column nowrap; | ||||
|         transition: background $transOutMs ease-in-out; | ||||
|  | ||||
|         &:hover { | ||||
|             background: $colorItemBgHov; | ||||
|             transition: $transIn; | ||||
|  | ||||
|             .c-grid-item__type-icon { | ||||
|                 filter: $colorKeyFilterHov; | ||||
|                 transform: scale(1); | ||||
|                 transition: $transInBounce; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         > * { | ||||
|             margin: 0; // Reset from mobile | ||||
|         } | ||||
|  | ||||
|         &__controls { | ||||
|             align-items: start; | ||||
|             flex: 0 0 auto; | ||||
|             order: 1; | ||||
|             .c-info-button, | ||||
|             .c-pointer-icon { display: none; } | ||||
|         } | ||||
|  | ||||
|         &__type-icon { | ||||
|             flex: 1 1 auto; | ||||
|             font-size: floor($gridItemDesk / 3); | ||||
|             margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%; | ||||
|             order: 2; | ||||
|             transform: scale(0.9); | ||||
|             transform-origin: center; | ||||
|             transition: all $transOutMs ease-in-out; | ||||
|         } | ||||
|  | ||||
|         &__details { | ||||
|             flex: 0 0 auto; | ||||
|             justify-content: flex-end; | ||||
|             order: 3; | ||||
|         } | ||||
|  | ||||
|         &__metadata { | ||||
|             display: flex; | ||||
|  | ||||
|             &__type { | ||||
|                 flex: 1 1 auto; | ||||
|                 @include ellipsize(); | ||||
|             } | ||||
|  | ||||
|             &__item-count { | ||||
|                 opacity: 0.7; | ||||
|                 flex: 0 0 auto; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -43,18 +43,20 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-status--missing { | ||||
|         @include isMissing(); | ||||
|     &.is-status--notebook-default { | ||||
|         .is-status__indicator { | ||||
|             display: block; | ||||
|  | ||||
|         [class*='__type-icon'], | ||||
|         [class*='__details'] { | ||||
|             opacity: $opacityMissing; | ||||
|             &:before { | ||||
|                 color: $colorFilter; | ||||
|                 content: $glyph-icon-notebook-page; | ||||
|                 font-family: symbolsfont; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-status--suspect { | ||||
|         @include isSuspect(); | ||||
|  | ||||
|     &[class*='is-status--missing'], | ||||
|     &[class*='is-status--suspect']{ | ||||
|         [class*='__type-icon'], | ||||
|         [class*='__details'] { | ||||
|             opacity: $opacityMissing; | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|         <div class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|         > | ||||
|             <div class="c-imagery__main-image__image" | ||||
|             <div class="c-imagery__main-image__image js-imageryView-image" | ||||
|                  :style="{ | ||||
|                      'background-image': imageUrl ? `url(${imageUrl})` : 'none', | ||||
|                      'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
| @@ -137,7 +137,8 @@ export default { | ||||
|             refreshCSS: false, | ||||
|             keyString: undefined, | ||||
|             focusedImageIndex: undefined, | ||||
|             numericDuration: undefined | ||||
|             numericDuration: undefined, | ||||
|             telemetryCollection: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -222,6 +223,7 @@ export default { | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
|         this.requestTelemetry(); | ||||
|     }, | ||||
|     updated() { | ||||
|         this.scrollToRight(); | ||||
| @@ -353,6 +355,15 @@ export default { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         async requestTelemetry() { | ||||
|             this.telemetryCollection = await this.openmct.telemetry.requestTelemetryCollection(this.domainObject); | ||||
|             this.telemetryCollection.on('add', (data) => { | ||||
|                 console.log('added data', data); | ||||
|             }); | ||||
|             this.telemetryCollection.on('remove', (data) => { | ||||
|                 console.log('removed data', data); | ||||
|             }); | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|   | ||||
							
								
								
									
										306
									
								
								src/plugins/imagery/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								src/plugins/imagery/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,306 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 ImageryPlugin from './plugin.js'; | ||||
| import Vue from 'vue'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     simulateKeyEvent | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| const ONE_MINUTE = 1000 * 60; | ||||
| const TEN_MINUTES = ONE_MINUTE * 10; | ||||
| const MAIN_IMAGE_CLASS = '.js-imageryView-image'; | ||||
| const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
|  | ||||
| function getImageInfo(doc) { | ||||
|     let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; | ||||
|     let timestamp = imageElement.dataset.openmctImageTimestamp; | ||||
|     let identifier = imageElement.dataset.openmctObjectKeystring; | ||||
|     let url = imageElement.style.backgroundImage; | ||||
|  | ||||
|     return { | ||||
|         timestamp, | ||||
|         identifier, | ||||
|         url | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function isNew(doc) { | ||||
|     let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS); | ||||
|  | ||||
|     return newIcon.length !== 0; | ||||
| } | ||||
|  | ||||
| function generateTelemetry(start, count) { | ||||
|     let telemetry = []; | ||||
|  | ||||
|     for (let i = 1, l = count + 1; i < l; i++) { | ||||
|         let stringRep = i + 'minute'; | ||||
|         let logo = 'images/logo-openmct.svg'; | ||||
|  | ||||
|         telemetry.push({ | ||||
|             "name": stringRep + " Imagery", | ||||
|             "utc": start + (i * ONE_MINUTE), | ||||
|             "url": location.host + '/' + logo + '?time=' + stringRep, | ||||
|             "timeId": stringRep | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return telemetry; | ||||
| } | ||||
|  | ||||
| describe("The Imagery View Layout", () => { | ||||
|     const imageryKey = 'example.imagery'; | ||||
|     const START = Date.now(); | ||||
|     const COUNT = 10; | ||||
|  | ||||
|     let openmct; | ||||
|     let imageryPlugin; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let timeFormat = 'utc'; | ||||
|     let bounds = { | ||||
|         start: START - TEN_MINUTES, | ||||
|         end: START | ||||
|     }; | ||||
|     let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); | ||||
|     let imageryObject = { | ||||
|         identifier: { | ||||
|             namespace: "", | ||||
|             key: "imageryId" | ||||
|         }, | ||||
|         name: "Example Imagery", | ||||
|         type: "example.imagery", | ||||
|         location: "parentId", | ||||
|         modified: 0, | ||||
|         persisted: 0, | ||||
|         telemetry: { | ||||
|             values: [ | ||||
|                 { | ||||
|                     "name": "Image", | ||||
|                     "key": "url", | ||||
|                     "format": "image", | ||||
|                     "hints": { | ||||
|                         "image": 1, | ||||
|                         "priority": 3 | ||||
|                     }, | ||||
|                     "source": "url" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Name", | ||||
|                     "key": "name", | ||||
|                     "source": "name", | ||||
|                     "hints": { | ||||
|                         "priority": 0 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Time", | ||||
|                     "key": "utc", | ||||
|                     "format": "utc", | ||||
|                     "hints": { | ||||
|                         "domain": 2, | ||||
|                         "priority": 1 | ||||
|                     }, | ||||
|                     "source": "utc" | ||||
|                 }, | ||||
|                 { | ||||
|                     "name": "Local Time", | ||||
|                     "key": "local", | ||||
|                     "format": "local-format", | ||||
|                     "hints": { | ||||
|                         "domain": 1, | ||||
|                         "priority": 2 | ||||
|                     }, | ||||
|                     "source": "local" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // this setups up the app | ||||
|     beforeEach((done) => { | ||||
|         const appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         parent = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|  | ||||
|         imageryPlugin = new ImageryPlugin(); | ||||
|         openmct.install(imageryPlugin); | ||||
|  | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|  | ||||
|         openmct.time.timeSystem(timeFormat, { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
|         }); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should provide an imagery view only for imagery producing objects", () => { | ||||
|         let applicableViews = openmct.objectViews.get(imageryObject); | ||||
|         let imageryView = applicableViews.find( | ||||
|             viewProvider => viewProvider.key === imageryKey | ||||
|         ); | ||||
|  | ||||
|         expect(imageryView).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("imagery view", () => { | ||||
|         let applicableViews; | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|  | ||||
|         beforeEach(async (done) => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryRequestPromise = new Promise((resolve) => { | ||||
|                 telemetryRequestResolve = resolve; | ||||
|             }); | ||||
|  | ||||
|             openmct.telemetry.request.and.callFake(() => { | ||||
|                 telemetryRequestResolve(imageTelemetry); | ||||
|  | ||||
|                 return telemetryRequestPromise; | ||||
|             }); | ||||
|  | ||||
|             openmct.time.clock('local', { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end + 100 | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             await telemetryRequestPromise; | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             return done(); | ||||
|         }); | ||||
|  | ||||
|         it("on mount should show the the most recent image", () => { | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show the clicked thumbnail as the main image", async () => { | ||||
|             const target = imageTelemetry[5].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|             await Vue.nextTick(); | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is new", async (done) => { | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeTrue(); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is not new", async (done) => { | ||||
|             const target = imageTelemetry[2].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeFalse(); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via arrow keys", async () => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
|                 keyCode: 37, | ||||
|                 type: 'keyup' | ||||
|             }; | ||||
|  | ||||
|             simulateKeyEvent(keyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via numerous arrow keys", async () => { | ||||
|             let element = parent.querySelector('.c-imagery'); | ||||
|             let type = 'keyup'; | ||||
|             let leftKeyOpts = { | ||||
|                 element, | ||||
|                 type, | ||||
|                 key: 'ArrowLeft', | ||||
|                 keyCode: 37 | ||||
|             }; | ||||
|             let rightKeyOpts = { | ||||
|                 element, | ||||
|                 type, | ||||
|                 key: 'ArrowRight', | ||||
|                 keyCode: 39 | ||||
|             }; | ||||
|  | ||||
|             // left thrice | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             simulateKeyEvent(leftKeyOpts); | ||||
|             // right once | ||||
|             simulateKeyEvent(rightKeyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -20,40 +20,21 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| define( | ||||
|     ['./AbstractComposeAction'], | ||||
|     function (AbstractComposeAction) { | ||||
| 
 | ||||
|         /** | ||||
|          * The MoveAction is available from context menus and allows a user to | ||||
|          * move an object to another location of their choosing. | ||||
|          * | ||||
|          * @implements {Action} | ||||
|          * @constructor | ||||
|          * @memberof platform/entanglement | ||||
|          */ | ||||
|         function MoveAction(policyService, locationService, moveService, context) { | ||||
|             AbstractComposeAction.apply( | ||||
|                 this, | ||||
|                 [policyService, locationService, moveService, context, "Move"] | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         MoveAction.prototype = Object.create(AbstractComposeAction.prototype); | ||||
| 
 | ||||
|         MoveAction.appliesTo = function (context) { | ||||
|             var applicableObject = | ||||
|                 context.selectedObject || context.domainObject; | ||||
| 
 | ||||
|             if (applicableObject && applicableObject.model.locked) { | ||||
|                 return false; | ||||
| export default function MissingObjectInterceptor(openmct) { | ||||
|     openmct.objects.addGetInterceptor({ | ||||
|         appliesTo: (identifier, domainObject) => { | ||||
|             return identifier.key !== 'mine'; | ||||
|         }, | ||||
|         invoke: (identifier, object) => { | ||||
|             if (object === undefined) { | ||||
|                 return { | ||||
|                     identifier, | ||||
|                     type: 'unknown', | ||||
|                     name: 'Missing: ' + openmct.objects.makeKeyString(identifier) | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             return Boolean(applicableObject | ||||
|                 && applicableObject.hasCapability('context')); | ||||
|         }; | ||||
| 
 | ||||
|         return MoveAction; | ||||
|     } | ||||
| ); | ||||
| 
 | ||||
|             return object; | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| @@ -20,44 +20,24 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| define([], function () { | ||||
| export default function MyItemsInterceptor(openmct) { | ||||
| 
 | ||||
|     /** | ||||
|      * Disallow moves when either the parent or the child are not | ||||
|      * modifiable by users. | ||||
|      * @constructor | ||||
|      * @implements {Policy} | ||||
|      * @memberof platform/entanglement | ||||
|      */ | ||||
|     function MovePolicy() { | ||||
|     } | ||||
|     openmct.objects.addGetInterceptor({ | ||||
|         appliesTo: (identifier, domainObject) => { | ||||
|             return identifier.key === 'mine'; | ||||
|         }, | ||||
|         invoke: (identifier, object) => { | ||||
|             if (object === undefined) { | ||||
|                 return { | ||||
|                     identifier, | ||||
|                     "name": "My Items", | ||||
|                     "type": "folder", | ||||
|                     "composition": [], | ||||
|                     "location": "ROOT" | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|     function parentOf(domainObject) { | ||||
|         var context = domainObject.getCapability('context'); | ||||
| 
 | ||||
|         return context && context.getParent(); | ||||
|     } | ||||
| 
 | ||||
|     function allowMutation(domainObject) { | ||||
|         var type = domainObject && domainObject.getCapability('type'); | ||||
| 
 | ||||
|         return Boolean(type && type.hasFeature('creation')); | ||||
|     } | ||||
| 
 | ||||
|     function selectedObject(context) { | ||||
|         return context.selectedObject || context.domainObject; | ||||
|     } | ||||
| 
 | ||||
|     MovePolicy.prototype.allow = function (action, context) { | ||||
|         var key = action.getMetadata().key; | ||||
| 
 | ||||
|         if (key === 'move') { | ||||
|             return allowMutation(selectedObject(context)) | ||||
|                 && allowMutation(parentOf(selectedObject(context))); | ||||
|             return object; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     }; | ||||
| 
 | ||||
|     return MovePolicy; | ||||
| }); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/plugins/interceptors/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/plugins/interceptors/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import missingObjectInterceptor from "./missingObjectInterceptor"; | ||||
| import myItemsInterceptor from "./myItemsInterceptor"; | ||||
|  | ||||
| export default function plugin() { | ||||
|     return function install(openmct) { | ||||
|         myItemsInterceptor(openmct); | ||||
|         missingObjectInterceptor(openmct); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										114
									
								
								src/plugins/localTimeSystem/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/plugins/localTimeSystem/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The local time", () => { | ||||
|     const LOCAL_FORMAT_KEY = 'local-format'; | ||||
|     const LOCAL_SYSTEM_KEY = 'local'; | ||||
|     const JUNK = "junk"; | ||||
|     const TIMESTAMP = -14256000000; | ||||
|     const DATESTRING = '1969-07-20 12:00:00.000 am'; | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("system", function () { | ||||
|  | ||||
|         let localTimeSystem; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("is installed", () => { | ||||
|             let timeSystems = openmct.time.getAllTimeSystems(); | ||||
|             let local = timeSystems.find(ts => ts.key === LOCAL_SYSTEM_KEY); | ||||
|  | ||||
|             expect(local).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("can be set to be the main time system", () => { | ||||
|             expect(openmct.time.timeSystem().key).toBe(LOCAL_SYSTEM_KEY); | ||||
|         }); | ||||
|  | ||||
|         it("uses the local-format time format", () => { | ||||
|             expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY); | ||||
|         }); | ||||
|  | ||||
|         it("is UTC based", () => { | ||||
|             expect(localTimeSystem.isUTCBased).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("defines expected metadata", () => { | ||||
|             expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY); | ||||
|             expect(localTimeSystem.name).toBeDefined(); | ||||
|             expect(localTimeSystem.cssClass).toBeDefined(); | ||||
|             expect(localTimeSystem.durationFormat).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("formatter can be obtained from the telemetry API and", () => { | ||||
|  | ||||
|         let localTimeFormatter; | ||||
|         let dateString; | ||||
|         let timeStamp; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY); | ||||
|             dateString = localTimeFormatter.format(TIMESTAMP); | ||||
|             timeStamp = localTimeFormatter.parse(DATESTRING); | ||||
|         }); | ||||
|  | ||||
|         it("will format a timestamp in local time format", () => { | ||||
|             expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString); | ||||
|         }); | ||||
|  | ||||
|         it("will parse an local time Date String into milliseconds", () => { | ||||
|             expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp); | ||||
|         }); | ||||
|  | ||||
|         it("will validate correctly", () => { | ||||
|             expect(localTimeFormatter.validate(DATESTRING)).toBe(true); | ||||
|             expect(localTimeFormatter.validate(JUNK)).toBe(false); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										169
									
								
								src/plugins/move/MoveAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/plugins/move/MoveAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 MoveAction { | ||||
|     constructor(openmct) { | ||||
|         this.name = 'Move'; | ||||
|         this.key = 'move'; | ||||
|         this.description = 'Move this object from its containing object to another object.'; | ||||
|         this.cssClass = "icon-move"; | ||||
|         this.group = "action"; | ||||
|         this.priority = 7; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     async invoke(objectPath) { | ||||
|         let object = objectPath[0]; | ||||
|         let inNavigationPath = this.inNavigationPath(object); | ||||
|         let oldParent = objectPath[1]; | ||||
|         let dialogService = this.openmct.$injector.get('dialogService'); | ||||
|         let dialogForm = this.getDialogForm(object, oldParent); | ||||
|         let userInput = await dialogService.getUserInput(dialogForm, { name: object.name }); | ||||
|  | ||||
|         // if we need to update name | ||||
|         if (object.name !== userInput.name) { | ||||
|             this.openmct.objects.mutate(object, 'name', userInput.name); | ||||
|         } | ||||
|  | ||||
|         let parentContext = userInput.location.getCapability('context'); | ||||
|         let newParent = await this.openmct.objects.get(parentContext.domainObject.id); | ||||
|  | ||||
|         if (inNavigationPath && this.openmct.editor.isEditing()) { | ||||
|             this.openmct.editor.save(); | ||||
|         } | ||||
|  | ||||
|         this.addToNewParent(object, newParent); | ||||
|         this.removeFromOldParent(oldParent, object); | ||||
|  | ||||
|         if (inNavigationPath) { | ||||
|             let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier); | ||||
|             let root = await this.openmct.objects.getRoot(); | ||||
|             let rootChildCount = root.composition.length; | ||||
|  | ||||
|             // if not multiple root children, remove root from path | ||||
|             if (rootChildCount < 2) { | ||||
|                 newObjectPath.pop(); // remove ROOT | ||||
|             } | ||||
|  | ||||
|             this.navigateTo(newObjectPath); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     inNavigationPath(object) { | ||||
|         return this.openmct.router.path | ||||
|             .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)); | ||||
|     } | ||||
|  | ||||
|     navigateTo(objectPath) { | ||||
|         let urlPath = objectPath.reverse() | ||||
|             .map(object => this.openmct.objects.makeKeyString(object.identifier)) | ||||
|             .join("/"); | ||||
|  | ||||
|         window.location.href = '#/browse/' + urlPath; | ||||
|     } | ||||
|  | ||||
|     addToNewParent(child, newParent) { | ||||
|         let newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier); | ||||
|         let compositionCollection = this.openmct.composition.get(newParent); | ||||
|  | ||||
|         this.openmct.objects.mutate(child, 'location', newParentKeyString); | ||||
|         compositionCollection.add(child); | ||||
|     } | ||||
|  | ||||
|     removeFromOldParent(parent, child) { | ||||
|         let compositionCollection = this.openmct.composition.get(parent); | ||||
|  | ||||
|         compositionCollection.remove(child); | ||||
|     } | ||||
|  | ||||
|     getDialogForm(object, parent) { | ||||
|         return { | ||||
|             name: "Move Item", | ||||
|             sections: [ | ||||
|                 { | ||||
|                     rows: [ | ||||
|                         { | ||||
|                             key: "name", | ||||
|                             control: "textfield", | ||||
|                             name: "Folder Name", | ||||
|                             pattern: "\\S+", | ||||
|                             required: true, | ||||
|                             cssClass: "l-input-lg" | ||||
|                         }, | ||||
|                         { | ||||
|                             name: "location", | ||||
|                             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 (!parentCandidateKeystring || !currentParentKeystring) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidateKeystring === currentParentKeystring) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidateKeystring === objectKeystring) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (parentCandidate.getModel().composition.indexOf(objectKeystring) !== -1) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             return this.openmct.composition.checkPolicy( | ||||
|                 parentCandidate.useCapability('adapter'), | ||||
|                 object | ||||
|             ); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|  | ||||
|         if (child.locked || (parent && parent.locked)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return parentType | ||||
|             && parentType.definition.creatable | ||||
|             && childType | ||||
|             && childType.definition.creatable | ||||
|             && Array.isArray(parent.composition); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/plugins/move/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/plugins/move/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 MoveAction from "./MoveAction"; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new MoveAction(openmct)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										110
									
								
								src/plugins/move/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/plugins/move/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 MoveActionPlugin from './plugin.js'; | ||||
| import MoveAction from './MoveAction.js'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     getMockObjects | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The Move Action plugin", () => { | ||||
|  | ||||
|     let openmct; | ||||
|     let moveAction; | ||||
|     let childObject; | ||||
|     let parentObject; | ||||
|     let anotherParentObject; | ||||
|  | ||||
|     // this setups up the app | ||||
|     beforeEach((done) => { | ||||
|         const appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
|         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", | ||||
|                     composition: [childObject.identifier] | ||||
|                 } | ||||
|             } | ||||
|         }).folder; | ||||
|         anotherParentObject = getMockObjects({ | ||||
|             objectKeyStrings: ['folder'], | ||||
|             overwrite: { | ||||
|                 folder: { | ||||
|                     name: "Another Parent Folder" | ||||
|                 } | ||||
|             } | ||||
|         }).folder; | ||||
|  | ||||
|         // already installed by default, but never hurts, just adds to context menu | ||||
|         openmct.install(MoveActionPlugin()); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|         expect(MoveActionPlugin).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("when moving an object to a new parent and removing from the old parent", () => { | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             moveAction = new MoveAction(openmct); | ||||
|             moveAction.addToNewParent(childObject, anotherParentObject); | ||||
|             moveAction.removeFromOldParent(parentObject, childObject); | ||||
|         }); | ||||
|  | ||||
|         it("the child object's identifier should be in the new parent's composition", () => { | ||||
|             let newParentChild = anotherParentObject.composition[0]; | ||||
|             expect(newParentChild).toEqual(childObject.identifier); | ||||
|         }); | ||||
|  | ||||
|         it("the child object's identifier should be removed from the old parent's composition", () => { | ||||
|             let oldParentComposition = parentObject.composition; | ||||
|             expect(oldParentComposition.length).toEqual(0); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -6,11 +6,11 @@ export default class CopyToNotebookAction { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy to Notebook action'; | ||||
|         this.description = 'Copy value to notebook as an entry'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToNotebook'; | ||||
|         this.name = 'Copy to Notebook'; | ||||
|         this.priority = 9; | ||||
|         this.priority = 1; | ||||
|     } | ||||
|  | ||||
|     copyToNotebook(entryText) { | ||||
| @@ -25,15 +25,16 @@ export default class CopyToNotebookAction { | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, viewContext) { | ||||
|     invoke(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         this.copyToNotebook(viewContext.formattedValueForCopy()); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, viewContext) { | ||||
|         if (viewContext && viewContext.getViewKey) { | ||||
|             return viewContext.getViewKey().includes('alphanumeric-format'); | ||||
|         } | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         return false; | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,12 +24,12 @@ | ||||
|                  :default-section-id="defaultSectionId" | ||||
|                  :domain-object="internalDomainObject" | ||||
|                  :page-title="internalDomainObject.configuration.pageTitle" | ||||
|                  :pages="pages" | ||||
|                  :section-title="internalDomainObject.configuration.sectionTitle" | ||||
|                  :sections="sections" | ||||
|                  :selected-section="selectedSection" | ||||
|                  :sidebar-covers-entries="sidebarCoversEntries" | ||||
|                  @updatePage="updatePage" | ||||
|                  @updateSection="updateSection" | ||||
|                  @pagesChanged="pagesChanged" | ||||
|                  @sectionsChanged="sectionsChanged" | ||||
|                  @toggleNav="toggleNav" | ||||
|         /> | ||||
|         <div class="c-notebook__page-view"> | ||||
| @@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue'; | ||||
| import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| import { throttle } from 'lodash'; | ||||
| @@ -220,7 +220,7 @@ export default { | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|             this.sectionsChanged({ sections }); | ||||
|             this.throttledSearchItem(''); | ||||
|         }, | ||||
|         createNotebookStorageObject() { | ||||
| @@ -309,7 +309,7 @@ export default { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d); | ||||
|             return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier); | ||||
|         }, | ||||
|         getPage(section, id) { | ||||
|             return section.pages.find(p => p.id === id); | ||||
| @@ -379,9 +379,6 @@ export default { | ||||
|  | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         }, | ||||
|         mutateObject(key, value) { | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, key, value); | ||||
|         }, | ||||
|         navigateToSectionPage() { | ||||
|             const { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|             if (!pageId || !sectionId) { | ||||
| @@ -398,7 +395,7 @@ export default { | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|             this.sectionsChanged({ sections }); | ||||
|         }, | ||||
|         newEntry(embed = null) { | ||||
|             this.search = ''; | ||||
| @@ -411,6 +408,24 @@ export default { | ||||
|         orientationChange() { | ||||
|             this.formatSidebar(); | ||||
|         }, | ||||
|         pagesChanged({ pages = [], id = null}) { | ||||
|             const selectedSection = this.getSelectedSection(); | ||||
|             if (!selectedSection) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             selectedSection.pages = pages; | ||||
|             const sections = this.sections.map(section => { | ||||
|                 if (section.id === selectedSection.id) { | ||||
|                     section = selectedSection; | ||||
|                 } | ||||
|  | ||||
|                 return section; | ||||
|             }); | ||||
|  | ||||
|             this.sectionsChanged({ sections }); | ||||
|             this.updateDefaultNotebookPage(pages, id); | ||||
|         }, | ||||
|         removeDefaultClass(domainObject) { | ||||
|             if (!domainObject) { | ||||
|                 return; | ||||
| @@ -433,12 +448,14 @@ export default { | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } | ||||
|  | ||||
|             if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { | ||||
|             if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { | ||||
|                 this.defaultSectionId = notebookStorage.section.id; | ||||
|                 setDefaultNotebookSection(notebookStorage.section); | ||||
|             } | ||||
|  | ||||
|             if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) { | ||||
|             if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) { | ||||
|                 this.defaultPageId = notebookStorage.page.id; | ||||
|                 setDefaultNotebookPage(notebookStorage.page); | ||||
|             } | ||||
|         }, | ||||
|         updateDefaultNotebookPage(pages, id) { | ||||
| @@ -502,29 +519,11 @@ export default { | ||||
|             const notebookEntries = configuration.entries || {}; | ||||
|             notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries; | ||||
|  | ||||
|             this.mutateObject('configuration.entries', notebookEntries); | ||||
|             mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries); | ||||
|         }, | ||||
|         updateInternalDomainObject(domainObject) { | ||||
|             this.internalDomainObject = domainObject; | ||||
|         }, | ||||
|         updatePage({ pages = [], id = null}) { | ||||
|             const selectedSection = this.getSelectedSection(); | ||||
|             if (!selectedSection) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             selectedSection.pages = pages; | ||||
|             const sections = this.sections.map(section => { | ||||
|                 if (section.id === selectedSection.id) { | ||||
|                     section = selectedSection; | ||||
|                 } | ||||
|  | ||||
|                 return section; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|             this.updateDefaultNotebookPage(pages, id); | ||||
|         }, | ||||
|         updateParams(sections) { | ||||
|             const selectedSection = sections.find(s => s.isSelected); | ||||
|             if (!selectedSection) { | ||||
| @@ -548,8 +547,8 @@ export default { | ||||
|                 pageId | ||||
|             }); | ||||
|         }, | ||||
|         updateSection({ sections, id = null }) { | ||||
|             this.mutateObject('configuration.sections', sections); | ||||
|         sectionsChanged({ sections, id = null }) { | ||||
|             mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections); | ||||
|  | ||||
|             this.updateParams(sections); | ||||
|             this.updateDefaultNotebookSection(sections, id); | ||||
|   | ||||
| @@ -118,7 +118,7 @@ export default { | ||||
|             painterroInstance.show(this.embed.snapshot.src); | ||||
|         }, | ||||
|         changeLocation() { | ||||
|             const link = this.embed.historicLink; | ||||
|             const hash = this.embed.historicLink; | ||||
|  | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const isTimeBoundChanged = this.embed.bounds.start !== bounds.start | ||||
| @@ -143,8 +143,9 @@ export default { | ||||
|                 this.openmct.notifications.alert(message); | ||||
|             } | ||||
|  | ||||
|             const url = new URL(link); | ||||
|             window.location.href = url.hash; | ||||
|             const relativeHash = hash.slice(hash.indexOf('#')); | ||||
|             const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); | ||||
|             window.location.hash = url.hash; | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|  | ||||
| <script> | ||||
| import Snapshot from '../snapshot'; | ||||
| import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage'; | ||||
| import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants'; | ||||
|  | ||||
| export default { | ||||
| @@ -49,6 +49,8 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         validateNotebookStorageObject(); | ||||
|  | ||||
|         this.notebookSnapshot = new Snapshot(this.openmct); | ||||
|         this.setDefaultNotebookStatus(); | ||||
|     }, | ||||
| @@ -86,7 +88,7 @@ export default { | ||||
|  | ||||
|             this.openmct.menus.showMenu(x, y, notebookTypes); | ||||
|         }, | ||||
|         snapshot(notebook) { | ||||
|         snapshot(notebookType) { | ||||
|             this.$nextTick(() => { | ||||
|                 const element = document.querySelector('.c-overlay__contents') | ||||
|                     || document.getElementsByClassName('l-shell__main-container')[0]; | ||||
| @@ -104,7 +106,7 @@ export default { | ||||
|                     openmct: this.openmct | ||||
|                 }; | ||||
|  | ||||
|                 this.notebookSnapshot.capture(snapshotMeta, notebook.type, element); | ||||
|                 this.notebookSnapshot.capture(snapshotMeta, notebookType, element); | ||||
|             }); | ||||
|         }, | ||||
|         setDefaultNotebookStatus() { | ||||
|   | ||||
| @@ -66,14 +66,10 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         deletePage(id) { | ||||
|             const selectedSection = this.sections.find(s => s.isSelected); | ||||
|             const page = this.pages.filter(p => p.id !== id); | ||||
|             const page = this.pages.find(p => p.id !== id); | ||||
|             deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page); | ||||
|  | ||||
|             const selectedPage = this.pages.find(p => p.isSelected); | ||||
|   | ||||
| @@ -53,10 +53,6 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         deleteSection(id) { | ||||
|             const section = this.sections.find(s => s.id === id); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|                                :domain-object="domainObject" | ||||
|                                :sections="sections" | ||||
|                                :section-title="sectionTitle" | ||||
|                                @updateSection="updateSection" | ||||
|                                @updateSection="sectionsChanged" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -48,7 +48,7 @@ | ||||
|                             :sidebar-covers-entries="sidebarCoversEntries" | ||||
|                             :page-title="pageTitle" | ||||
|                             @toggleNav="toggleNav" | ||||
|                             @updatePage="updatePage" | ||||
|                             @updatePage="pagesChanged" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -85,13 +85,6 @@ export default { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         pages: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         pageTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
| @@ -122,9 +115,16 @@ export default { | ||||
|         return { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         pages() { | ||||
|             const selectedSection = this.sections.find(section => section.isSelected); | ||||
|  | ||||
|             return selectedSection && selectedSection.pages || []; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         pages(newpages) { | ||||
|             if (!newpages.length) { | ||||
|         pages(newPages) { | ||||
|             if (!newPages.length) { | ||||
|                 this.addPage(); | ||||
|             } | ||||
|         }, | ||||
| @@ -141,55 +141,79 @@ export default { | ||||
|     }, | ||||
|     methods: { | ||||
|         addPage() { | ||||
|             const newPage = this.createNewPage(); | ||||
|             const pages = this.addNewPage(newPage); | ||||
|  | ||||
|             this.pagesChanged({ | ||||
|                 pages, | ||||
|                 id: newPage.id | ||||
|             }); | ||||
|         }, | ||||
|         addSection() { | ||||
|             const newSection = this.createNewSection(); | ||||
|             const sections = this.addNewSection(newSection); | ||||
|  | ||||
|             this.sectionsChanged({ | ||||
|                 sections, | ||||
|                 id: newSection.id | ||||
|             }); | ||||
|         }, | ||||
|         addNewPage(page) { | ||||
|             const pages = this.pages.map(p => { | ||||
|                 p.isSelected = false; | ||||
|  | ||||
|                 return p; | ||||
|             }); | ||||
|  | ||||
|             return pages.concat(page); | ||||
|         }, | ||||
|         addNewSection(section) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
|  | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             return sections.concat(section); | ||||
|         }, | ||||
|         createNewPage() { | ||||
|             const pageTitle = this.pageTitle; | ||||
|             const id = uuid(); | ||||
|             const page = { | ||||
|  | ||||
|             return { | ||||
|                 id, | ||||
|                 isDefault: false, | ||||
|                 isSelected: true, | ||||
|                 name: `Unnamed ${pageTitle}`, | ||||
|                 pageTitle | ||||
|             }; | ||||
|  | ||||
|             this.pages.forEach(p => p.isSelected = false); | ||||
|             const pages = this.pages.concat(page); | ||||
|  | ||||
|             this.updatePage({ | ||||
|                 pages, | ||||
|                 id | ||||
|             }); | ||||
|         }, | ||||
|         addSection() { | ||||
|         createNewSection() { | ||||
|             const sectionTitle = this.sectionTitle; | ||||
|             const id = uuid(); | ||||
|             const section = { | ||||
|             const page = this.createNewPage(); | ||||
|             const pages = [page]; | ||||
|  | ||||
|             return { | ||||
|                 id, | ||||
|                 isDefault: false, | ||||
|                 isSelected: true, | ||||
|                 name: `Unnamed ${sectionTitle}`, | ||||
|                 pages: [], | ||||
|                 pages, | ||||
|                 sectionTitle | ||||
|             }; | ||||
|  | ||||
|             this.sections.forEach(s => s.isSelected = false); | ||||
|             const sections = this.sections.concat(section); | ||||
|  | ||||
|             this.updateSection({ | ||||
|                 sections, | ||||
|                 id | ||||
|             }); | ||||
|         }, | ||||
|         toggleNav() { | ||||
|             this.$emit('toggleNav'); | ||||
|         }, | ||||
|         updatePage({ pages, id }) { | ||||
|             this.$emit('updatePage', { | ||||
|         pagesChanged({ pages, id }) { | ||||
|             this.$emit('pagesChanged', { | ||||
|                 pages, | ||||
|                 id | ||||
|             }); | ||||
|         }, | ||||
|         updateSection({ sections, id }) { | ||||
|             this.$emit('updateSection', { | ||||
|         sectionsChanged({ sections, id }) { | ||||
|             this.$emit('sectionsChanged', { | ||||
|                 sections, | ||||
|                 id | ||||
|             }); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing'; | ||||
| import NotebookPlugin from './plugin'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| @@ -133,4 +133,89 @@ describe("Notebook plugin:", () => { | ||||
|             expect(hasMajorElements).toBe(true); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Notebook Snapshots view:", () => { | ||||
|         let snapshotIndicator; | ||||
|         let drawerElement; | ||||
|  | ||||
|         function clickSnapshotIndicator() { | ||||
|             const indicator = element.querySelector('.icon-camera'); | ||||
|             const button = indicator.querySelector('button'); | ||||
|             const clickEvent = createMouseEvent('click'); | ||||
|  | ||||
|             button.dispatchEvent(clickEvent); | ||||
|         } | ||||
|  | ||||
|         beforeAll(() => { | ||||
|             snapshotIndicator = openmct.indicators.indicatorObjects | ||||
|                 .find(indicator => indicator.key === 'notebook-snapshot-indicator').element; | ||||
|  | ||||
|             element.append(snapshotIndicator); | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         afterAll(() => { | ||||
|             snapshotIndicator.remove(); | ||||
|             snapshotIndicator = undefined; | ||||
|  | ||||
|             if (drawerElement) { | ||||
|                 drawerElement.remove(); | ||||
|                 drawerElement = undefined; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             drawerElement = document.querySelector('.l-shell__drawer'); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             if (drawerElement) { | ||||
|                 drawerElement.classList.remove('is-expanded'); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         it("has Snapshots indicator", () => { | ||||
|             const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined; | ||||
|             expect(hasSnapshotIndicator).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("snapshots container has class isExpanded", () => { | ||||
|             let classes = drawerElement.classList; | ||||
|             const isExpandedBefore = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterFirstClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             expect(isExpandedBefore).toBeFalse(); | ||||
|             expect(isExpandedAfterFirstClick).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it("snapshots container does not have class isExpanded", () => { | ||||
|             let classes = drawerElement.classList; | ||||
|             const isExpandedBefore = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterFirstClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             clickSnapshotIndicator(); | ||||
|             classes = drawerElement.classList; | ||||
|             const isExpandedAfterSecondClick = classes.contains('is-expanded'); | ||||
|  | ||||
|             expect(isExpandedBefore).toBeFalse(); | ||||
|             expect(isExpandedAfterFirstClick).toBeTrue(); | ||||
|             expect(isExpandedAfterSecondClick).toBeFalse(); | ||||
|         }); | ||||
|  | ||||
|         it("show notebook snapshots container text", () => { | ||||
|             clickSnapshotIndicator(); | ||||
|  | ||||
|             const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name'); | ||||
|             const snapshotsText = notebookSnapshots.textContent.trim(); | ||||
|  | ||||
|             expect(snapshotsText).toBe('Notebook Snapshots'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -7,15 +7,14 @@ export default class Snapshot { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.snapshotContainer = new SnapshotContainer(openmct); | ||||
|         this.exportImageService = openmct.$injector.get('exportImageService'); | ||||
|         this.dialogService = openmct.$injector.get('dialogService'); | ||||
|  | ||||
|         this.capture = this.capture.bind(this); | ||||
|         this._saveSnapShot = this._saveSnapShot.bind(this); | ||||
|     } | ||||
|  | ||||
|     capture(snapshotMeta, notebookType, domElement) { | ||||
|         this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot') | ||||
|         const exportImageService = this.openmct.$injector.get('exportImageService'); | ||||
|         exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot') | ||||
|             .then(function (blob) { | ||||
|                 const reader = new window.FileReader(); | ||||
|                 reader.readAsDataURL(blob); | ||||
|   | ||||
| @@ -8,6 +8,29 @@ const TIME_BOUNDS = { | ||||
|     END_DELTA: 'tc.endDelta' | ||||
| }; | ||||
|  | ||||
| export function addEntryIntoPage(notebookStorage, entries, entry) { | ||||
|     const defaultSection = notebookStorage.section; | ||||
|     const defaultPage = notebookStorage.page; | ||||
|     if (!defaultSection || !defaultPage) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const newEntries = JSON.parse(JSON.stringify(entries)); | ||||
|     let section = newEntries[defaultSection.id]; | ||||
|     if (!section) { | ||||
|         newEntries[defaultSection.id] = {}; | ||||
|     } | ||||
|  | ||||
|     let page = newEntries[defaultSection.id][defaultPage.id]; | ||||
|     if (!page) { | ||||
|         newEntries[defaultSection.id][defaultPage.id] = []; | ||||
|     } | ||||
|  | ||||
|     newEntries[defaultSection.id][defaultPage.id].push(entry); | ||||
|  | ||||
|     return newEntries; | ||||
| } | ||||
|  | ||||
| export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) { | ||||
|     if (historicLink.includes('tc.mode=fixed')) { | ||||
|         return historicLink; | ||||
| @@ -38,35 +61,6 @@ export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) { | ||||
|     return params.join('&'); | ||||
| } | ||||
|  | ||||
| export function getNotebookDefaultEntries(notebookStorage, domainObject) { | ||||
|     if (!notebookStorage || !domainObject) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     const defaultSection = notebookStorage.section; | ||||
|     const defaultPage = notebookStorage.page; | ||||
|     if (!defaultSection || !defaultPage) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     const configuration = domainObject.configuration; | ||||
|     const entries = configuration.entries || {}; | ||||
|  | ||||
|     let section = entries[defaultSection.id]; | ||||
|     if (!section) { | ||||
|         section = {}; | ||||
|         entries[defaultSection.id] = section; | ||||
|     } | ||||
|  | ||||
|     let page = entries[defaultSection.id][defaultPage.id]; | ||||
|     if (!page) { | ||||
|         page = []; | ||||
|         entries[defaultSection.id][defaultPage.id] = []; | ||||
|     } | ||||
|  | ||||
|     return entries[defaultSection.id][defaultPage.id]; | ||||
| } | ||||
|  | ||||
| export function createNewEmbed(snapshotMeta, snapshot = '') { | ||||
|     const { | ||||
|         bounds, | ||||
| @@ -120,24 +114,25 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = | ||||
|         ? [embed] | ||||
|         : []; | ||||
|  | ||||
|     const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject); | ||||
|     const id = `entry-${date}`; | ||||
|     defaultEntries.push({ | ||||
|     const entry = { | ||||
|         id, | ||||
|         createdOn: date, | ||||
|         text: entryText, | ||||
|         embeds | ||||
|     }); | ||||
|     }; | ||||
|  | ||||
|     const newEntries = addEntryIntoPage(notebookStorage, entries, entry); | ||||
|  | ||||
|     addDefaultClass(domainObject, openmct); | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', entries); | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', newEntries); | ||||
|  | ||||
|     return id; | ||||
| } | ||||
|  | ||||
| export function getNotebookEntries(domainObject, selectedSection, selectedPage) { | ||||
|     if (!domainObject || !selectedSection || !selectedPage) { | ||||
|         return null; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const configuration = domainObject.configuration; | ||||
| @@ -145,12 +140,12 @@ export function getNotebookEntries(domainObject, selectedSection, selectedPage) | ||||
|  | ||||
|     let section = entries[selectedSection.id]; | ||||
|     if (!section) { | ||||
|         return null; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let page = entries[selectedSection.id][selectedPage.id]; | ||||
|     if (!page) { | ||||
|         return null; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     return entries[selectedSection.id][selectedPage.id]; | ||||
| @@ -196,7 +191,11 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se | ||||
|  | ||||
|     delete entries[selectedSection.id][selectedPage.id]; | ||||
|  | ||||
|     openmct.objects.mutate(domainObject, 'configuration.entries', entries); | ||||
|     mutateObject(openmct, domainObject, 'configuration.entries', entries); | ||||
| } | ||||
|  | ||||
| export function mutateObject(openmct, object, key, value) { | ||||
|     openmct.objects.mutate(object, key, value); | ||||
| } | ||||
|  | ||||
| function addDefaultClass(domainObject, openmct) { | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import * as NotebookEntries from './notebook-entries'; | ||||
| import { createOpenMct, spyOnBuiltins, resetApplicationState } from 'utils/testing'; | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
|  | ||||
| const notebookStorage = { | ||||
|     domainObject: { | ||||
| @@ -121,7 +121,6 @@ describe('Notebook Entries:', () => { | ||||
|     beforeEach(done => { | ||||
|         openmct = createOpenMct(); | ||||
|         window.localStorage.setItem('notebook-storage', null); | ||||
|         spyOnBuiltins(['mutate'], openmct.objects); | ||||
|  | ||||
|         done(); | ||||
|     }); | ||||
| @@ -137,24 +136,16 @@ describe('Notebook Entries:', () => { | ||||
|         expect(entries.length).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     it('addNotebookEntry mutates object', () => { | ||||
|     it('addNotebookEntry adds entry', (done) => { | ||||
|         const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => { | ||||
|             const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); | ||||
|  | ||||
|             expect(entries.length).toEqual(1); | ||||
|             done(); | ||||
|             unlisten(); | ||||
|         }); | ||||
|  | ||||
|         NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|  | ||||
|         expect(openmct.objects.mutate).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('addNotebookEntry adds entry', () => { | ||||
|         NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|         const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); | ||||
|  | ||||
|         expect(entries.length).toEqual(1); | ||||
|     }); | ||||
|  | ||||
|     it('getEntryPosById returns valid position', () => { | ||||
|         const entryId = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|         const position = NotebookEntries.getEntryPosById(entryId, notebookDomainObject, selectedSection, selectedPage); | ||||
|  | ||||
|         expect(position).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     it('getEntryPosById returns valid position', () => { | ||||
| @@ -174,22 +165,13 @@ describe('Notebook Entries:', () => { | ||||
|         expect(success).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('deleteNotebookEntries mutates object', () => { | ||||
|         openmct.objects.mutate.calls.reset(); | ||||
|  | ||||
|         NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|         NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage); | ||||
|  | ||||
|         expect(openmct.objects.mutate).toHaveBeenCalledTimes(2); | ||||
|     }); | ||||
|  | ||||
|     it('deleteNotebookEntries deletes correct entry', () => { | ||||
|     it('deleteNotebookEntries deletes correct page entries', () => { | ||||
|         NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|         NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage); | ||||
|  | ||||
|         NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage); | ||||
|         const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage); | ||||
|  | ||||
|         expect(afterEntries).toEqual(null); | ||||
|         expect(afterEntries).toEqual(undefined); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -67,3 +67,24 @@ export function setDefaultNotebookPage(page) { | ||||
|     notebookStorage.page = page; | ||||
|     saveDefaultNotebook(notebookStorage); | ||||
| } | ||||
|  | ||||
| export function validateNotebookStorageObject() { | ||||
|     const notebookStorage = getDefaultNotebook(); | ||||
|  | ||||
|     let valid = false; | ||||
|     if (notebookStorage) { | ||||
|         Object.entries(notebookStorage).forEach(([key, value]) => { | ||||
|             const validKey = key !== undefined && key !== null; | ||||
|             const validValue = value !== undefined && value !== null; | ||||
|             valid = validKey && validValue; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     if (valid) { | ||||
|         return notebookStorage; | ||||
|     } | ||||
|  | ||||
|     console.warn('Invalid Notebook object, clearing default notebook storage'); | ||||
|  | ||||
|     clearDefaultNotebook(); | ||||
| } | ||||
|   | ||||
| @@ -87,7 +87,8 @@ export default class CouchObjectProvider { | ||||
|             } | ||||
|  | ||||
|             //Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress | ||||
|             if (!this.objectQueue[key].pending) { | ||||
|             //Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates. | ||||
|             if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) { | ||||
|                 this.objectQueue[key].updateRevision(response[REV]); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -171,6 +171,7 @@ define([ | ||||
|          * Update yAxis format, values, and label from known series. | ||||
|          */ | ||||
|         updateFromSeries: function (series) { | ||||
|             this.unset('displayRange'); | ||||
|             const plotModel = this.plot.get('domainObject'); | ||||
|             const label = _.get(plotModel, 'configuration.yAxis.label'); | ||||
|             const sampleSeries = series.first(); | ||||
|   | ||||
| @@ -117,8 +117,10 @@ define( | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportJPG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, "jpg", className).then(function (img) { | ||||
|                 saveAs(img, filename); | ||||
|                 saveAs(img, processedFilename); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
| @@ -130,8 +132,10 @@ define( | ||||
|          * @returns {promise} | ||||
|          */ | ||||
|         ExportImageService.prototype.exportPNG = function (element, filename, className) { | ||||
|             const processedFilename = replaceDotsWithUnderscores(filename); | ||||
|  | ||||
|             return this.renderElement(element, "png", className).then(function (img) { | ||||
|                 saveAs(img, filename); | ||||
|                 saveAs(img, processedFilename); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
| @@ -146,6 +150,12 @@ define( | ||||
|             return this.renderElement(element, "png", className); | ||||
|         }; | ||||
|  | ||||
|         function replaceDotsWithUnderscores(filename) { | ||||
|             const regex = /\./gi; | ||||
|  | ||||
|             return filename.replace(regex, '_'); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill | ||||
|          * implements the method in browsers that would not otherwise support it. | ||||
|   | ||||
| @@ -39,10 +39,11 @@ define([], function () { | ||||
|             const thisRequest = { | ||||
|                 pending: 0 | ||||
|             }; | ||||
|             currentRequest = thisRequest; | ||||
|             $scope.currentRequest = thisRequest; | ||||
|             const telemetryObjects = $scope.telemetryObjects = []; | ||||
|             const thisTickWidthMap = {}; | ||||
|  | ||||
|             currentRequest = thisRequest; | ||||
|             $scope.currentRequest = thisRequest; | ||||
|             tickWidthMap = thisTickWidthMap; | ||||
|  | ||||
|             if (unlisten) { | ||||
| @@ -52,14 +53,10 @@ define([], function () { | ||||
|  | ||||
|             function addChild(child) { | ||||
|                 const id = openmct.objects.makeKeyString(child.identifier); | ||||
|                 const legacyObject = openmct.legacyObject(child); | ||||
|  | ||||
|                 thisTickWidthMap[id] = 0; | ||||
|                 thisRequest.pending += 1; | ||||
|                 objectService.getObjects([id]) | ||||
|                     .then(function (objects) { | ||||
|                         thisRequest.pending -= 1; | ||||
|                         const childObj = objects[id]; | ||||
|                         telemetryObjects.push(childObj); | ||||
|                     }); | ||||
|                 telemetryObjects.push(legacyObject); | ||||
|             } | ||||
|  | ||||
|             function removeChild(childIdentifier) { | ||||
| @@ -84,6 +81,7 @@ define([], function () { | ||||
|             } | ||||
|  | ||||
|             thisRequest.pending += 1; | ||||
|  | ||||
|             openmct.objects.get(domainObject.getId()) | ||||
|                 .then(function (obj) { | ||||
|                     thisRequest.pending -= 1; | ||||
|   | ||||
| @@ -59,7 +59,8 @@ define([ | ||||
|     './persistence/couch/plugin', | ||||
|     './defaultRootName/plugin', | ||||
|     './timeline/plugin', | ||||
|     './viewDatumAction/plugin' | ||||
|     './viewDatumAction/plugin', | ||||
|     './interceptors/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
|     UTCTimeSystem, | ||||
| @@ -99,7 +100,8 @@ define([ | ||||
|     CouchDBPlugin, | ||||
|     DefaultRootName, | ||||
|     Timeline, | ||||
|     ViewDatumAction | ||||
|     ViewDatumAction, | ||||
|     ObjectInterceptors | ||||
| ) { | ||||
|     const bundleMap = { | ||||
|         LocalStorage: 'platform/persistence/local', | ||||
| @@ -194,6 +196,7 @@ define([ | ||||
|     plugins.DefaultRootName = DefaultRootName.default; | ||||
|     plugins.Timeline = Timeline.default; | ||||
|     plugins.ViewDatumAction = ViewDatumAction.default; | ||||
|     plugins.ObjectInterceptors = ObjectInterceptors.default; | ||||
|  | ||||
|     return plugins; | ||||
| }); | ||||
|   | ||||
| @@ -20,8 +20,8 @@ | ||||
|             Drag objects here to add them to this view. | ||||
|         </div> | ||||
|         <div | ||||
|             v-for="(tab,index) in tabsList" | ||||
|             :key="index" | ||||
|             v-for="(tab, index) in tabsList" | ||||
|             :key="tab.keyString" | ||||
|             class="c-tab c-tabs-view__tab" | ||||
|             :class="{ | ||||
|                 'is-current': isCurrent(tab) | ||||
| @@ -29,13 +29,13 @@ | ||||
|             @click="showTab(tab, index)" | ||||
|         > | ||||
|             <div class="c-tabs-view__tab__label c-object-label" | ||||
|                  :class="[tab.status ? `is-${tab.status}` : '']" | ||||
|                  :class="[tab.status ? `is-status--${tab.status}` : '']" | ||||
|             > | ||||
|                 <div class="c-object-label__type-icon" | ||||
|                      :class="tab.type.definition.cssClass" | ||||
|                 > | ||||
|                     <span class="is-status__indicator" | ||||
|                           title="This item is missing or suspect" | ||||
|                           :title="`This item is ${tab.status}`" | ||||
|                     ></span> | ||||
|                 </div> | ||||
|                 <span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span> | ||||
| @@ -47,8 +47,8 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div | ||||
|         v-for="(tab, index) in tabsList" | ||||
|         :key="index" | ||||
|         v-for="tab in tabsList" | ||||
|         :key="tab.keyString" | ||||
|         class="c-tabs-view__object-holder" | ||||
|         :class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}" | ||||
|     > | ||||
| @@ -56,6 +56,7 @@ | ||||
|             v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)" | ||||
|             class="c-tabs-view__object" | ||||
|             :object="tab.domainObject" | ||||
|             :object-path="tab.objectPath" | ||||
|         /> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -78,7 +79,7 @@ const unknownObjectType = { | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'composition'], | ||||
|     inject: ['openmct', 'domainObject', 'composition', 'objectPath'], | ||||
|     components: { | ||||
|         ObjectView | ||||
|     }, | ||||
| @@ -139,6 +140,10 @@ export default { | ||||
|         this.composition.off('remove', this.removeItem); | ||||
|         this.composition.off('reorder', this.onReorder); | ||||
|  | ||||
|         this.tabsList.forEach(tab => { | ||||
|             tab.statusUnsubscribe(); | ||||
|         }); | ||||
|  | ||||
|         this.unsubscribe(); | ||||
|         this.clearCurrentTabIndexFromURL(); | ||||
|  | ||||
| @@ -192,12 +197,19 @@ export default { | ||||
|         }, | ||||
|         addItem(domainObject) { | ||||
|             let type = this.openmct.types.get(domainObject.type) || unknownObjectType; | ||||
|             let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             let status = this.openmct.status.get(domainObject.identifier); | ||||
|             let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => { | ||||
|                 this.updateStatus(keyString, updatedStatus); | ||||
|             }); | ||||
|             let objectPath = [domainObject].concat(this.objectPath.slice()); | ||||
|             let tabItem = { | ||||
|                 domainObject, | ||||
|                 status, | ||||
|                 type: type, | ||||
|                 key: this.openmct.objects.makeKeyString(domainObject.identifier) | ||||
|                 statusUnsubscribe, | ||||
|                 objectPath, | ||||
|                 type, | ||||
|                 keyString | ||||
|             }; | ||||
|  | ||||
|             this.tabsList.push(tabItem); | ||||
| @@ -213,10 +225,12 @@ export default { | ||||
|         }, | ||||
|         removeItem(identifier) { | ||||
|             let pos = this.tabsList.findIndex(tab => | ||||
|                 tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.key === identifier.key | ||||
|                 tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.keyString === identifier.keyString | ||||
|             ); | ||||
|             let tabToBeRemoved = this.tabsList[pos]; | ||||
|  | ||||
|             tabToBeRemoved.statusUnsubscribe(); | ||||
|  | ||||
|             this.tabsList.splice(pos, 1); | ||||
|  | ||||
|             if (this.isCurrent(tabToBeRemoved)) { | ||||
| @@ -254,7 +268,7 @@ export default { | ||||
|             this.allowDrop = false; | ||||
|         }, | ||||
|         isCurrent(tab) { | ||||
|             return this.currentTab.key === tab.key; | ||||
|             return this.currentTab.keyString === tab.keyString; | ||||
|         }, | ||||
|         updateInternalDomainObject(domainObject) { | ||||
|             this.internalDomainObject = domainObject; | ||||
| @@ -272,6 +286,16 @@ export default { | ||||
|         }, | ||||
|         clearCurrentTabIndexFromURL() { | ||||
|             deleteSearchParam(this.searchTabKey); | ||||
|         }, | ||||
|         updateStatus(keyString, status) { | ||||
|             let tabPos = this.tabsList.findIndex((tab) => { | ||||
|                 return tab.keyString === keyString; | ||||
|             }); | ||||
|  | ||||
|             if (tabPos !== -1) { | ||||
|                 let tab = this.tabsList[tabPos]; | ||||
|                 this.$set(tab, 'status', status); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ define([ | ||||
|             canEdit: function (domainObject) { | ||||
|                 return domainObject.type === 'tabs'; | ||||
|             }, | ||||
|             view: function (domainObject) { | ||||
|             view: function (domainObject, objectPath) { | ||||
|                 let component; | ||||
|  | ||||
|                 return { | ||||
| @@ -56,6 +56,7 @@ define([ | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 domainObject, | ||||
|                                 objectPath, | ||||
|                                 composition: openmct.composition.get(domainObject) | ||||
|                             }, | ||||
|                             template: '<tabs-component :isEditing="isEditing"></tabs-component>' | ||||
|   | ||||
| @@ -30,13 +30,13 @@ let exportCSV = { | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| let exportMarkedRows = { | ||||
| let exportMarkedDataAsCSV = { | ||||
|     name: 'Export Marked Rows', | ||||
|     key: 'export-csv-marked', | ||||
|     description: "Export marked rows as CSV", | ||||
|     cssClass: 'icon-download labeled', | ||||
|     invoke: (objectPath, viewProvider) => { | ||||
|         viewProvider.getViewContext().exportMarkedRows(); | ||||
|         viewProvider.getViewContext().exportMarkedDataAsCSV(); | ||||
|     }, | ||||
|     group: 'view' | ||||
| }; | ||||
| @@ -98,7 +98,7 @@ let autosizeColumns = { | ||||
|  | ||||
| let viewActions = [ | ||||
|     exportCSV, | ||||
|     exportMarkedRows, | ||||
|     exportMarkedDataAsCSV, | ||||
|     unmarkAllRows, | ||||
|     pause, | ||||
|     play, | ||||
|   | ||||
| @@ -108,8 +108,7 @@ export default { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: this.getDatum, | ||||
|                         skipCache: true | ||||
|                         getDatum: this.getDatum | ||||
|                     }; | ||||
|                 } | ||||
|             } | ||||
| @@ -192,7 +191,8 @@ export default { | ||||
|                 let contextualObjectPath = this.objectPath.slice(); | ||||
|                 contextualObjectPath.unshift(domainObject); | ||||
|  | ||||
|                 let allActions = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext); | ||||
|                 let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext); | ||||
|                 let allActions = actionsCollection.getActionsObject(); | ||||
|                 let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]); | ||||
|  | ||||
|                 if (applicableActions.length) { | ||||
|   | ||||
| @@ -960,7 +960,7 @@ export default { | ||||
|             return { | ||||
|                 type: 'telemetry-table', | ||||
|                 exportAllDataAsCSV: this.exportAllDataAsCSV, | ||||
|                 exportMarkedRows: this.exportMarkedRows, | ||||
|                 exportMarkedDataAsCSV: this.exportMarkedDataAsCSV, | ||||
|                 unmarkAllRows: this.unmarkAllRows, | ||||
|                 togglePauseByButton: this.togglePauseByButton, | ||||
|                 expandColumns: this.recalculateColumnWidths, | ||||
|   | ||||
| @@ -61,13 +61,21 @@ export default { | ||||
|         this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities); | ||||
|         this.openmct.time.on("bounds", this.updateViewBounds); | ||||
|         this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL); | ||||
|         this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         clearInterval(this.resizeTimer); | ||||
|         this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities); | ||||
|         this.openmct.time.off("bounds", this.updateViewBounds); | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         observeForChanges(mutatedObject) { | ||||
|             this.validateJSON(mutatedObject.selectFile.body); | ||||
|             this.setScaleAndPlotActivities(); | ||||
|         }, | ||||
|         resize() { | ||||
|             if (this.$refs.axisHolder.clientWidth !== this.width) { | ||||
|                 this.setDimensions(); | ||||
| @@ -200,7 +208,7 @@ export default { | ||||
|             return 0; | ||||
|         }, | ||||
|         // Get the row where the next activity will land. | ||||
|         getRowForActivity(rectX, width, defaultActivityRow = 0) { | ||||
|         getRowForActivity(rectX, width, minimumActivityRow = 0) { | ||||
|             let currentRow; | ||||
|             let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn); | ||||
|  | ||||
| @@ -216,17 +224,18 @@ export default { | ||||
|  | ||||
|             for (let i = 0; i < sortedActivityRows.length; i++) { | ||||
|                 let row = sortedActivityRows[i]; | ||||
|                 if (getOverlap(this.activitiesByRow[row])) { | ||||
|                 if (row >= minimumActivityRow && getOverlap(this.activitiesByRow[row])) { | ||||
|                     currentRow = row; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (currentRow === undefined && sortedActivityRows.length) { | ||||
|                 currentRow = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + ROW_HEIGHT + ROW_PADDING; | ||||
|                 let row = Math.max(parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10), minimumActivityRow); | ||||
|                 currentRow = row + ROW_HEIGHT + ROW_PADDING; | ||||
|             } | ||||
|  | ||||
|             return (currentRow || defaultActivityRow); | ||||
|             return (currentRow || minimumActivityRow); | ||||
|         }, | ||||
|         calculatePlanLayout() { | ||||
|             this.activitiesByRow = {}; | ||||
| @@ -236,8 +245,9 @@ export default { | ||||
|             let groups = Object.keys(this.json); | ||||
|             groups.forEach((key, index) => { | ||||
|                 let activities = this.json[key]; | ||||
|                 //set the currentRow to the beginning of the next logical row | ||||
|                 currentRow = currentRow + ROW_HEIGHT * index; | ||||
|                 //set the new group's first row. It should be greater than the largest row of the last group | ||||
|                 let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn); | ||||
|                 const groupRowStart = sortedActivityRows.length ? parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + 1 : 0; | ||||
|                 let newGroup = true; | ||||
|                 activities.forEach((activity) => { | ||||
|                     if (this.isActivityInBounds(activity)) { | ||||
| @@ -256,9 +266,9 @@ export default { | ||||
|                         const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING; | ||||
|  | ||||
|                         if (activityNameFitsRect) { | ||||
|                             currentRow = this.getRowForActivity(rectX, rectWidth); | ||||
|                             currentRow = this.getRowForActivity(rectX, rectWidth, groupRowStart); | ||||
|                         } else { | ||||
|                             currentRow = this.getRowForActivity(rectX, textWidth); | ||||
|                             currentRow = this.getRowForActivity(rectX, textWidth, groupRowStart); | ||||
|                         } | ||||
|  | ||||
|                         let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING); | ||||
|   | ||||
| @@ -98,6 +98,10 @@ describe('the plugin', function () { | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             planDomainObject = { | ||||
|                 identifier: { | ||||
|                     key: 'test-object', | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 type: 'plan', | ||||
|                 id: "test-object", | ||||
|                 selectFile: { | ||||
|   | ||||
| @@ -1,48 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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(['./UTCTimeSystem'], function (UTCTimeSystem) { | ||||
|     describe("The UTCTimeSystem class", function () { | ||||
|         let timeSystem; | ||||
|         let mockTimeout; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockTimeout = jasmine.createSpy("timeout"); | ||||
|             timeSystem = new UTCTimeSystem(mockTimeout); | ||||
|         }); | ||||
|  | ||||
|         it("Uses the UTC time format", function () { | ||||
|             expect(timeSystem.timeFormat).toBe('utc'); | ||||
|         }); | ||||
|  | ||||
|         it("is UTC based", function () { | ||||
|             expect(timeSystem.isUTCBased).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("defines expected metadata", function () { | ||||
|             expect(timeSystem.key).toBeDefined(); | ||||
|             expect(timeSystem.name).toBeDefined(); | ||||
|             expect(timeSystem.cssClass).toBeDefined(); | ||||
|             expect(timeSystem.durationFormat).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										103
									
								
								src/plugins/utcTimeSystem/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/plugins/utcTimeSystem/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 LocalClock from './LocalClock.js'; | ||||
| import UTCTimeSystem from './UTCTimeSystem'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The UTC Time System", () => { | ||||
|     const UTC_SYSTEM_AND_FORMAT_KEY = 'utc'; | ||||
|     let openmct; | ||||
|     let utcTimeSystem; | ||||
|     let mockTimeout; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("plugin", function () { | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockTimeout = jasmine.createSpy("timeout"); | ||||
|             utcTimeSystem = new UTCTimeSystem(mockTimeout); | ||||
|         }); | ||||
|  | ||||
|         it("is installed", () => { | ||||
|             let timeSystems = openmct.time.getAllTimeSystems(); | ||||
|             let utc = timeSystems.find(ts => ts.key === UTC_SYSTEM_AND_FORMAT_KEY); | ||||
|  | ||||
|             expect(utc).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("can be set to be the main time system", () => { | ||||
|             openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|             }); | ||||
|  | ||||
|             expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); | ||||
|         }); | ||||
|  | ||||
|         it("uses the utc time format", () => { | ||||
|             expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY); | ||||
|         }); | ||||
|  | ||||
|         it("is UTC based", () => { | ||||
|             expect(utcTimeSystem.isUTCBased).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         it("defines expected metadata", () => { | ||||
|             expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); | ||||
|             expect(utcTimeSystem.name).toBeDefined(); | ||||
|             expect(utcTimeSystem.cssClass).toBeDefined(); | ||||
|             expect(utcTimeSystem.durationFormat).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("LocalClock class", function () { | ||||
|         let clock; | ||||
|         const timeoutHandle = {}; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             mockTimeout = jasmine.createSpy("timeout"); | ||||
|             mockTimeout.and.returnValue(timeoutHandle); | ||||
|  | ||||
|             clock = new LocalClock(0); | ||||
|             clock.start(); | ||||
|         }); | ||||
|  | ||||
|         it("calls listeners on tick with current time", function () { | ||||
|             const mockListener = jasmine.createSpy("listener"); | ||||
|             clock.on('tick', mockListener); | ||||
|             clock.tick(); | ||||
|             expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number)); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -56,7 +56,7 @@ export default class ViewDatumAction { | ||||
|         }); | ||||
|     } | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext() || {}; | ||||
|         let viewContext = (view.getViewContext && view.getViewContext()) || {}; | ||||
|         let datum = viewContext.getDatum; | ||||
|         let enabled = viewContext.viewDatumAction; | ||||
|  | ||||
|   | ||||
| @@ -70,11 +70,8 @@ define( | ||||
|             if (multiSelect) { | ||||
|                 this.handleMultiSelect(selectable); | ||||
|             } else { | ||||
|                 this.setSelectionStyles(selectable); | ||||
|                 this.selected = [selectable]; | ||||
|                 this.handleSingleSelect(selectable); | ||||
|             } | ||||
|  | ||||
|             this.emit('change', this.selected); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -87,6 +84,20 @@ define( | ||||
|                 this.addSelectionAttributes(selectable); | ||||
|                 this.selected.push(selectable); | ||||
|             } | ||||
|  | ||||
|             this.emit('change', this.selected); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * @private | ||||
|          */ | ||||
|         Selection.prototype.handleSingleSelect = function (selectable) { | ||||
|             if (!_.isEqual([selectable], this.selected)) { | ||||
|                 this.setSelectionStyles(selectable); | ||||
|                 this.selected = [selectable]; | ||||
|  | ||||
|                 this.emit('change', this.selected); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -126,7 +126,7 @@ button { | ||||
|  | ||||
| .c-icon-button { | ||||
|     [class*='label'] { | ||||
|         opacity: 0.6; | ||||
|         opacity: 0.8; | ||||
|     } | ||||
|  | ||||
|     &--mixed { | ||||
| @@ -468,9 +468,6 @@ select { | ||||
|  | ||||
|     > * { | ||||
|         flex: 0 0 auto; | ||||
|         //+ * { | ||||
|         //    margin-top: $interiorMarginSm; | ||||
|         //} | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -485,7 +482,7 @@ select { | ||||
|         transition: $transIn; | ||||
|         white-space: nowrap; | ||||
|  | ||||
|         &:hover { | ||||
|         @include hover { | ||||
|             background: $colorMenuHovBg; | ||||
|             color: $colorMenuHovFg; | ||||
|             &:before { | ||||
| @@ -500,8 +497,12 @@ select { | ||||
|             min-width: 1em; | ||||
|         } | ||||
|  | ||||
|         &:not([class]):before { | ||||
|              content: ''; // Add this element so that menu items without an icon still indent properly | ||||
|         &:not([class*='icon']):before { | ||||
|              content: ''; // Enable :before so that menu items without an icon still indent properly | ||||
|         } | ||||
|  | ||||
|         .menus-no-icon & { | ||||
|             &:before { display: none; } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -734,7 +735,6 @@ select { | ||||
| .c-toolbar { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
|  | ||||
|     > * { | ||||
|         // First level items | ||||
|   | ||||
| @@ -47,6 +47,7 @@ mct-plot { | ||||
| .c-plot, | ||||
| .gl-plot { | ||||
|     overflow: hidden; | ||||
|     min-height: 100px; | ||||
|  | ||||
|         .s-status-taking-snapshot & { | ||||
|         .c-control-bar { | ||||
| @@ -60,13 +61,7 @@ mct-plot { | ||||
|  | ||||
|     /*********************** MISSING ITEM INDICATORS */ | ||||
|     .is-status__indicator { | ||||
|         display: none; | ||||
|     } | ||||
|     .is-status--missing { | ||||
|         @include isMissing(); | ||||
|         .is-status__indicator { | ||||
|             font-size: 0.8em; | ||||
|         } | ||||
|         font-size: 0.8em; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -85,7 +80,8 @@ mct-plot { | ||||
|         display: flex; | ||||
|         flex: 1 1 auto; | ||||
|         flex-direction: column; | ||||
|         overflow: hidden; | ||||
|         overflow-y: auto; | ||||
|         overflow-x: hidden; | ||||
|     } | ||||
|  | ||||
|     &--stacked { | ||||
|   | ||||
| @@ -129,44 +129,25 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| @mixin isStatus($absPos: false) { | ||||
| @mixin isStatus($absPos: false, $glyph: '', $color: $colorBodyFg) { | ||||
|     // Supports CSS classing as follows: | ||||
|     // is-status--missing, is-status--suspect, etc. | ||||
|     // Common styles to be applied to tree items, object labels, grid and list item views | ||||
|  | ||||
|     .is-status__indicator { | ||||
|         display: none ; | ||||
|         display: block ; // Set to display: none in status.scss | ||||
|         text-shadow: $colorBodyBg 0 0 2px; | ||||
|         font-family: symbolsfont; | ||||
|  | ||||
|         &[class^='is-status'] .is-status__indicator, | ||||
|         [class^='is-status'] .is-status__indicator { | ||||
|             display: block !important; | ||||
|         } | ||||
|  | ||||
|         @if $absPos { | ||||
|             display: block; | ||||
|             position: absolute; | ||||
|             z-index: 3; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @mixin isMissing($absPos: false) { | ||||
|     @include isStatus($absPos); | ||||
|  | ||||
|     .is-status__indicator:before { | ||||
|         color: $colorAlert; | ||||
|         content: $glyph-icon-alert-triangle; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @mixin isSuspect($absPos: false) { | ||||
|     @include isStatus($absPos); | ||||
|  | ||||
|     .is-status__indicator:before { | ||||
|         color: $colorWarningLo; | ||||
|         content: $glyph-icon-alert-rect; | ||||
|         &:before { | ||||
|             color: $color; | ||||
|             content: $glyph; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -571,7 +552,8 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='--menus-left'] { | ||||
|     &[class*='--menus-left'], | ||||
|     &[class*='menus-to-left'] { | ||||
|         .c-menu { | ||||
|             left: auto; right: 0; | ||||
|         } | ||||
|   | ||||
| @@ -198,3 +198,17 @@ tr { | ||||
|  | ||||
| .u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); } | ||||
| .u-error { @include uIndicator($colorError, $colorErrorFg, $glyph-icon-alert-triangle); } | ||||
|  | ||||
| .is-status { | ||||
|     &__indicator { | ||||
|         display: none; // Default state; is set to block when within an actual is-status class | ||||
|     } | ||||
|  | ||||
|     &--missing { | ||||
|         @include isStatus($glyph: $glyph-icon-alert-triangle, $color: $colorAlert); | ||||
|     } | ||||
|  | ||||
|     &--suspect { | ||||
|         @include isStatus($glyph: $glyph-icon-alert-rect, $color: $colorWarningLo); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -213,17 +213,16 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .is-notebook-default { | ||||
| .is-notebook-default, | ||||
| .is-status--notebook-default { | ||||
|     &:after { | ||||
|         color: $colorFilter; | ||||
|         content: $glyph-icon-notebook-page; | ||||
|         display: block; | ||||
|         font-family: symbolsfont; | ||||
|         font-size: 0.9em; | ||||
|         margin-left: $interiorMargin; | ||||
|     } | ||||
|  | ||||
|     &.c-list__item:after { | ||||
|         content: $glyph-icon-notebook-page; | ||||
|         flex: 1 0 auto; | ||||
|         text-align: right; | ||||
|     } | ||||
|   | ||||
| @@ -23,11 +23,11 @@ | ||||
| <div | ||||
|     class="c-so-view" | ||||
|     :class="[ | ||||
|         statusClass, | ||||
|         'c-so-view--' + domainObject.type, | ||||
|         { | ||||
|             'c-so-view--no-frame': !hasFrame, | ||||
|             'has-complex-content': complexContent, | ||||
|             'is-missing': domainObject.status === 'missing' | ||||
|             'has-complex-content': complexContent | ||||
|         } | ||||
|     ]" | ||||
| > | ||||
| @@ -85,9 +85,6 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="is-status__indicator" | ||||
|          title="This item is missing or suspect" | ||||
|     ></div> | ||||
|     <object-view | ||||
|         ref="objectView" | ||||
|         class="c-so-view__object-view" | ||||
| @@ -96,7 +93,7 @@ | ||||
|         :object-path="objectPath" | ||||
|         :layout-font-size="layoutFontSize" | ||||
|         :layout-font="layoutFont" | ||||
|         @change-provider="setViewProvider" | ||||
|         @change-action-collection="setActionCollection" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
| @@ -149,15 +146,10 @@ export default { | ||||
|  | ||||
|         let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type); | ||||
|  | ||||
|         let viewProvider = {}; | ||||
|  | ||||
|         let statusBarItems = {}; | ||||
|  | ||||
|         return { | ||||
|             cssClass, | ||||
|             complexContent, | ||||
|             viewProvider, | ||||
|             statusBarItems, | ||||
|             statusBarItems: [], | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
| @@ -204,6 +196,7 @@ export default { | ||||
|         }, | ||||
|         getPreviewHeader() { | ||||
|             const domainObject = this.objectPath[0]; | ||||
|             const actionCollection = this.actionCollection; | ||||
|             const preview = new Vue({ | ||||
|                 components: { | ||||
|                     PreviewHeader | ||||
| @@ -214,10 +207,11 @@ export default { | ||||
|                 }, | ||||
|                 data() { | ||||
|                     return { | ||||
|                         domainObject | ||||
|                         domainObject, | ||||
|                         actionCollection | ||||
|                     }; | ||||
|                 }, | ||||
|                 template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>' | ||||
|                 template: '<PreviewHeader :actionCollection="actionCollection" :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>' | ||||
|             }); | ||||
|  | ||||
|             return preview.$mount().$el; | ||||
| @@ -225,27 +219,17 @@ export default { | ||||
|         getSelectionContext() { | ||||
|             return this.$refs.objectView.getSelectionContext(); | ||||
|         }, | ||||
|         setViewProvider(provider) { | ||||
|             this.viewProvider = provider; | ||||
|             this.initializeStatusBarItems(); | ||||
|         }, | ||||
|         initializeStatusBarItems() { | ||||
|         setActionCollection(actionCollection) { | ||||
|             if (this.actionCollection) { | ||||
|                 this.unlistenToActionCollection(); | ||||
|             } | ||||
|  | ||||
|             if (this.viewProvider) { | ||||
|                 this.actionCollection = this.openmct.actions.get(this.objectPath, this.viewProvider); | ||||
|                 this.actionCollection.on('update', this.updateActionItems); | ||||
|                 this.updateActionItems(this.actionCollection.applicableActions); | ||||
|             } else { | ||||
|                 this.statusBarItems = []; | ||||
|                 this.menuActionItems = []; | ||||
|             } | ||||
|             this.actionCollection = actionCollection; | ||||
|             this.actionCollection.on('update', this.updateActionItems); | ||||
|             this.updateActionItems(this.actionCollection.applicableActions); | ||||
|         }, | ||||
|         unlistenToActionCollection() { | ||||
|             this.actionCollection.off('update', this.updateActionItems); | ||||
|             this.actionCollection.destroy(); | ||||
|             delete this.actionCollection; | ||||
|         }, | ||||
|         updateActionItems(actionItems) { | ||||
| @@ -253,15 +237,7 @@ export default { | ||||
|             this.menuActionItems = this.actionCollection.getVisibleActions(); | ||||
|         }, | ||||
|         showMenuItems(event) { | ||||
|             let actions; | ||||
|  | ||||
|             if (this.menuActionItems.length) { | ||||
|                 actions = this.menuActionItems; | ||||
|             } else { | ||||
|                 actions = this.openmct.actions.get(this.objectPath); | ||||
|             } | ||||
|  | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(actions); | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, sortedActions); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|   | ||||
| @@ -42,6 +42,15 @@ export default { | ||||
|         navigateToPath: { | ||||
|             type: String, | ||||
|             default: undefined | ||||
|         }, | ||||
|         liteObject: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         beforeInteraction: { | ||||
|             type: Function, | ||||
|             required: false, | ||||
|             default: undefined | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -52,7 +61,8 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         typeClass() { | ||||
|             let type = this.openmct.types.get(this.observedObject.type); | ||||
|             let domainObjectType = this.liteObject ? this.domainObject.type : this.observedObject.type; | ||||
|             let type = this.openmct.types.get(domainObjectType); | ||||
|             if (!type) { | ||||
|                 return 'icon-object-unknown'; | ||||
|             } | ||||
| @@ -64,13 +74,15 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.observedObject) { | ||||
|         // if it's a liteObject nothing to observe | ||||
|         if (this.observedObject && !this.liteObject) { | ||||
|             let removeListener = this.openmct.objects.observe(this.observedObject, '*', (newObject) => { | ||||
|                 this.observedObject = newObject; | ||||
|             }); | ||||
|             this.$once('hook:destroyed', removeListener); | ||||
|         } | ||||
|  | ||||
|         // liteObjects do have identifiers, so statuses can be observed | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.observedObject.identifier, this.setStatus); | ||||
|         this.status = this.openmct.status.get(this.observedObject.identifier); | ||||
|         this.previewAction = new PreviewAction(this.openmct); | ||||
| @@ -79,35 +91,74 @@ export default { | ||||
|         this.removeStatusListener(); | ||||
|     }, | ||||
|     methods: { | ||||
|         navigateOrPreview(event) { | ||||
|         async navigateOrPreview(event) { | ||||
|             // skip if editing or is a lite object with an interaction function | ||||
|             if (this.openmct.editor.isEditing() || !(this.liteObject && this.beforeInteraction)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             event.preventDefault(); | ||||
|  | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 event.preventDefault(); | ||||
|                 this.preview(); | ||||
|             } else if (this.liteObject && this.beforeInteraction) { | ||||
|                 let fullObjectInfo = await this.getFullObjectInfo(); | ||||
|                 // need to update when new route functions are merged (back button PR) | ||||
|                 window.location.href = '#/browse/' + fullObjectInfo.navigationPath; | ||||
|             } | ||||
|         }, | ||||
|         preview() { | ||||
|             if (this.previewAction.appliesTo(this.objectPath)) { | ||||
|                 this.previewAction.invoke(this.objectPath); | ||||
|         async preview() { | ||||
|             let objectPath = this.objectPath; | ||||
|  | ||||
|             if (this.liteObject && this.beforeInteraction) { | ||||
|                 let fullObjectInfo = await this.getFullObjectInfo(); | ||||
|                 objectPath = fullObjectInfo.objectPath; | ||||
|             } | ||||
|  | ||||
|             if (this.previewAction.appliesTo(objectPath)) { | ||||
|                 this.previewAction.invoke(objectPath); | ||||
|             } | ||||
|         }, | ||||
|         dragStart(event) { | ||||
|             const LITE_DOMAIN_OBJECT_TYPE = "openmct/domain-object-lite"; | ||||
|  | ||||
|             let navigatedObject = this.openmct.router.path[0]; | ||||
|             let serializedPath = JSON.stringify(this.objectPath); | ||||
|             let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let serializedPath = JSON.stringify(this.objectPath); | ||||
|  | ||||
|             /* | ||||
|              * Cannot inspect data transfer objects on dragover/dragenter so impossible to determine composability at | ||||
|              * that point. If dragged object can be composed by navigated object, then indicate with presence of | ||||
|              * 'composable-domain-object' in data transfer | ||||
|              */ | ||||
|             if (this.openmct.composition.checkPolicy(navigatedObject, this.observedObject)) { | ||||
|                 event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject)); | ||||
|             if (this.liteObject) { | ||||
|                 event.dataTransfer.setData(LITE_DOMAIN_OBJECT_TYPE, JSON.stringify(this.domainObject.identifier)); | ||||
|  | ||||
|             } else { | ||||
|  | ||||
|                 /* | ||||
|                 * Cannot inspect data transfer objects on dragover/dragenter so impossible to determine composability at | ||||
|                 * that point. If dragged object can be composed by navigated object, then indicate with presence of | ||||
|                 * 'composable-domain-object' in data transfer | ||||
|                 */ | ||||
|                 if (this.openmct.composition.checkPolicy(navigatedObject, this.observedObject)) { | ||||
|                     event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject)); | ||||
|                 } | ||||
|  | ||||
|                 // serialize domain object anyway, because some views can drag-and-drop objects without composition | ||||
|                 // (eg. notabook.) | ||||
|                 event.dataTransfer.setData("openmct/domain-object-path", serializedPath); | ||||
|                 event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         async getFullObjectInfo() { | ||||
|             let fullObjectInfo = await this.beforeInteraction(); | ||||
|             let objectPath = fullObjectInfo.objectPath; | ||||
|             let navigationPath = objectPath | ||||
|                 .reverse() | ||||
|                 .map(object => | ||||
|                     this.openmct.objects.makeKeyString(object.identifier) | ||||
|                 ).join('/'); | ||||
|  | ||||
|             // serialize domain object anyway, because some views can drag-and-drop objects without composition | ||||
|             // (eg. notabook.) | ||||
|             event.dataTransfer.setData("openmct/domain-object-path", serializedPath); | ||||
|             event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject); | ||||
|             fullObjectInfo.objectPath = objectPath; | ||||
|             fullObjectInfo.navigationPath = navigationPath; | ||||
|  | ||||
|             return fullObjectInfo; | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|   | ||||
| @@ -74,6 +74,11 @@ export default { | ||||
|             this.styleRuleManager.destroy(); | ||||
|             delete this.styleRuleManager; | ||||
|         } | ||||
|  | ||||
|         if (this.actionCollection) { | ||||
|             this.actionCollection.destroy(); | ||||
|             delete this.actionCollection; | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|         this.debounceUpdateView = _.debounce(this.updateView, 10); | ||||
| @@ -149,6 +154,7 @@ export default { | ||||
|  | ||||
|             let keys = Object.keys(styleObj); | ||||
|             let elemToStyle = this.getStyleReceiver(); | ||||
|  | ||||
|             keys.forEach(key => { | ||||
|                 if (elemToStyle) { | ||||
|                     if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) { | ||||
| @@ -207,6 +213,7 @@ export default { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.getActionCollection(); | ||||
|             this.currentView.show(this.viewContainer, this.openmct.editor.isEditing()); | ||||
|  | ||||
|             if (immediatelySelect) { | ||||
| @@ -214,9 +221,16 @@ export default { | ||||
|                     this.$el, this.getSelectionContext(), true); | ||||
|             } | ||||
|  | ||||
|             this.$emit('change-provider', this.currentView); | ||||
|             this.openmct.objectViews.on('clearData', this.clearData); | ||||
|         }, | ||||
|         getActionCollection() { | ||||
|             if (this.actionCollection) { | ||||
|                 this.actionCollection.destroy(); | ||||
|             } | ||||
|  | ||||
|             this.actionCollection = this.openmct.actions._get(this.currentObjectPath || this.objectPath, this.currentView); | ||||
|             this.$emit('change-action-collection', this.actionCollection); | ||||
|         }, | ||||
|         show(object, viewKey, immediatelySelect, currentObjectPath) { | ||||
|             this.updateStyle(); | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         margin-bottom: $interiorMarginSm; | ||||
|         overflow: hidden; | ||||
|         padding: 3px; | ||||
|  | ||||
|         .c-object-label { | ||||
| @@ -35,6 +36,7 @@ | ||||
|     /*************************** FRAME CONTROLS */ | ||||
|     &__frame-controls { | ||||
|         display: flex; | ||||
|         flex: 0 0 auto; | ||||
|  | ||||
|         &__btns, | ||||
|         &__more { | ||||
| @@ -110,22 +112,8 @@ | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-status--missing { | ||||
|             @include isMissing($absPos: true); | ||||
|  | ||||
|             .is-status__indicator { | ||||
|                 top: $interiorMargin; | ||||
|                 left: $interiorMargin; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &.is-status--suspect { | ||||
|             @include isSuspect($absPos: true); | ||||
|  | ||||
|             .is-status__indicator { | ||||
|                 top: $interiorMargin; | ||||
|                 left: $interiorMargin; | ||||
|             } | ||||
|         &[class*='is-status'] { | ||||
|             border: $borderMissing; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -158,10 +146,6 @@ | ||||
|         border: $borderMissing; | ||||
|     } | ||||
|  | ||||
|     &.is-status--suspect { | ||||
|         border: $borderMissing; | ||||
|     } | ||||
|  | ||||
|     &__object-view { | ||||
|         display: flex; | ||||
|         flex: 1 1 auto; | ||||
| @@ -178,4 +162,5 @@ | ||||
|     // This element is the recipient for object styling; cannot be display: contents | ||||
|     flex: 1 1 auto; | ||||
|     overflow: hidden; | ||||
|     display: block; | ||||
| } | ||||
|   | ||||
| @@ -19,30 +19,31 @@ | ||||
|         display: block; | ||||
|         flex: 0 0 auto; | ||||
|         font-size: 1.1em; | ||||
|         opacity: $objectLabelTypeIconOpacity; | ||||
|     } | ||||
|  | ||||
|     &.is-status--missing { | ||||
|         @include isMissing($absPos: true); | ||||
|  | ||||
|         [class*='__type-icon']:before, | ||||
|         [class*='__type-icon']:after{ | ||||
|             opacity: $opacityMissing; | ||||
|         } | ||||
|  | ||||
|         .is-status__indicator { | ||||
|             right: -3px; | ||||
|             top: -3px; | ||||
|             transform: scale(0.7); | ||||
|         } | ||||
|     .is-status__indicator { | ||||
|         position: absolute; | ||||
|         right: -3px; | ||||
|         top: -3px; | ||||
|         transform: scale(0.5); | ||||
|     } | ||||
|  | ||||
|     &.is-status--missing, | ||||
|     &.is-status--suspect { | ||||
|         @include isSuspect($absPos: true); | ||||
|         [class*='__type-icon'] { | ||||
|             &:before, | ||||
|             &:after { | ||||
|                 opacity: $opacityMissing; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         .is-status__indicator { | ||||
|             right: -3px; | ||||
|             top: -3px; | ||||
|             transform: scale(0.7); | ||||
|     &.is-status--notebook-default { | ||||
|         &:after { | ||||
|             content: $glyph-icon-notebook-page; | ||||
|             display: block; | ||||
|             margin-left: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,13 +2,13 @@ | ||||
| <div class="c-inspector__header"> | ||||
|     <div v-if="!multiSelect" | ||||
|          class="c-inspector__selected c-object-label" | ||||
|          :class="{'is-status--missing': isMissing }" | ||||
|          :class="[statusClass]" | ||||
|     > | ||||
|         <div class="c-object-label__type-icon" | ||||
|              :class="typeCssClass" | ||||
|         > | ||||
|             <span class="is-status__indicator" | ||||
|                   title="This item is missing or suspect" | ||||
|                   :title="`This item is ${status}`" | ||||
|             ></span> | ||||
|         </div> | ||||
|         <span v-if="!singleSelectNonObject" | ||||
| @@ -37,8 +37,10 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: {}, | ||||
|             keyString: undefined, | ||||
|             multiSelect: false, | ||||
|             itemsSelected: 0 | ||||
|             itemsSelected: 0, | ||||
|             status: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -58,9 +60,8 @@ export default { | ||||
|         singleSelectNonObject() { | ||||
|             return !this.item.identifier && !this.multiSelect; | ||||
|         }, | ||||
|         isMissing() { | ||||
|             // safe check this.domainObject since for layout objects this.domainOjbect is undefined | ||||
|             return this.domainObject && this.domainObject.status === 'missing'; | ||||
|         statusClass() { | ||||
|             return this.status ? `is-status--${this.status}` : ''; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -69,25 +70,48 @@ export default { | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.selection.off('change', this.updateSelection); | ||||
|  | ||||
|         if (this.statusUnsubscribe) { | ||||
|             this.statusUnsubscribe(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         updateSelection(selection) { | ||||
|             if (this.statusUnsubscribe) { | ||||
|                 this.statusUnsubscribe(); | ||||
|                 this.statusUnsubscribe = undefined; | ||||
|             } | ||||
|  | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 this.domainObject = {}; | ||||
|                 this.resetDomainObject(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (selection.length > 1) { | ||||
|                 this.multiSelect = true; | ||||
|                 this.domainObject = {}; | ||||
|                 this.itemsSelected = selection.length; | ||||
|                 this.resetDomainObject(); | ||||
|  | ||||
|                 return; | ||||
|             } else { | ||||
|                 this.multiSelect = false; | ||||
|                 this.domainObject = selection[0][0].context.item; | ||||
|  | ||||
|                 if (this.domainObject) { | ||||
|                     this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|                     this.status = this.openmct.status.get(this.keyString); | ||||
|                     this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         resetDomainObject() { | ||||
|             this.domainObject = {}; | ||||
|             this.status = undefined; | ||||
|             this.keyString = undefined; | ||||
|         }, | ||||
|         updateStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -3,11 +3,13 @@ | ||||
|     <toolbar-select-menu | ||||
|         :options="fontSizeMenuOptions" | ||||
|         @change="setFontSize" | ||||
|         class="menus-to-left menus-no-icon" | ||||
|     /> | ||||
|     <div class="c-toolbar__separator"></div> | ||||
|     <toolbar-select-menu | ||||
|         :options="fontMenuOptions" | ||||
|         @change="setFont" | ||||
|         class="menus-to-left menus-no-icon" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
| @@ -15,12 +15,12 @@ | ||||
|                  :class="type.cssClass" | ||||
|             > | ||||
|                 <span class="is-status__indicator" | ||||
|                       title="This item is missing or suspect" | ||||
|                       :title="`This item is ${status}`" | ||||
|                 ></span> | ||||
|             </div> | ||||
|             <span | ||||
|                 class="l-browse-bar__object-name c-object-label__name c-input-inline" | ||||
|                 contenteditable | ||||
|                 :contenteditable="type.creatable" | ||||
|                 @blur="updateName" | ||||
|                 @keydown.enter.prevent | ||||
|                 @keyup.enter.prevent="updateNameOnEnterKeyPress" | ||||
| @@ -130,7 +130,7 @@ export default { | ||||
|         ViewSwitcher | ||||
|     }, | ||||
|     props: { | ||||
|         viewProvider: { | ||||
|         actionCollection: { | ||||
|             type: Object, | ||||
|             default: () => { | ||||
|                 return {}; | ||||
| @@ -226,20 +226,14 @@ export default { | ||||
|             this.status = this.openmct.status.get(this.domainObject.identifier, this.setStatus); | ||||
|             this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus); | ||||
|         }, | ||||
|         viewProvider(viewProvider) { | ||||
|         actionCollection(actionCollection) { | ||||
|             if (this.actionCollection) { | ||||
|                 this.unlistenToActionCollection(); | ||||
|             } | ||||
|  | ||||
|             if (viewProvider) { | ||||
|                 this.actionCollection = this.openmct.actions.get(this.openmct.router.path, viewProvider); | ||||
|                 this.actionCollection.on('update', this.updateActionItems); | ||||
|                 this.updateActionItems(this.actionCollection.applicableActions); | ||||
|             } else { | ||||
|                 this.statusBarViewKey = undefined; | ||||
|                 this.statusBarItems = []; | ||||
|                 this.menuActionItems = []; | ||||
|             } | ||||
|             this.actionCollection = actionCollection; | ||||
|             this.actionCollection.on('update', this.updateActionItems); | ||||
|             this.updateActionItems(this.actionCollection.getActionsObject()); | ||||
|         } | ||||
|     }, | ||||
|     mounted: function () { | ||||
| @@ -352,20 +346,11 @@ export default { | ||||
|             this.menuActionItems = this.actionCollection.getVisibleActions(); | ||||
|         }, | ||||
|         showMenuItems(event) { | ||||
|             let actions; | ||||
|  | ||||
|             if (this.menuActionItems.length) { | ||||
|                 actions = this.menuActionItems; | ||||
|             } else { | ||||
|                 actions = this.openmct.actions.get(this.openmct.router.path); | ||||
|             } | ||||
|  | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(actions); | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, sortedActions); | ||||
|         }, | ||||
|         unlistenToActionCollection() { | ||||
|             this.actionCollection.off('update', this.updateActionItems); | ||||
|             this.actionCollection.destroy(); | ||||
|             delete this.actionCollection; | ||||
|         }, | ||||
|         toggleLock(flag) { | ||||
|   | ||||
| @@ -70,7 +70,7 @@ | ||||
|             <browse-bar | ||||
|                 ref="browseBar" | ||||
|                 class="l-shell__main-view-browse-bar" | ||||
|                 :view-provider="viewProvider" | ||||
|                 :action-collection="actionCollection" | ||||
|                 @sync-tree-navigation="handleSyncTreeNavigation" | ||||
|             /> | ||||
|             <toolbar | ||||
| @@ -82,7 +82,7 @@ | ||||
|                 class="l-shell__main-container" | ||||
|                 data-selectable | ||||
|                 :show-edit-view="true" | ||||
|                 @change-provider="setProvider" | ||||
|                 @change-action-collection="setActionCollection" | ||||
|             /> | ||||
|             <component | ||||
|                 :is="conductorComponent" | ||||
| @@ -146,9 +146,9 @@ export default { | ||||
|             conductorComponent: undefined, | ||||
|             isEditing: false, | ||||
|             hasToolbar: false, | ||||
|             viewProvider: undefined, | ||||
|             headExpanded, | ||||
|             triggerSync: false | ||||
|             actionCollection: undefined, | ||||
|             triggerSync: false, | ||||
|             headExpanded | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -223,8 +223,8 @@ export default { | ||||
|  | ||||
|             this.hasToolbar = structure.length > 0; | ||||
|         }, | ||||
|         setProvider(provider) { | ||||
|             this.viewProvider = provider; | ||||
|         setActionCollection(actionCollection) { | ||||
|             this.actionCollection = actionCollection; | ||||
|         }, | ||||
|         handleSyncTreeNavigation() { | ||||
|             this.triggerSync = !this.triggerSync; | ||||
|   | ||||
| @@ -279,10 +279,12 @@ | ||||
|     } | ||||
|  | ||||
|     &__toolbar { | ||||
|         // Toolbar in the main shell, used by Display Layouts | ||||
|         $p: $interiorMargin; | ||||
|         background: $editUIBaseColor; | ||||
|         border-radius: $basicCr; | ||||
|         height: $p + 24px; // Need to standardize the height | ||||
|         justify-content: space-between; | ||||
|         padding: $p; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -98,7 +98,11 @@ | ||||
|                     :style="scrollableStyles()" | ||||
|                     @scroll="scrollItems" | ||||
|                 > | ||||
|                     <div :style="{ height: childrenHeight + 'px' }"> | ||||
|                     <!-- Regular Tree Items --> | ||||
|                     <div | ||||
|                         v-if="!activeSearch" | ||||
|                         :style="{ height: childrenHeight + 'px' }" | ||||
|                     > | ||||
|                         <tree-item | ||||
|                             v-for="(treeItem, index) in visibleItems" | ||||
|                             :key="treeItem.id" | ||||
| @@ -108,7 +112,32 @@ | ||||
|                             :item-index="index" | ||||
|                             :item-height="itemHeight" | ||||
|                             :virtual-scroll="true" | ||||
|                             :show-down="activeSearch ? false : true" | ||||
|                             :show-down="true" | ||||
|                             @expanded="beginNavigationRequest('handleExpanded', treeItem)" | ||||
|                         /> | ||||
|                         <div | ||||
|                             v-if="showNoItems" | ||||
|                             :style="indicatorLeftOffset" | ||||
|                             class="c-tree__item c-tree__item--empty" | ||||
|                         > | ||||
|                             No items | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <!-- Search Result Items (Index Only) --> | ||||
|                     <div | ||||
|                         v-if="activeSearch" | ||||
|                         :style="{ height: childrenHeight + 'px' }" | ||||
|                     > | ||||
|                         <tree-item-lite | ||||
|                             v-for="(treeItem, index) in visibleItems" | ||||
|                             :key="treeItem.id" | ||||
|                             :node="treeItem" | ||||
|                             :left-offset="itemLeftOffset" | ||||
|                             :item-offset="itemOffset" | ||||
|                             :item-index="index" | ||||
|                             :item-height="itemHeight" | ||||
|                             :virtual-scroll="true" | ||||
|                             :show-down="false" | ||||
|                             @expanded="beginNavigationRequest('handleExpanded', treeItem)" | ||||
|                         /> | ||||
|                         <div | ||||
| @@ -131,6 +160,7 @@ | ||||
| <script> | ||||
| import _ from 'lodash'; | ||||
| import treeItem from './tree-item.vue'; | ||||
| import treeItemLite from './tree-item-lite.vue'; | ||||
| import search from '../components/search.vue'; | ||||
| import objectUtils from 'objectUtils'; | ||||
| import uuid from 'uuid'; | ||||
| @@ -145,7 +175,8 @@ export default { | ||||
|     name: 'MctTree', | ||||
|     components: { | ||||
|         search, | ||||
|         treeItem | ||||
|         treeItem, | ||||
|         treeItemLite | ||||
|     }, | ||||
|     props: { | ||||
|         syncTreeNavigation: { | ||||
| @@ -596,6 +627,21 @@ export default { | ||||
|                 navigationPath | ||||
|             }; | ||||
|         }, | ||||
|         buildTreeItemLite(indexResult) { | ||||
|             let liteObject = { | ||||
|                 identifier: objectUtils.parseKeyString(indexResult.id), | ||||
|                 name: indexResult.name, | ||||
|                 type: indexResult.type | ||||
|             }; | ||||
|             let navigationPath = ''; | ||||
|  | ||||
|             return { | ||||
|                 id: indexResult.id, | ||||
|                 object: liteObject, | ||||
|                 objectPath: [], | ||||
|                 navigationPath | ||||
|             }; | ||||
|         }, | ||||
|         // domainObject: the item we're building the path for (will be used in url and links) | ||||
|         // objects: array of domainObjects representing path to domainobject passed in | ||||
|         buildNavigationPath(domainObject, objects) { | ||||
| @@ -693,30 +739,37 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         async getSearchResults() { | ||||
|             let results = await this.searchService.query(this.searchValue); | ||||
|             let results = await this.searchService.queryLite(this.searchValue); | ||||
|             this.searchResultItems = []; | ||||
|  | ||||
|             // build out tree-item-lite results | ||||
|             for (let i = 0; i < results.hits.length; i++) { | ||||
|                 let result = results.hits[i]; | ||||
|                 let newStyleObject = objectUtils.toNewFormat(result.object.getModel(), result.object.getId()); | ||||
|                 let objectPath = await this.openmct.objects.getOriginalPath(newStyleObject.identifier); | ||||
|  | ||||
|                 // removing the item itself, as the path we pass to buildTreeItem is a parent path | ||||
|                 objectPath.shift(); | ||||
|  | ||||
|                 // if root, remove, we're not using in object path for tree | ||||
|                 let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false; | ||||
|                 if (lastObject && lastObject.type === 'root') { | ||||
|                     objectPath.pop(); | ||||
|                 } | ||||
|  | ||||
|                 // we reverse the objectPath in the tree, so have to do it here first, | ||||
|                 // since this one is already in the correct direction | ||||
|                 let resultObject = this.buildTreeItem(newStyleObject, objectPath.reverse()); | ||||
|  | ||||
|                 let resultObject = this.buildTreeItemLite(result); | ||||
|                 this.searchResultItems.push(resultObject); | ||||
|             } | ||||
|  | ||||
|             // for (let i = 0; i < results.hits.length; i++) { | ||||
|             //     let result = results.hits[i]; | ||||
|             //     let newStyleObject = objectUtils.toNewFormat(result.object.getModel(), result.object.getId()); | ||||
|             //     let objectPath = await this.openmct.objects.getOriginalPath(newStyleObject.identifier); | ||||
|  | ||||
|             //     // removing the item itself, as the path we pass to buildTreeItem is a parent path | ||||
|             //     objectPath.shift(); | ||||
|  | ||||
|             //     // if root, remove, we're not using in object path for tree | ||||
|             //     let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false; | ||||
|             //     if (lastObject && lastObject.type === 'root') { | ||||
|             //         objectPath.pop(); | ||||
|             //     } | ||||
|  | ||||
|             //     // we reverse the objectPath in the tree, so have to do it here first, | ||||
|             //     // since this one is already in the correct direction | ||||
|             //     let resultObject = this.buildTreeItem(newStyleObject, objectPath.reverse()); | ||||
|  | ||||
|             //     this.searchResultItems.push(resultObject); | ||||
|             // } | ||||
|  | ||||
|             this.searchLoading = false; | ||||
|         }, | ||||
|         searchTree(value) { | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/ui/layout/tree-item-lite.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/ui/layout/tree-item-lite.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| <template> | ||||
| <div | ||||
|     ref="me" | ||||
|     :style="{ | ||||
|         'top': virtualScroll ? itemTop : 'auto', | ||||
|         'position': virtualScroll ? 'absolute' : 'relative' | ||||
|     }" | ||||
|     class="c-tree__item-h" | ||||
| > | ||||
|     <div | ||||
|         class="c-tree__item" | ||||
|     > | ||||
|         <view-control | ||||
|             v-model="expanded" | ||||
|             class="c-tree__item__view-control" | ||||
|             :control-class="'c-nav__up'" | ||||
|             :enabled="showUp" | ||||
|         /> | ||||
|         <object-label | ||||
|             :domain-object="node.object" | ||||
|             :object-path="node.objectPath" | ||||
|             :navigate-to-path="''" | ||||
|             :style="{ paddingLeft: leftOffset }" | ||||
|             :lite-object="true" | ||||
|             :before-interaction="onInteraction" | ||||
|         /> | ||||
|         <view-control | ||||
|             v-model="expanded" | ||||
|             class="c-tree__item__view-control" | ||||
|             :control-class="'c-nav__down'" | ||||
|             :enabled="showDown" | ||||
|         /> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import viewControl from '../components/viewControl.vue'; | ||||
| import ObjectLabel from '../components/ObjectLabel.vue'; | ||||
|  | ||||
| export default { | ||||
|     name: 'TreeItem', | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         viewControl, | ||||
|         ObjectLabel | ||||
|     }, | ||||
|     props: { | ||||
|         node: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         leftOffset: { | ||||
|             type: String, | ||||
|             default: '0px' | ||||
|         }, | ||||
|         showUp: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         showDown: { | ||||
|             type: Boolean, | ||||
|             default: true | ||||
|         }, | ||||
|         itemIndex: { | ||||
|             type: Number, | ||||
|             required: false, | ||||
|             default: undefined | ||||
|         }, | ||||
|         itemOffset: { | ||||
|             type: Number, | ||||
|             required: false, | ||||
|             default: undefined | ||||
|         }, | ||||
|         itemHeight: { | ||||
|             type: Number, | ||||
|             required: false, | ||||
|             default: 0 | ||||
|         }, | ||||
|         virtualScroll: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             expanded: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         itemTop() { | ||||
|             return (this.itemOffset + this.itemIndex) * this.itemHeight + 'px'; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         async onInteraction() { | ||||
|             let domainObject = await this.openmct.objects.get(this.node.object.identifier); | ||||
|             let objectPath = await this.openmct.objects.getOriginalPath(this.node.object.identifier); | ||||
|             objectPath.pop(); | ||||
|  | ||||
|             return { | ||||
|                 domainObject, | ||||
|                 objectPath | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -13,25 +13,36 @@ export default { | ||||
|         this.$el.addEventListener('contextmenu', this.showContextMenu); | ||||
|  | ||||
|         function updateObject(oldObject, newObject) { | ||||
|             if (oldObject.name == 'drag n drop sg') console.log({ oldObject, newObject}); | ||||
|             Object.assign(oldObject, newObject); | ||||
|         } | ||||
|  | ||||
|         this.objectPath.forEach(object => { | ||||
|             if (object) { | ||||
|                 this.$once('hook:destroyed', | ||||
|                     this.openmct.objects.observe(object, '*', updateObject.bind(this, object))); | ||||
|             } | ||||
|         }); | ||||
|         if (!this.liteObject) { | ||||
|             this.objectPath.forEach(object => { | ||||
|                 if (object) { | ||||
|                     this.$once('hook:destroyed', | ||||
|                         this.openmct.objects.observe(object, '*', updateObject.bind(this, object))); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.$el.removeEventListener('contextMenu', this.showContextMenu); | ||||
|     }, | ||||
|     methods: { | ||||
|         showContextMenu(event) { | ||||
|         async showContextMenu(event) { | ||||
|             event.preventDefault(); | ||||
|             event.stopPropagation(); | ||||
|  | ||||
|             let actions = this.openmct.actions.get(this.objectPath); | ||||
|             let objectPath = this.objectPath; | ||||
|  | ||||
|             if (this.liteObject && this.beforeInteraction) { | ||||
|                 let fullObjectInfo = await this.beforeInteraction(); | ||||
|                 objectPath = fullObjectInfo.objectPath; | ||||
|             } | ||||
|  | ||||
|             let actionsCollection = this.openmct.actions.get(objectPath); | ||||
|             let actions = actionsCollection.getVisibleActions(); | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(actions); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.clientX, event.clientY, sortedActions); | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| <div class="l-preview-window"> | ||||
|     <PreviewHeader | ||||
|         :current-view="currentView" | ||||
|         :action-collection="actionCollection" | ||||
|         :domain-object="domainObject" | ||||
|         :views="views" | ||||
|     /> | ||||
| @@ -52,7 +53,8 @@ export default { | ||||
|             domainObject: domainObject, | ||||
|             viewKey: undefined, | ||||
|             views: [], | ||||
|             currentView: {} | ||||
|             currentView: {}, | ||||
|             actionCollection: undefined | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -65,8 +67,7 @@ export default { | ||||
|         }); | ||||
|         this.setView(this.views[0]); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.view.destroy(); | ||||
|     beforeDestroy() { | ||||
|         if (this.stopListeningStyles) { | ||||
|             this.stopListeningStyles(); | ||||
|         } | ||||
| @@ -75,6 +76,13 @@ export default { | ||||
|             this.styleRuleManager.destroy(); | ||||
|             delete this.styleRuleManager; | ||||
|         } | ||||
|  | ||||
|         if (this.actionCollection) { | ||||
|             this.actionCollection.destroy(); | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.view.destroy(); | ||||
|     }, | ||||
|     methods: { | ||||
|         clear() { | ||||
| @@ -97,11 +105,19 @@ export default { | ||||
|             this.viewContainer = document.createElement('div'); | ||||
|             this.viewContainer.classList.add('l-angular-ov-wrapper'); | ||||
|             this.$refs.objectView.append(this.viewContainer); | ||||
|  | ||||
|             this.view = this.currentView.view(this.domainObject, this.objectPath); | ||||
|  | ||||
|             this.getActionsCollection(); | ||||
|             this.view.show(this.viewContainer, false); | ||||
|             this.initObjectStyles(); | ||||
|         }, | ||||
|         getActionsCollection() { | ||||
|             if (this.actionCollection) { | ||||
|                 this.actionCollection.destroy(); | ||||
|             } | ||||
|  | ||||
|             this.actionCollection = this.openmct.actions._get(this.objectPath, this.view); | ||||
|         }, | ||||
|         initObjectStyles() { | ||||
|             if (!this.styleRuleManager) { | ||||
|                 this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this)); | ||||
| @@ -124,8 +140,9 @@ export default { | ||||
|             } | ||||
|  | ||||
|             let keys = Object.keys(styleObj); | ||||
|             let firstChild = this.$refs.objectView.querySelector(':first-child'); | ||||
|  | ||||
|             keys.forEach(key => { | ||||
|                 let firstChild = this.$refs.objectView.querySelector(':first-child'); | ||||
|                 if (firstChild) { | ||||
|                     if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) { | ||||
|                         if (firstChild.style[key]) { | ||||
|   | ||||
| @@ -13,12 +13,25 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="l-browse-bar__end"> | ||||
|         <view-switcher | ||||
|             :v-if="!hideViewSwitcher" | ||||
|             :views="views" | ||||
|             :current-view="currentView" | ||||
|         /> | ||||
|         <div class="l-browse-bar__actions"> | ||||
|             <view-switcher | ||||
|                 :v-if="!hideViewSwitcher" | ||||
|                 :views="views" | ||||
|                 :current-view="currentView" | ||||
|             /> | ||||
|             <button | ||||
|                 v-for="(item, index) in statusBarItems" | ||||
|                 :key="index" | ||||
|                 class="c-button" | ||||
|                 :class="item.cssClass" | ||||
|                 @click="item.callBack" | ||||
|             > | ||||
|             </button> | ||||
|             <button | ||||
|                 class="l-browse-bar__actions c-icon-button icon-3-dots" | ||||
|                 title="More options" | ||||
|                 @click.prevent.stop="showMenuItems($event)" | ||||
|             ></button> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -26,6 +39,11 @@ | ||||
|  | ||||
| <script> | ||||
| import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue'; | ||||
| const HIDDEN_ACTIONS = [ | ||||
|     'remove', | ||||
|     'move', | ||||
|     'preview' | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     inject: [ | ||||
| @@ -58,16 +76,53 @@ export default { | ||||
|             default: () => { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         actionCollection: { | ||||
|             type: Object, | ||||
|             default: () => { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             type: this.openmct.types.get(this.domainObject.type) | ||||
|             type: this.openmct.types.get(this.domainObject.type), | ||||
|             statusBarItems: [], | ||||
|             menuActionItems: [] | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         actionCollection(actionCollection) { | ||||
|             if (this.actionCollection) { | ||||
|                 this.unlistenToActionCollection(); | ||||
|             } | ||||
|  | ||||
|             this.actionCollection.on('update', this.updateActionItems); | ||||
|             this.updateActionItems(this.actionCollection.getActionsObject()); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (this.actionCollection) { | ||||
|             this.actionCollection.on('update', this.updateActionItems); | ||||
|             this.updateActionItems(this.actionCollection.getActionsObject()); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setView(view) { | ||||
|             this.$emit('setView', view); | ||||
|         }, | ||||
|         unlistenToActionCollection() { | ||||
|             this.actionCollection.off('update', this.updateActionItems); | ||||
|             delete this.actionCollection; | ||||
|         }, | ||||
|         updateActionItems() { | ||||
|             this.actionCollection.hide(HIDDEN_ACTIONS); | ||||
|             this.statusBarItems = this.actionCollection.getStatusBarActions(); | ||||
|             this.menuActionItems = this.actionCollection.getVisibleActions(); | ||||
|         }, | ||||
|         showMenuItems(event) { | ||||
|             let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); | ||||
|             this.openmct.menus.showMenu(event.x, event.y, sortedActions); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user