Compare commits
	
		
			72 Commits
		
	
	
		
			v1.3.1
			...
			styles/uni
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 05c84e823e | ||
|   | 9cf476beec | ||
|   | bfd025b581 | ||
|   | f0d77f99e3 | ||
|   | 676df931d7 | ||
|   | eb79ec1a6d | ||
|   | 1af4e94fdd | ||
|   | 805c3821bc | ||
|   | 9952908eab | ||
|   | 1bdf25f6c8 | ||
|   | 2e142e3bba | ||
|   | 10627cc4bb | ||
|   | 59f9413c0e | ||
|   | 6ef73b6d64 | ||
|   | dff393a714 | ||
|   | fd9c9aee03 | ||
|   | 59bf981fb0 | ||
|   | bee4c917af | ||
|   | 4bbdac759f | ||
|   | 13fe7509de | ||
|   | 6fd8f6cd43 | ||
|   | 8f0d45e9b9 | ||
|   | 6375ecda34 | ||
|   | d232dacc65 | ||
|   | 59946e89ef | ||
|   | d75c4b4049 | ||
|   | 30ca4b707d | ||
|   | 27704c9a48 | ||
|   | b0203f2272 | ||
|   | 77b720d00d | ||
|   | ba982671b2 | ||
|   | 5df7d92d64 | ||
|   | a8228406de | ||
|   | 2401473012 | ||
|   | e502fb88fa | ||
|   | 37a52cb011 | ||
|   | 04fb4e8a82 | ||
|   | 5646a252f7 | ||
|   | 0e6ce7f58b | ||
|   | 8cd6a4c6a3 | ||
|   | 02fc162197 | ||
|   | 84d21a3695 | ||
|   | 1a6369c2b9 | ||
|   | 463c44679d | ||
|   | c1f3ea4e61 | ||
|   | 142b767470 | ||
|   | 184b716b53 | ||
|   | e53399495b | ||
|   | d27f73579b | ||
|   | 1ae8199e89 | ||
|   | 2deb4e8474 | ||
|   | 7f10681424 | ||
|   | c756adad6f | ||
|   | f3d593bc1e | ||
|   | b637307de6 | ||
|   | b6e0208e71 | ||
|   | 631876cab3 | ||
|   | a192d46c2b | ||
|   | 6923f17645 | ||
|   | 87a45de05b | ||
|   | ab76451360 | ||
|   | a91179091f | ||
|   | 5f7e34ce6c | ||
|   | db33f0538a | ||
|   | 257a8e2e2d | ||
|   | baa8078d23 | ||
|   | ee60013f45 | ||
|   | 505796d9f0 | ||
|   | 56120ba1bb | ||
|   | 225b235059 | ||
|   | de614ff606 | ||
|   | 7879752f47 | 
| @@ -76,6 +76,7 @@ define([ | ||||
|  | ||||
|             workerRequest[prop] = Number(workerRequest[prop]); | ||||
|         }); | ||||
|  | ||||
|         workerRequest.name = domainObject.name; | ||||
|  | ||||
|         return workerRequest; | ||||
|   | ||||
| @@ -108,7 +108,6 @@ | ||||
|  | ||||
|         for (; nextStep < end && data.length < 5000; nextStep += step) { | ||||
|             data.push({ | ||||
|                 name: request.name, | ||||
|                 utc: nextStep, | ||||
|                 yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                 sin: sin(nextStep, period, amplitude, offset, phase, randomness), | ||||
|   | ||||
| @@ -27,7 +27,7 @@ define([ | ||||
| ) { | ||||
|     function ImageryPlugin() { | ||||
|  | ||||
|         var IMAGE_SAMPLES = [ | ||||
|         const IMAGE_SAMPLES = [ | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg", | ||||
| @@ -47,13 +47,14 @@ define([ | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg", | ||||
|             "https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg" | ||||
|         ]; | ||||
|         const IMAGE_DELAY = 20000; | ||||
|  | ||||
|         function pointForTimestamp(timestamp, name) { | ||||
|             return { | ||||
|                 name: name, | ||||
|                 utc: Math.floor(timestamp / 5000) * 5000, | ||||
|                 local: Math.floor(timestamp / 5000) * 5000, | ||||
|                 url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length] | ||||
|                 utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY, | ||||
|                 url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length] | ||||
|             }; | ||||
|         } | ||||
|  | ||||
| @@ -64,7 +65,7 @@ define([ | ||||
|             subscribe: function (domainObject, callback) { | ||||
|                 var interval = setInterval(function () { | ||||
|                     callback(pointForTimestamp(Date.now(), domainObject.name)); | ||||
|                 }, 5000); | ||||
|                 }, IMAGE_DELAY); | ||||
|  | ||||
|                 return function () { | ||||
|                     clearInterval(interval); | ||||
| @@ -81,9 +82,9 @@ define([ | ||||
|                 var start = options.start; | ||||
|                 var end = Math.min(options.end, Date.now()); | ||||
|                 var data = []; | ||||
|                 while (start <= end && data.length < 5000) { | ||||
|                 while (start <= end && data.length < IMAGE_DELAY) { | ||||
|                     data.push(pointForTimestamp(start, domainObject.name)); | ||||
|                     start += 5000; | ||||
|                     start += IMAGE_DELAY; | ||||
|                 } | ||||
|  | ||||
|                 return Promise.resolve(data); | ||||
|   | ||||
							
								
								
									
										88
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								index.html
									
									
									
									
									
								
							| @@ -30,12 +30,50 @@ | ||||
|         <link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon"> | ||||
|         <link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon"> | ||||
|         <link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon"> | ||||
|         <style type="text/css"> | ||||
|             @keyframes splash-spinner { | ||||
|                 0% { | ||||
|                     transform: translate(-50%, -50%) rotate(0deg); } | ||||
|                 100% { | ||||
|                     transform: translate(-50%, -50%) rotate(360deg); } } | ||||
|  | ||||
|             #splash-screen { | ||||
|                 background-color: black; | ||||
|                 position: absolute; | ||||
|                 top: 0; right: 0; bottom: 0; left: 0; | ||||
|                 z-index: 10000; | ||||
|             } | ||||
|  | ||||
|             #splash-screen:before { | ||||
|                 animation-name: splash-spinner; | ||||
|                 animation-duration: 0.5s; | ||||
|                 animation-iteration-count: infinite; | ||||
|                 animation-timing-function: linear; | ||||
|                 border-radius: 50%; | ||||
|                 border-color: rgba(255,255,255,0.25); | ||||
|                 border-top-color: white; | ||||
|                 border-style: solid; | ||||
|                 border-width: 10px; | ||||
|                 content: ''; | ||||
|                 display: block; | ||||
|                 opacity: 0.25; | ||||
|                 position: absolute; | ||||
|                 left: 50%; top: 50%; | ||||
|                 height: 100px; width: 100px; | ||||
|             } | ||||
|         </style> | ||||
|     </head> | ||||
|     <body> | ||||
|     </body> | ||||
|     <script> | ||||
|         const THIRTY_SECONDS = 30 * 1000; | ||||
|         const THIRTY_MINUTES = THIRTY_SECONDS * 60; | ||||
|         const ONE_MINUTE = THIRTY_SECONDS * 2; | ||||
|         const FIVE_MINUTES = ONE_MINUTE * 5; | ||||
|         const FIFTEEN_MINUTES = FIVE_MINUTES * 3; | ||||
|         const THIRTY_MINUTES = FIFTEEN_MINUTES * 2; | ||||
|         const ONE_HOUR = THIRTY_MINUTES * 2; | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|         [ | ||||
|             'example/eventGenerator' | ||||
| @@ -48,6 +86,7 @@ | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|         openmct.install(openmct.plugins.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.Timeline()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|         openmct.install(openmct.plugins.AutoflowView({ | ||||
|             type: "telemetry.panel" | ||||
| @@ -72,21 +111,21 @@ | ||||
|                         { | ||||
|                             label: 'Last Day', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60 * 24, | ||||
|                                 start: () => Date.now() - ONE_DAY, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last 2 hours', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60 * 2, | ||||
|                                 start: () => Date.now() - TWO_HOURS, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: 'Last hour', | ||||
|                             bounds: { | ||||
|                                 start: () => Date.now() - 1000 * 60 * 60, | ||||
|                                 start: () => Date.now() - ONE_HOUR, | ||||
|                                 end: () => Date.now() | ||||
|                             } | ||||
|                         } | ||||
| @@ -95,7 +134,7 @@ | ||||
|                     records: 10, | ||||
|                     // maximum duration between start and end bounds | ||||
|                     // for utc-based time systems this is in milliseconds | ||||
|                     limit: 1000 * 60 * 60 * 24 | ||||
|                     limit: ONE_DAY | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Realtime", | ||||
| @@ -104,7 +143,44 @@ | ||||
|                     clockOffsets: { | ||||
|                         start: - THIRTY_MINUTES, | ||||
|                         end: THIRTY_SECONDS | ||||
|                     } | ||||
|                     }, | ||||
|                     presets: [ | ||||
|                         { | ||||
|                             label: '1 Hour', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_HOUR, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '30 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - THIRTY_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '15 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIFTEEN_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '5 Minutes', | ||||
|                             bounds: { | ||||
|                                 start: - FIVE_MINUTES, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             label: '1 Minute', | ||||
|                             bounds: { | ||||
|                                 start: - ONE_MINUTE, | ||||
|                                 end: THIRTY_SECONDS | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         })); | ||||
|   | ||||
| @@ -86,7 +86,7 @@ module.exports = (config) => { | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 64 | ||||
|                     lines: 65 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.3.0-SNAPSHOT", | ||||
|   "version": "1.4.1-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|   | ||||
| @@ -143,8 +143,8 @@ define([ | ||||
|                             "$window" | ||||
|                         ], | ||||
|                         "group": "windowing", | ||||
|                         "cssClass": "icon-new-window", | ||||
|                         "priority": "preferred" | ||||
|                         "priority": 10, | ||||
|                         "cssClass": "icon-new-window" | ||||
|                     } | ||||
|                 ], | ||||
|                 "runs": [ | ||||
|   | ||||
| @@ -139,7 +139,9 @@ define([ | ||||
|                         ], | ||||
|                         "description": "Edit", | ||||
|                         "category": "view-control", | ||||
|                         "cssClass": "major icon-pencil" | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "group": "action", | ||||
|                         "priority": 10 | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "properties", | ||||
| @@ -150,6 +152,8 @@ define([ | ||||
|                         "implementation": PropertiesAction, | ||||
|                         "cssClass": "major icon-pencil", | ||||
|                         "name": "Edit Properties...", | ||||
|                         "group": "action", | ||||
|                         "priority": 10, | ||||
|                         "description": "Edit properties of this object.", | ||||
|                         "depends": [ | ||||
|                             "dialogService" | ||||
|   | ||||
| @@ -20,12 +20,12 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="c-object-label" | ||||
|      ng-class="{ 'is-missing': model.status === 'missing' }" | ||||
|      ng-class="{ 'is-status--missing': model.status === 'missing' }" | ||||
| > | ||||
|     <div class="c-object-label__type-icon {{type.getCssClass()}}" | ||||
|          ng-class="{ 'l-icon-link':location.isLink() }" | ||||
|     > | ||||
|         <span class="is-missing__indicator" title="This item is missing"></span> | ||||
|         <span class="is-status__indicator" title="This item is missing or suspect"></span> | ||||
|     </div> | ||||
|     <div class='c-object-label__name'>{{model.name}}</div> | ||||
| </div> | ||||
|   | ||||
| @@ -114,7 +114,12 @@ define(["objectUtils"], | ||||
|             var self = this, | ||||
|                 domainObject = this.domainObject; | ||||
|  | ||||
|             let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId()); | ||||
|             const identifier = { | ||||
|                 namespace: this.getSpace(), | ||||
|                 key: this.getKey() | ||||
|             }; | ||||
|  | ||||
|             let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier); | ||||
|  | ||||
|             return this.openmct.objects | ||||
|                 .save(newStyleObject) | ||||
| @@ -146,6 +151,7 @@ define(["objectUtils"], | ||||
|                     return domainObject.useCapability("mutation", function () { | ||||
|                         return model; | ||||
|                     }, modified); | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -99,8 +99,8 @@ define( | ||||
|  | ||||
|                 mockNewStyleDomainObject = Object.assign({}, model); | ||||
|                 mockNewStyleDomainObject.identifier = { | ||||
|                     namespace: "", | ||||
|                     key: id | ||||
|                     namespace: SPACE, | ||||
|                     key: key | ||||
|                 }; | ||||
|  | ||||
|                 // Simulate mutation capability | ||||
|   | ||||
| @@ -22,7 +22,6 @@ | ||||
|  | ||||
| define([ | ||||
|     "./src/actions/MoveAction", | ||||
|     "./src/actions/CopyAction", | ||||
|     "./src/actions/LinkAction", | ||||
|     "./src/actions/SetPrimaryLocationAction", | ||||
|     "./src/services/LocatingCreationDecorator", | ||||
| @@ -37,7 +36,6 @@ define([ | ||||
|     "./src/services/LocationService" | ||||
| ], function ( | ||||
|     MoveAction, | ||||
|     CopyAction, | ||||
|     LinkAction, | ||||
|     SetPrimaryLocationAction, | ||||
|     LocatingCreationDecorator, | ||||
| @@ -66,6 +64,8 @@ define([ | ||||
|                         "description": "Move object to another location.", | ||||
|                         "cssClass": "icon-move", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 9, | ||||
|                         "implementation": MoveAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
| @@ -73,28 +73,14 @@ define([ | ||||
|                             "moveService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "copy", | ||||
|                         "name": "Duplicate", | ||||
|                         "description": "Duplicate object to another location.", | ||||
|                         "cssClass": "icon-duplicate", | ||||
|                         "category": "contextual", | ||||
|                         "implementation": CopyAction, | ||||
|                         "depends": [ | ||||
|                             "$log", | ||||
|                             "policyService", | ||||
|                             "locationService", | ||||
|                             "copyService", | ||||
|                             "dialogService", | ||||
|                             "notificationService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "link", | ||||
|                         "name": "Create Link", | ||||
|                         "description": "Create Link to object in another location.", | ||||
|                         "cssClass": "icon-link", | ||||
|                         "category": "contextual", | ||||
|                         "group": "action", | ||||
|                         "priority": 7, | ||||
|                         "implementation": LinkAction, | ||||
|                         "depends": [ | ||||
|                             "policyService", | ||||
|   | ||||
| @@ -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,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); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -19,7 +19,7 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="c-clock l-time-display" ng-controller="ClockController as clock"> | ||||
| <div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock"> | ||||
| 	<div class="c-clock__timezone"> | ||||
| 		{{clock.zone()}} | ||||
| 	</div> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="c-timer is-{{timer.timerState}}" ng-controller="TimerController as timer"> | ||||
| <div class="c-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer"> | ||||
|     <div class="c-timer__controls"> | ||||
|         <button ng-click="timer.clickStopButton()" | ||||
|                 ng-hide="timer.timerState == 'stopped'" | ||||
|   | ||||
| @@ -30,7 +30,6 @@ define([ | ||||
|     "./src/controllers/CompositeController", | ||||
|     "./src/controllers/ColorController", | ||||
|     "./src/controllers/DialogButtonController", | ||||
|     "./src/controllers/SnapshotPreviewController", | ||||
|     "./res/templates/controls/autocomplete.html", | ||||
|     "./res/templates/controls/checkbox.html", | ||||
|     "./res/templates/controls/datetime.html", | ||||
| @@ -44,8 +43,7 @@ define([ | ||||
|     "./res/templates/controls/menu-button.html", | ||||
|     "./res/templates/controls/dialog.html", | ||||
|     "./res/templates/controls/radio.html", | ||||
|     "./res/templates/controls/file-input.html", | ||||
|     "./res/templates/controls/snap-view.html" | ||||
|     "./res/templates/controls/file-input.html" | ||||
| ], function ( | ||||
|     MCTForm, | ||||
|     MCTControl, | ||||
| @@ -56,7 +54,6 @@ define([ | ||||
|     CompositeController, | ||||
|     ColorController, | ||||
|     DialogButtonController, | ||||
|     SnapshotPreviewController, | ||||
|     autocompleteTemplate, | ||||
|     checkboxTemplate, | ||||
|     datetimeTemplate, | ||||
| @@ -70,8 +67,7 @@ define([ | ||||
|     menuButtonTemplate, | ||||
|     dialogTemplate, | ||||
|     radioTemplate, | ||||
|     fileInputTemplate, | ||||
|     snapViewTemplate | ||||
|     fileInputTemplate | ||||
| ) { | ||||
|  | ||||
|     return { | ||||
| @@ -157,10 +153,6 @@ define([ | ||||
|                     { | ||||
|                         "key": "file-input", | ||||
|                         "template": fileInputTemplate | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "snap-view", | ||||
|                         "template": snapViewTemplate | ||||
|                     } | ||||
|                 ], | ||||
|                 "controllers": [ | ||||
| @@ -194,14 +186,6 @@ define([ | ||||
|                             "$scope", | ||||
|                             "dialogService" | ||||
|                         ] | ||||
|                     }, | ||||
|                     { | ||||
|                         "key": "SnapshotPreviewController", | ||||
|                         "implementation": SnapshotPreviewController, | ||||
|                         "depends": [ | ||||
|                             "$scope", | ||||
|                             "openmct" | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 "components": [ | ||||
|   | ||||
| @@ -1,36 +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. | ||||
| --> | ||||
| <span ng-controller="SnapshotPreviewController"  | ||||
|     class='form-control shell'> | ||||
|     <span class='field control {{structure.cssClass}}'> | ||||
|         <image  | ||||
|                class="c-ne__embed__snap-thumb" | ||||
|                src="{{imageUrl || structure.src}}" | ||||
|                ng-click="previewImage(imageUrl || structure.src)" | ||||
|                name="mctControl"> | ||||
|         </image> | ||||
|         <br> | ||||
|         <a title="Annotate" class="s-button icon-pencil" ng-click="annotateImage(ngModel, field, imageUrl || structure.src)"> | ||||
|             <span class="title-label">Annotate</span> | ||||
|         </a> | ||||
|     </span> | ||||
| </span> | ||||
| @@ -29,7 +29,6 @@ define(["zepto"], function ($) { | ||||
|      * @memberof platform/forms | ||||
|      */ | ||||
|     function FileInputService() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -38,7 +37,7 @@ define(["zepto"], function ($) { | ||||
|      * | ||||
|      * @returns {Promise} promise for an object containing file meta-data | ||||
|      */ | ||||
|     FileInputService.prototype.getInput = function () { | ||||
|     FileInputService.prototype.getInput = function (fileType) { | ||||
|         var input = this.newInput(); | ||||
|         var read = this.readFile; | ||||
|         var fileInfo = {}; | ||||
| @@ -51,6 +50,10 @@ define(["zepto"], function ($) { | ||||
|                 file = this.files[0]; | ||||
|                 input.remove(); | ||||
|                 if (file) { | ||||
|                     if (fileType && (!file.type || (file.type !== fileType))) { | ||||
|                         reject("Incompatible file type"); | ||||
|                     } | ||||
|  | ||||
|                     read(file) | ||||
|                         .then(function (contents) { | ||||
|                             fileInfo.name = file.name; | ||||
|   | ||||
| @@ -40,7 +40,7 @@ define( | ||||
|                 } | ||||
|  | ||||
|                 function handleClick() { | ||||
|                     fileInputService.getInput().then(function (result) { | ||||
|                     fileInputService.getInput(scope.structure.type).then(function (result) { | ||||
|                         setText(result.name); | ||||
|                         scope.ngModel[scope.field] = result; | ||||
|                         control.$setValidity("file-input", true); | ||||
|   | ||||
| @@ -1,132 +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( | ||||
|     [ | ||||
|         'painterro' | ||||
|     ], | ||||
|     function (Painterro) { | ||||
|  | ||||
|         function SnapshotPreviewController($scope, openmct) { | ||||
|  | ||||
|             $scope.previewImage = function (imageUrl) { | ||||
|                 let imageDiv = document.createElement('div'); | ||||
|                 imageDiv.classList = 'image-main s-image-main'; | ||||
|                 imageDiv.style.backgroundImage = `url(${imageUrl})`; | ||||
|  | ||||
|                 let previewImageOverlay = openmct.overlays.overlay( | ||||
|                     { | ||||
|                         element: imageDiv, | ||||
|                         size: 'large', | ||||
|                         buttons: [ | ||||
|                             { | ||||
|                                 label: 'Done', | ||||
|                                 callback: function () { | ||||
|                                     previewImageOverlay.dismiss(); | ||||
|                                 } | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 ); | ||||
|             }; | ||||
|  | ||||
|             $scope.annotateImage = function (ngModel, field, imageUrl) { | ||||
|                 $scope.imageUrl = imageUrl; | ||||
|  | ||||
|                 let div = document.createElement('div'), | ||||
|                     painterroInstance = {}, | ||||
|                     save = false; | ||||
|  | ||||
|                 div.id = 'snap-annotation'; | ||||
|  | ||||
|                 let annotateImageOverlay = openmct.overlays.overlay( | ||||
|                     { | ||||
|                         element: div, | ||||
|                         size: 'large', | ||||
|                         buttons: [ | ||||
|                             { | ||||
|                                 label: 'Cancel', | ||||
|                                 callback: function () { | ||||
|                                     save = false; | ||||
|                                     painterroInstance.save(); | ||||
|                                     annotateImageOverlay.dismiss(); | ||||
|                                 } | ||||
|                             }, | ||||
|                             { | ||||
|                                 label: 'Save', | ||||
|                                 callback: function () { | ||||
|                                     save = true; | ||||
|                                     painterroInstance.save(); | ||||
|                                     annotateImageOverlay.dismiss(); | ||||
|                                 } | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 painterroInstance = Painterro({ | ||||
|                     id: 'snap-annotation', | ||||
|                     activeColor: '#ff0000', | ||||
|                     activeColorAlpha: 1.0, | ||||
|                     activeFillColor: '#fff', | ||||
|                     activeFillColorAlpha: 0.0, | ||||
|                     backgroundFillColor: '#000', | ||||
|                     backgroundFillColorAlpha: 0.0, | ||||
|                     defaultFontSize: 16, | ||||
|                     defaultLineWidth: 2, | ||||
|                     defaultTool: 'ellipse', | ||||
|                     hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'], | ||||
|                     translation: { | ||||
|                         name: 'en', | ||||
|                         strings: { | ||||
|                             lineColor: 'Line', | ||||
|                             fillColor: 'Fill', | ||||
|                             lineWidth: 'Size', | ||||
|                             textColor: 'Color', | ||||
|                             fontSize: 'Size', | ||||
|                             fontStyle: 'Style' | ||||
|                         } | ||||
|                     }, | ||||
|                     saveHandler: function (image, done) { | ||||
|                         if (save) { | ||||
|                             let url = image.asBlob(), | ||||
|                                 reader = new window.FileReader(); | ||||
|  | ||||
|                             reader.readAsDataURL(url); | ||||
|                             reader.onloadend = function () { | ||||
|                                 $scope.imageUrl = reader.result; | ||||
|                                 ngModel[field] = reader.result; | ||||
|                             }; | ||||
|                         } else { | ||||
|                             ngModel.field = imageUrl; | ||||
|                             console.warn('You cancelled the annotation!!!'); | ||||
|                         } | ||||
|  | ||||
|                         done(true); | ||||
|                     } | ||||
|                 }).show(imageUrl); | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         return SnapshotPreviewController; | ||||
|     } | ||||
| ); | ||||
| @@ -47,6 +47,8 @@ define([ | ||||
|                             "implementation": ExportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-export", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "openmct", | ||||
|                                 "exportService", | ||||
| @@ -61,6 +63,8 @@ define([ | ||||
|                             "implementation": ImportAsJSONAction, | ||||
|                             "category": "contextual", | ||||
|                             "cssClass": "icon-import", | ||||
|                             "group": "json", | ||||
|                             "priority": 2, | ||||
|                             "depends": [ | ||||
|                                 "exportService", | ||||
|                                 "identifierService", | ||||
|   | ||||
| @@ -104,7 +104,7 @@ define([ | ||||
|                         "depends": [ | ||||
|                             "$q", | ||||
|                             "$log", | ||||
|                             "modelService", | ||||
|                             "objectService", | ||||
|                             "workerService", | ||||
|                             "topic", | ||||
|                             "GENERIC_SEARCH_ROOTS", | ||||
|   | ||||
| @@ -38,16 +38,16 @@ define([ | ||||
|      * @constructor | ||||
|      * @param $q Angular's $q, for promise consolidation. | ||||
|      * @param $log Anglar's $log, for logging. | ||||
|      * @param {ModelService} modelService the model service. | ||||
|      * @param {ObjectService} objectService the object service. | ||||
|      * @param {WorkerService} workerService the workerService. | ||||
|      * @param {TopicService} topic the topic service. | ||||
|      * @param {Array} ROOTS An array of object Ids to begin indexing. | ||||
|      */ | ||||
|     function GenericSearchProvider($q, $log, modelService, workerService, topic, ROOTS, USE_LEGACY_INDEXER, openmct) { | ||||
|     function GenericSearchProvider($q, $log, objectService, workerService, topic, ROOTS, USE_LEGACY_INDEXER, openmct) { | ||||
|         var provider = this; | ||||
|         this.$q = $q; | ||||
|         this.$log = $log; | ||||
|         this.modelService = modelService; | ||||
|         this.objectService = objectService; | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.indexedIds = {}; | ||||
| @@ -218,12 +218,12 @@ define([ | ||||
|             provider = this; | ||||
|  | ||||
|         this.pendingRequests += 1; | ||||
|         this.modelService | ||||
|             .getModels([idToIndex]) | ||||
|             .then(function (models) { | ||||
|         this.objectService | ||||
|             .getObjects([idToIndex]) | ||||
|             .then(function (objects) { | ||||
|                 delete provider.pendingIndex[idToIndex]; | ||||
|                 if (models[idToIndex]) { | ||||
|                     provider.index(idToIndex, models[idToIndex]); | ||||
|                 if (objects[idToIndex]) { | ||||
|                     provider.index(idToIndex, objects[idToIndex].model); | ||||
|                 } | ||||
|             }, function () { | ||||
|                 provider | ||||
|   | ||||
							
								
								
									
										10
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -46,6 +46,7 @@ define([ | ||||
|     './api/Branding', | ||||
|     './plugins/licenses/plugin', | ||||
|     './plugins/remove/plugin', | ||||
|     './plugins/duplicate/plugin', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
| @@ -73,6 +74,7 @@ define([ | ||||
|     BrandingAPI, | ||||
|     LicensesPlugin, | ||||
|     RemoveActionPlugin, | ||||
|     DuplicateActionPlugin, | ||||
|     Vue | ||||
| ) { | ||||
|     /** | ||||
| @@ -242,7 +244,11 @@ define([ | ||||
|  | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|  | ||||
|         this.contextMenu = new api.ContextMenuRegistry(); | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|  | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|         this.router = new ApplicationRouter(); | ||||
|  | ||||
| @@ -259,6 +265,7 @@ define([ | ||||
|         this.install(LegacyIndicatorsPlugin()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
|         this.install(RemoveActionPlugin.default()); | ||||
|         this.install(DuplicateActionPlugin.default()); | ||||
|         this.install(this.plugins.FolderView()); | ||||
|         this.install(this.plugins.Tabs()); | ||||
|         this.install(ImageryPlugin.default()); | ||||
| @@ -271,6 +278,7 @@ define([ | ||||
|         this.install(this.plugins.URLTimeSettingsSynchronizer()); | ||||
|         this.install(this.plugins.NotificationIndicator()); | ||||
|         this.install(this.plugins.NewFolderAction()); | ||||
|         this.install(this.plugins.ViewDatumAction()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
| @@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) { | ||||
|  | ||||
|     legacyActions.filter(contextualCategoryOnly) | ||||
|         .map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction)) | ||||
|         .forEach(openmct.contextMenu.registerAction); | ||||
|         .forEach(openmct.actions.register); | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,8 @@ export default class LegacyContextMenuAction { | ||||
|         this.description = LegacyAction.definition.description; | ||||
|         this.cssClass = LegacyAction.definition.cssClass; | ||||
|         this.LegacyAction = LegacyAction; | ||||
|         this.group = LegacyAction.definition.group; | ||||
|         this.priority = LegacyAction.definition.priority; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath) { | ||||
|   | ||||
| @@ -128,7 +128,7 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     ObjectServiceProvider.prototype.get = function (key) { | ||||
|         const keyString = utils.makeKeyString(key); | ||||
|         let keyString = utils.makeKeyString(key); | ||||
|  | ||||
|         return this.objectService.getObjects([keyString]) | ||||
|             .then(function (results) { | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import objectUtils from 'objectUtils'; | ||||
| import utils from 'objectUtils'; | ||||
|  | ||||
| export default class LegacyPersistenceAdapter { | ||||
|     constructor(openmct) { | ||||
| @@ -35,13 +35,43 @@ export default class LegacyPersistenceAdapter { | ||||
|         return Promise.resolve(Object.keys(this.openmct.objects.providers)); | ||||
|     } | ||||
|  | ||||
|     updateObject(legacyDomainObject) { | ||||
|         return this.openmct.objects.save(legacyDomainObject.useCapability('adapter')); | ||||
|     createObject(space, key, legacyDomainObject) { | ||||
|         let object = utils.toNewFormat(legacyDomainObject, { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }); | ||||
|  | ||||
|         return this.openmct.objects.save(object); | ||||
|     } | ||||
|  | ||||
|     readObject(keystring) { | ||||
|         let identifier = objectUtils.parseKeyString(keystring); | ||||
|     deleteObject(space, key) { | ||||
|         const identifier = { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }; | ||||
|  | ||||
|         return this.openmct.legacyObject(this.openmct.objects.get(identifier)); | ||||
|         return this.openmct.objects.delete(identifier); | ||||
|     } | ||||
|  | ||||
|     updateObject(space, key, legacyDomainObject) { | ||||
|         let object = utils.toNewFormat(legacyDomainObject, { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }); | ||||
|  | ||||
|         return this.openmct.objects.save(object); | ||||
|     } | ||||
|  | ||||
|     readObject(space, key) { | ||||
|         const identifier = { | ||||
|             namespace: space, | ||||
|             key: key | ||||
|         }; | ||||
|  | ||||
|         return this.openmct.objects.get(identifier).then(domainObject => { | ||||
|             let object = this.openmct.legacyObject(domainObject); | ||||
|  | ||||
|             return object.model; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionCollection extends EventEmitter { | ||||
|     constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) { | ||||
|         super(); | ||||
|  | ||||
|         this.applicableActions = applicableActions; | ||||
|         this.openmct = openmct; | ||||
|         this.objectPath = objectPath; | ||||
|         this.view = view; | ||||
|         this.skipEnvironmentObservers = skipEnvironmentObservers; | ||||
|         this.objectUnsubscribes = []; | ||||
|  | ||||
|         let debounceOptions = { | ||||
|             leading: false, | ||||
|             trailing: true | ||||
|         }; | ||||
|  | ||||
|         this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions); | ||||
|         this._update = _.debounce(this._update.bind(this), 150, debounceOptions); | ||||
|  | ||||
|         if (!skipEnvironmentObservers) { | ||||
|             this._observeObjectPath(); | ||||
|             this.openmct.editor.on('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this._initializeActions(); | ||||
|     } | ||||
|  | ||||
|     disable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     enable(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isDisabled = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     hide(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = true; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     show(actionKeys) { | ||||
|         actionKeys.forEach(actionKey => { | ||||
|             if (this.applicableActions[actionKey]) { | ||||
|                 this.applicableActions[actionKey].isHidden = false; | ||||
|             } | ||||
|         }); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
|             }); | ||||
|  | ||||
|             this.openmct.editor.off('isEditing', this._updateActions); | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let visibleActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (!action.isHidden) { | ||||
|                 visibleActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return visibleActions; | ||||
|     } | ||||
|  | ||||
|     getStatusBarActions() { | ||||
|         let actionsArray = Object.keys(this.applicableActions); | ||||
|         let statusBarActions = []; | ||||
|  | ||||
|         actionsArray.forEach(actionKey => { | ||||
|             let action = this.applicableActions[actionKey]; | ||||
|  | ||||
|             if (action.showInStatusBar && !action.isDisabled && !action.isHidden) { | ||||
|                 statusBarActions.push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return statusBarActions; | ||||
|     } | ||||
|  | ||||
|     getActionsObject() { | ||||
|         return this.applicableActions; | ||||
|     } | ||||
|  | ||||
|     _update() { | ||||
|         this.emit('update', this.applicableActions); | ||||
|     } | ||||
|  | ||||
|     _observeObjectPath() { | ||||
|         let actionCollection = this; | ||||
|  | ||||
|         function updateObject(oldObject, newObject) { | ||||
|             Object.assign(oldObject, newObject); | ||||
|  | ||||
|             actionCollection._updateActions(); | ||||
|         } | ||||
|  | ||||
|         this.objectPath.forEach(object => { | ||||
|             if (object) { | ||||
|                 let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object)); | ||||
|  | ||||
|                 this.objectUnsubscribes.push(unsubscribe); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _initializeActions() { | ||||
|         Object.keys(this.applicableActions).forEach(key => { | ||||
|             this.applicableActions[key].callBack = () => { | ||||
|                 return this.applicableActions[key].invoke(this.objectPath, this.view); | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     _updateActions() { | ||||
|         let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view); | ||||
|  | ||||
|         this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions); | ||||
|         this._initializeActions(); | ||||
|         this._update(); | ||||
|     } | ||||
|  | ||||
|     _mergeOldAndNewActions(oldActions, newActions) { | ||||
|         let mergedActions = {}; | ||||
|         Object.keys(newActions).forEach(key => { | ||||
|             if (oldActions[key]) { | ||||
|                 mergedActions[key] = oldActions[key]; | ||||
|             } else { | ||||
|                 mergedActions[key] = newActions[key]; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return mergedActions; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionCollection; | ||||
							
								
								
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import ActionCollection from './ActionCollection'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| class ActionsAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._allActions = {}; | ||||
|         this._actionCollections = new WeakMap(); | ||||
|         this._openmct = openmct; | ||||
|  | ||||
|         this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; | ||||
|  | ||||
|         this.register = this.register.bind(this); | ||||
|         this.get = this.get.bind(this); | ||||
|         this._applicableActions = this._applicableActions.bind(this); | ||||
|         this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this); | ||||
|     } | ||||
|  | ||||
|     register(actionDefinition) { | ||||
|         this._allActions[actionDefinition.key] = actionDefinition; | ||||
|     } | ||||
|  | ||||
|     get(objectPath, view) { | ||||
|         if (view) { | ||||
|  | ||||
|             return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true); | ||||
|         } else { | ||||
|  | ||||
|             return this._newActionCollection(objectPath, view, true); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     updateGroupOrder(groupArray) { | ||||
|         this._groupOrder = groupArray; | ||||
|     } | ||||
|  | ||||
|     _get(objectPath, view) { | ||||
|         let actionCollection = this._newActionCollection(objectPath, view); | ||||
|  | ||||
|         this._actionCollections.set(view, actionCollection); | ||||
|         actionCollection.on('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|         return actionCollection; | ||||
|     } | ||||
|  | ||||
|     _getCachedActionCollection(objectPath, view) { | ||||
|         let cachedActionCollection = this._actionCollections.get(view); | ||||
|  | ||||
|         return cachedActionCollection; | ||||
|     } | ||||
|  | ||||
|     _newActionCollection(objectPath, view, skipEnvironmentObservers) { | ||||
|         let applicableActions = this._applicableActions(objectPath, view); | ||||
|  | ||||
|         return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers); | ||||
|     } | ||||
|  | ||||
|     _updateCachedActionCollections(key) { | ||||
|         if (this._actionCollections.has(key)) { | ||||
|             let actionCollection = this._actionCollections.get(key); | ||||
|             actionCollection.off('destroy', this._updateCachedActionCollections); | ||||
|  | ||||
|             this._actionCollections.delete(key); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _applicableActions(objectPath, view) { | ||||
|         let actionsObject = {}; | ||||
|  | ||||
|         let keys = Object.keys(this._allActions).filter(key => { | ||||
|             let actionDefinition = this._allActions[key]; | ||||
|  | ||||
|             if (actionDefinition.appliesTo === undefined) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return actionDefinition.appliesTo(objectPath, view); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         keys.forEach(key => { | ||||
|             let action = _.clone(this._allActions[key]); | ||||
|  | ||||
|             actionsObject[key] = action; | ||||
|         }); | ||||
|  | ||||
|         return actionsObject; | ||||
|     } | ||||
|  | ||||
|     _groupAndSortActions(actionsArray) { | ||||
|         if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { | ||||
|             actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); | ||||
|         } | ||||
|  | ||||
|         let actionsObject = {}; | ||||
|         let groupedSortedActionsArray = []; | ||||
|  | ||||
|         function sortDescending(a, b) { | ||||
|             return b.priority - a.priority; | ||||
|         } | ||||
|  | ||||
|         actionsArray.forEach(action => { | ||||
|             if (actionsObject[action.group] === undefined) { | ||||
|                 actionsObject[action.group] = [action]; | ||||
|             } else { | ||||
|                 actionsObject[action.group].push(action); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this._groupOrder.forEach(group => { | ||||
|             let groupArray = actionsObject[group]; | ||||
|  | ||||
|             if (groupArray) { | ||||
|                 groupedSortedActionsArray.push(groupArray.sort(sortDescending)); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return groupedSortedActionsArray; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default ActionsAPI; | ||||
							
								
								
									
										113
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ActionsAPI from './ActionsAPI'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe('The Actions API', () => { | ||||
|     let openmct; | ||||
|     let actionsAPI; | ||||
|     let mockAction; | ||||
|     let mockObjectPath; | ||||
|     let mockViewContext1; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         actionsAPI = new ActionsAPI(openmct); | ||||
|         mockAction = { | ||||
|             name: 'Test Action', | ||||
|             key: 'test-action', | ||||
|             cssClass: 'test-action', | ||||
|             description: 'This is a test action', | ||||
|             group: 'action', | ||||
|             priority: 9, | ||||
|             appliesTo: (objectPath, view = {}) => { | ||||
|                 if (view.getViewContext) { | ||||
|                     let viewContext = view.getViewContext(); | ||||
|  | ||||
|                     return viewContext.onlyAppliesToTestCase; | ||||
|                 } else if (objectPath.length) { | ||||
|                     return objectPath[0].type === 'fake-folder'; | ||||
|                 } | ||||
|  | ||||
|                 return false; | ||||
|             }, | ||||
|             invoke: () => { | ||||
|             } | ||||
|         }; | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'mock parent folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-parent-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         mockViewContext1 = { | ||||
|             getViewContext: () => { | ||||
|                 return { | ||||
|                     onlyAppliesToTestCase: true | ||||
|                 }; | ||||
|             } | ||||
|         }; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("register method", () => { | ||||
|         it("adds action to ActionsAPI", () => { | ||||
|             actionsAPI.register(mockAction); | ||||
|  | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("get method", () => { | ||||
|         beforeEach(() => { | ||||
|             actionsAPI.register(mockAction); | ||||
|         }); | ||||
|  | ||||
|         it("returns an object with relevant actions when invoked with objectPath only", () => { | ||||
|             let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1); | ||||
|             let action = actionCollection.getActionsObject()[mockAction.key]; | ||||
|  | ||||
|             expect(action.key).toEqual(mockAction.key); | ||||
|             expect(action.name).toEqual(mockAction.name); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -28,9 +28,10 @@ define([ | ||||
|     './telemetry/TelemetryAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './notifications/NotificationAPI', | ||||
|     './contextMenu/ContextMenuAPI', | ||||
|     './Editor' | ||||
|  | ||||
|     './Editor', | ||||
|     './menu/MenuAPI', | ||||
|     './actions/ActionsAPI', | ||||
|     './status/StatusAPI' | ||||
| ], function ( | ||||
|     TimeAPI, | ||||
|     ObjectAPI, | ||||
| @@ -39,8 +40,10 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     IndicatorAPI, | ||||
|     NotificationAPI, | ||||
|     ContextMenuAPI, | ||||
|     EditorAPI | ||||
|     EditorAPI, | ||||
|     MenuAPI, | ||||
|     ActionsAPI, | ||||
|     StatusAPI | ||||
| ) { | ||||
|     return { | ||||
|         TimeAPI: TimeAPI, | ||||
| @@ -51,6 +54,8 @@ define([ | ||||
|         IndicatorAPI: IndicatorAPI, | ||||
|         NotificationAPI: NotificationAPI.default, | ||||
|         EditorAPI: EditorAPI, | ||||
|         ContextMenuRegistry: ContextMenuAPI.default | ||||
|         MenuAPI: MenuAPI.default, | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         StatusAPI: StatusAPI.default | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.invoke(objectPath)" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions', 'objectPath'] | ||||
| }; | ||||
| </script> | ||||
| @@ -1,159 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import ContextMenuComponent from './ContextMenu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| /** | ||||
|  * The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface ContextMenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
| class ContextMenuAPI { | ||||
|     constructor() { | ||||
|         this._allActions = []; | ||||
|         this._activeContextMenu = undefined; | ||||
|  | ||||
|         this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this); | ||||
|         this.registerAction = this.registerAction.bind(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when | ||||
|      * selected. Applicabilioty can be restricted by specification of an `appliesTo` function. | ||||
|      * | ||||
|      * @interface ContextMenuAction | ||||
|      * @memberof module:openmct | ||||
|      * @property {string} name the human-readable name of this view | ||||
|      * @property {string} description a longer-form description (typically | ||||
|      *           a single sentence or short paragraph) of this kind of view | ||||
|      * @property {string} cssClass the CSS class to apply to labels for this | ||||
|      *           view (to add icons, for instance) | ||||
|      * @property {string} key unique key to identify the context menu action | ||||
|      *           (used in custom context menu eg table rows, to identify which actions to include) | ||||
|      * @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item) | ||||
|      */ | ||||
|     /** | ||||
|      * @method appliesTo | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on. | ||||
|      * @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false. | ||||
|      */ | ||||
|     /** | ||||
|      * Code to be executed when the action is selected from a context menu | ||||
|      * @method invoke | ||||
|      * @memberof module:openmct.ContextMenuAction# | ||||
|      * @param {DomainObject[]} objectPath the path of the object to invoke the action on. | ||||
|      */ | ||||
|     /** | ||||
|      * @param {ContextMenuAction} actionDefinition | ||||
|      */ | ||||
|     registerAction(actionDefinition) { | ||||
|         this._allActions.push(actionDefinition); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) { | ||||
|  | ||||
|         let applicableActions = this._allActions.filter((action) => { | ||||
|  | ||||
|             if (actionsToBeIncluded) { | ||||
|                 if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key); | ||||
|             } else { | ||||
|                 if (action.appliesTo === undefined) { | ||||
|                     return true; | ||||
|                 } | ||||
|  | ||||
|                 return action.appliesTo(objectPath) && !action.hideInDefaultMenu; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (this._activeContextMenu) { | ||||
|             this._hideActiveContextMenu(); | ||||
|         } | ||||
|  | ||||
|         this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions); | ||||
|         this._activeContextMenu.$mount(); | ||||
|         document.body.appendChild(this._activeContextMenu.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$el.style.left = `${position.x}px`; | ||||
|         this._activeContextMenu.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this._hideActiveContextMenu); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _hideActiveContextMenu() { | ||||
|         document.removeEventListener('click', this._hideActiveContextMenu); | ||||
|         document.body.removeChild(this._activeContextMenu.$el); | ||||
|         this._activeContextMenu.$destroy(); | ||||
|         this._activeContextMenu = undefined; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _createContextMenuForObject(objectPath, actions) { | ||||
|         return new Vue({ | ||||
|             components: { | ||||
|                 ContextMenu: ContextMenuComponent | ||||
|             }, | ||||
|             provide: { | ||||
|                 actions: actions, | ||||
|                 objectPath: objectPath | ||||
|             }, | ||||
|             template: '<ContextMenu></ContextMenu>' | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| export default ContextMenuAPI; | ||||
							
								
								
									
										67
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import Menu from './menu.js'; | ||||
|  | ||||
| /** | ||||
|  * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from | ||||
|  * custom HTML elements. | ||||
|  * @interface MenuAPI | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|  | ||||
| class MenuAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.showMenu = this.showMenu.bind(this); | ||||
|         this._clearMenuComponent = this._clearMenuComponent.bind(this); | ||||
|         this._showObjectMenu = this._showObjectMenu.bind(this); | ||||
|     } | ||||
|  | ||||
|     showMenu(x, y, actions) { | ||||
|         if (this.menuComponent) { | ||||
|             this.menuComponent.dismiss(); | ||||
|         } | ||||
|  | ||||
|         let options = { | ||||
|             x, | ||||
|             y, | ||||
|             actions | ||||
|         }; | ||||
|  | ||||
|         this.menuComponent = new Menu(options); | ||||
|         this.menuComponent.once('destroy', this._clearMenuComponent); | ||||
|     } | ||||
|  | ||||
|     _clearMenuComponent() { | ||||
|         this.menuComponent = undefined; | ||||
|         delete this.menuComponent; | ||||
|     } | ||||
|  | ||||
|     _showObjectMenu(objectPath, x, y, actionsToBeIncluded) { | ||||
|         let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded); | ||||
|  | ||||
|         this.showMenu(x, y, applicableActions); | ||||
|     } | ||||
| } | ||||
| export default MenuAPI; | ||||
							
								
								
									
										125
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import MenuAPI from './MenuAPI'; | ||||
| import Menu from './menu'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe ('The Menu API', () => { | ||||
|     let openmct; | ||||
|     let menuAPI; | ||||
|     let actionsArray; | ||||
|     let x; | ||||
|     let y; | ||||
|     let result; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         menuAPI = new MenuAPI(openmct); | ||||
|         actionsArray = [ | ||||
|             { | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'test-css-class-1', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'test-css-class-2', | ||||
|                 description: 'This is a test action', | ||||
|                 callBack: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         x = 8; | ||||
|         y = 16; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("showMenu method", () => { | ||||
|         it("creates an instance of Menu when invoked", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             expect(menuAPI.menuComponent).toBeInstanceOf(Menu); | ||||
|         }); | ||||
|  | ||||
|         describe("creates a menu component", () => { | ||||
|             let menuComponent; | ||||
|             let vueComponent; | ||||
|  | ||||
|             beforeEach(() => { | ||||
|                 menuAPI.showMenu(x, y, actionsArray); | ||||
|                 vueComponent = menuAPI.menuComponent.component; | ||||
|                 menuComponent = document.querySelector(".c-menu"); | ||||
|  | ||||
|                 spyOn(vueComponent, '$destroy'); | ||||
|             }); | ||||
|  | ||||
|             it("renders a menu component in the expected x and y coordinates", () => { | ||||
|                 let boundingClientRect = menuComponent.getBoundingClientRect(); | ||||
|                 let left = boundingClientRect.left; | ||||
|                 let top = boundingClientRect.top; | ||||
|  | ||||
|                 expect(left).toEqual(x); | ||||
|                 expect(top).toEqual(y); | ||||
|             }); | ||||
|  | ||||
|             it("with all the actions passed in", () => { | ||||
|                 expect(menuComponent).toBeDefined(); | ||||
|  | ||||
|                 let listItems = menuComponent.children[0].children; | ||||
|  | ||||
|                 expect(listItems.length).toEqual(actionsArray.length); | ||||
|             }); | ||||
|  | ||||
|             it("with click-able menu items, that will invoke the correct callBacks", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|  | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 expect(result).toEqual("Test Action 1 Invoked"); | ||||
|             }); | ||||
|  | ||||
|             it("dismisses the menu when action is clicked on", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|  | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 let menu = document.querySelector('.c-menu'); | ||||
|  | ||||
|                 expect(menu).toBeNull(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the destroy method when menu is dismissed", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(vueComponent.$destroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| <template> | ||||
| <div class="c-menu"> | ||||
|     <ul v-if="actions.length && actions[0].length"> | ||||
|         <template | ||||
|             v-for="(actionGroups, index) in actions" | ||||
|         > | ||||
|             <li | ||||
|                 v-for="action in actionGroups" | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 @click="action.callBack" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
|             </li> | ||||
|             <div | ||||
|                 v-if="index !== actions.length - 1" | ||||
|                 :key="index" | ||||
|                 class="c-menu__section-separator" | ||||
|             > | ||||
|             </div> | ||||
|             <li | ||||
|                 v-if="actionGroups.length === 0" | ||||
|                 :key="index" | ||||
|             > | ||||
|                 No actions defined. | ||||
|             </li> | ||||
|         </template> | ||||
|     </ul> | ||||
|  | ||||
|     <ul v-else> | ||||
|         <li | ||||
|             v-for="action in actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             @click="action.callBack" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|         </li> | ||||
|         <li v-if="actions.length === 0"> | ||||
|             No actions defined. | ||||
|         </li> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['actions'] | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import MenuComponent from './components/Menu.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| class Menu extends EventEmitter { | ||||
|     constructor(options) { | ||||
|         super(); | ||||
|  | ||||
|         this.options = options; | ||||
|  | ||||
|         this.component = new Vue({ | ||||
|             provide: { | ||||
|                 actions: options.actions | ||||
|             }, | ||||
|             components: { | ||||
|                 MenuComponent | ||||
|             }, | ||||
|             template: '<menu-component />' | ||||
|         }); | ||||
|  | ||||
|         if (options.onDestroy) { | ||||
|             this.once('destroy', options.onDestroy); | ||||
|         } | ||||
|  | ||||
|         this.dismiss = this.dismiss.bind(this); | ||||
|         this.show = this.show.bind(this); | ||||
|  | ||||
|         this.show(); | ||||
|     } | ||||
|  | ||||
|     dismiss() { | ||||
|         this.emit('destroy'); | ||||
|         document.body.removeChild(this.component.$el); | ||||
|         document.removeEventListener('click', this.dismiss); | ||||
|         this.component.$destroy(); | ||||
|     } | ||||
|  | ||||
|     show() { | ||||
|         this.component.$mount(); | ||||
|         document.body.appendChild(this.component.$el); | ||||
|  | ||||
|         let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el); | ||||
|  | ||||
|         this.component.$el.style.left = `${position.x}px`; | ||||
|         this.component.$el.style.top = `${position.y}px`; | ||||
|  | ||||
|         document.addEventListener('click', this.dismiss); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _calculatePopupPosition(eventPosX, eventPosY, menuElement) { | ||||
|         let menuDimensions = menuElement.getBoundingClientRect(); | ||||
|         let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|         let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|         if (overflowX > 0) { | ||||
|             eventPosX = eventPosX - overflowX; | ||||
|         } | ||||
|  | ||||
|         if (overflowY > 0) { | ||||
|             eventPosY = eventPosY - overflowY; | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             x: eventPosX, | ||||
|             y: eventPosY | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default Menu; | ||||
| @@ -47,6 +47,7 @@ define([ | ||||
|         this.providers = {}; | ||||
|         this.rootRegistry = new RootRegistry(); | ||||
|         this.rootProvider = new RootObjectProvider.default(this.rootRegistry); | ||||
|         this.cache = {}; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -154,6 +155,11 @@ define([ | ||||
|      *          has been saved, or be rejected if it cannot be saved | ||||
|      */ | ||||
|     ObjectAPI.prototype.get = function (identifier) { | ||||
|         let keystring = this.makeKeyString(identifier); | ||||
|         if (this.cache[keystring] !== undefined) { | ||||
|             return this.cache[keystring]; | ||||
|         } | ||||
|  | ||||
|         identifier = utils.parseKeyString(identifier); | ||||
|         const provider = this.getProvider(identifier); | ||||
|  | ||||
| @@ -165,7 +171,15 @@ define([ | ||||
|             throw new Error('Provider does not support get!'); | ||||
|         } | ||||
|  | ||||
|         return provider.get(identifier); | ||||
|         let objectPromise = provider.get(identifier); | ||||
|  | ||||
|         this.cache[keystring] = objectPromise; | ||||
|  | ||||
|         return objectPromise.then(result => { | ||||
|             delete this.cache[keystring]; | ||||
|  | ||||
|             return result; | ||||
|         }); | ||||
|     }; | ||||
|  | ||||
|     ObjectAPI.prototype.delete = function () { | ||||
| @@ -200,17 +214,19 @@ define([ | ||||
|         } else if (hasAlreadyBeenPersisted(domainObject)) { | ||||
|             result = Promise.resolve(true); | ||||
|         } else { | ||||
|             const persistedTime = Date.now(); | ||||
|             if (domainObject.persisted === undefined) { | ||||
|                 domainObject.persisted = domainObject.modified; | ||||
|                 result = new Promise((resolve) => { | ||||
|                     savedResolve = resolve; | ||||
|                 }); | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 provider.create(domainObject).then((response) => { | ||||
|                     this.mutate(domainObject, 'persisted', domainObject.modified); | ||||
|                     this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                     savedResolve(response); | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.mutate(domainObject, 'persisted', domainObject.modified); | ||||
|                 domainObject.persisted = persistedTime; | ||||
|                 this.mutate(domainObject, 'persisted', persistedTime); | ||||
|                 result = provider.update(domainObject); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -59,4 +59,25 @@ describe("The Object API", () => { | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The get function", () => { | ||||
|         describe("when a provider is available", () => { | ||||
|             let mockProvider; | ||||
|             beforeEach(() => { | ||||
|                 mockProvider = jasmine.createSpyObj("mock provider", [ | ||||
|                     "get" | ||||
|                 ]); | ||||
|                 mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject)); | ||||
|                 objectAPI.addProvider(TEST_NAMESPACE, mockProvider); | ||||
|             }); | ||||
|  | ||||
|             it("Caches multiple requests for the same object", () => { | ||||
|                 expect(mockProvider.get.calls.count()).toBe(0); | ||||
|                 objectAPI.get(mockDomainObject.identifier); | ||||
|                 expect(mockProvider.get.calls.count()).toBe(1); | ||||
|                 objectAPI.get(mockDomainObject.identifier); | ||||
|                 expect(mockProvider.get.calls.count()).toBe(1); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class OverlayAPI { | ||||
|                 this.dismissLastOverlay(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -127,6 +128,7 @@ class OverlayAPI { | ||||
|  | ||||
|         return progressDialog; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default OverlayAPI; | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/api/status/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2020, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| export default class StatusAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._statusCache = {}; | ||||
|  | ||||
|         this.get = this.get.bind(this); | ||||
|         this.set = this.set.bind(this); | ||||
|         this.observe = this.observe.bind(this); | ||||
|     } | ||||
|  | ||||
|     get(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         return this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     set(identifier, value) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = value; | ||||
|         this.emit(keyString, value); | ||||
|     } | ||||
|  | ||||
|     delete(identifier) { | ||||
|         let keyString = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this._statusCache[keyString] = undefined; | ||||
|         this.emit(keyString, undefined); | ||||
|         delete this._statusCache[keyString]; | ||||
|     } | ||||
|  | ||||
|     observe(identifier, callback) { | ||||
|         let key = this._openmct.objects.makeKeyString(identifier); | ||||
|  | ||||
|         this.on(key, callback); | ||||
|  | ||||
|         return () => { | ||||
|             this.off(key, callback); | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/api/status/StatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import StatusAPI from './StatusAPI.js'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe("The Status API", () => { | ||||
|     let statusAPI; | ||||
|     let openmct; | ||||
|     let identifier; | ||||
|     let status; | ||||
|     let status2; | ||||
|     let callback; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         statusAPI = new StatusAPI(openmct); | ||||
|         identifier = { | ||||
|             namespace: "test-namespace", | ||||
|             key: "test-key" | ||||
|         }; | ||||
|         status = "test-status"; | ||||
|         status2 = 'test-status-deux'; | ||||
|         callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("set function", () => { | ||||
|         it("sets status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("get function", () => { | ||||
|         it("returns status for identifier", () => { | ||||
|             statusAPI.set(identifier, status2); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toEqual(status2); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("delete function", () => { | ||||
|         it("deletes status for identifier", () => { | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             let resultingStatus = statusAPI.get(identifier); | ||||
|             expect(resultingStatus).toEqual(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|             resultingStatus = statusAPI.get(identifier); | ||||
|  | ||||
|             expect(resultingStatus).toBeUndefined(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("observe function", () => { | ||||
|  | ||||
|         it("allows callbacks to be attached to status set and delete events", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(status); | ||||
|  | ||||
|             statusAPI.delete(identifier); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledWith(undefined); | ||||
|             unsubscribe(); | ||||
|         }); | ||||
|  | ||||
|         it("returns a unsubscribe function", () => { | ||||
|             let unsubscribe = statusAPI.observe(identifier, callback); | ||||
|             unsubscribe(); | ||||
|  | ||||
|             statusAPI.set(identifier, status); | ||||
|  | ||||
|             expect(callback).toHaveBeenCalledTimes(0); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -21,12 +21,14 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
|     './TelemetryValueFormatter', | ||||
|     './DefaultMetadataProvider', | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     CustomStringFormatter, | ||||
|     TelemetryMetadataManager, | ||||
|     TelemetryValueFormatter, | ||||
|     DefaultMetadataProvider, | ||||
| @@ -142,6 +144,17 @@ define([ | ||||
|         this.valueFormatterCache = new WeakMap(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return Custom String Formatter | ||||
|      * | ||||
|      * @param {Object} valueMetadata valueMetadata for given telemetry object | ||||
|      * @param {string} format custom formatter string (eg: %.4f, <s etc.) | ||||
|      * @returns {CustomStringFormatter} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { | ||||
|         return new CustomStringFormatter.default(this.openmct, valueMetadata, format); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return true if the given domainObject is a telemetry object.  A telemetry | ||||
|      * object is any object which has telemetry metadata-- regardless of whether | ||||
| @@ -400,6 +413,17 @@ define([ | ||||
|         return _.sortBy(options, sortKeys); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatService = function () { | ||||
|         if (!this.formatService) { | ||||
|             this.formatService = this.openmct.$injector.get('formatService'); | ||||
|         } | ||||
|  | ||||
|         return this.formatService; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given valueMetadata. | ||||
|      * | ||||
| @@ -407,19 +431,27 @@ define([ | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { | ||||
|         if (!this.valueFormatterCache.has(valueMetadata)) { | ||||
|             if (!this.formatService) { | ||||
|                 this.formatService = this.openmct.$injector.get('formatService'); | ||||
|             } | ||||
|  | ||||
|             this.valueFormatterCache.set( | ||||
|                 valueMetadata, | ||||
|                 new TelemetryValueFormatter(valueMetadata, this.formatService) | ||||
|                 new TelemetryValueFormatter(valueMetadata, this.getFormatService()) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return this.valueFormatterCache.get(valueMetadata); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given key. | ||||
|      * @param {string} key | ||||
|      * | ||||
|      * @returns {Format} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatter = function (key) { | ||||
|         const formatMap = this.getFormatService().formatMap; | ||||
|  | ||||
|         return formatMap[key]; | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a format map of all value formatters for a given piece of telemetry | ||||
|      * metadata. | ||||
|   | ||||
| @@ -44,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,25 @@ export default { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         viewDatumAction: true, | ||||
|                         getDatum: () => { | ||||
|                             return this.datum; | ||||
|                         } | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|             let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             let allActions = actionCollection.getActionsObject(); | ||||
|             let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, applicableActions); | ||||
|         }, | ||||
|         resetValues() { | ||||
|             this.value = '---'; | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-lad-table-wrapper"> | ||||
| <div class="c-lad-table-wrapper u-style-receiver js-style-receiver"> | ||||
|     <table class="c-table c-lad-table"> | ||||
|         <thead> | ||||
|             <tr> | ||||
|   | ||||
| @@ -19,342 +19,352 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import AutoflowTabularPlugin from './AutoflowTabularPlugin'; | ||||
| import AutoflowTabularConstants from './AutoflowTabularConstants'; | ||||
| import $ from 'zepto'; | ||||
| import DOMObserver from './dom-observer'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     spyOnBuiltins | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| define([ | ||||
|     './AutoflowTabularPlugin', | ||||
|     './AutoflowTabularConstants', | ||||
|     '../../MCT', | ||||
|     'zepto', | ||||
|     './dom-observer' | ||||
| ], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) { | ||||
|     describe("AutoflowTabularPlugin", function () { | ||||
|         let testType; | ||||
|         let testObject; | ||||
|         let mockmct; | ||||
| describe("AutoflowTabularPlugin", () => { | ||||
|     let testType; | ||||
|     let testObject; | ||||
|     let mockmct; | ||||
|  | ||||
|         beforeEach(function () { | ||||
|             testType = "some-type"; | ||||
|             testObject = { type: testType }; | ||||
|             mockmct = new MCT(); | ||||
|             spyOn(mockmct.composition, 'get'); | ||||
|             spyOn(mockmct.objectViews, 'addProvider'); | ||||
|             spyOn(mockmct.telemetry, 'getMetadata'); | ||||
|             spyOn(mockmct.telemetry, 'getValueFormatter'); | ||||
|             spyOn(mockmct.telemetry, 'limitEvaluator'); | ||||
|             spyOn(mockmct.telemetry, 'request'); | ||||
|             spyOn(mockmct.telemetry, 'subscribe'); | ||||
|     beforeEach(() => { | ||||
|         testType = "some-type"; | ||||
|         testObject = { type: testType }; | ||||
|         mockmct = createOpenMct(); | ||||
|         spyOn(mockmct.composition, 'get'); | ||||
|         spyOn(mockmct.objectViews, 'addProvider'); | ||||
|         spyOn(mockmct.telemetry, 'getMetadata'); | ||||
|         spyOn(mockmct.telemetry, 'getValueFormatter'); | ||||
|         spyOn(mockmct.telemetry, 'limitEvaluator'); | ||||
|         spyOn(mockmct.telemetry, 'request'); | ||||
|         spyOn(mockmct.telemetry, 'subscribe'); | ||||
|  | ||||
|             const plugin = new AutoflowTabularPlugin({ type: testType }); | ||||
|             plugin(mockmct); | ||||
|         const plugin = new AutoflowTabularPlugin({ type: testType }); | ||||
|         plugin(mockmct); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(mockmct); | ||||
|     }); | ||||
|  | ||||
|     it("installs a view provider", () => { | ||||
|         expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     describe("installs a view provider which", () => { | ||||
|         let provider; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             provider = | ||||
|                 mockmct.objectViews.addProvider.calls.mostRecent().args[0]; | ||||
|         }); | ||||
|  | ||||
|         it("installs a view provider", function () { | ||||
|             expect(mockmct.objectViews.addProvider).toHaveBeenCalled(); | ||||
|         it("applies its view to the type from options", () => { | ||||
|             expect(provider.canView(testObject)).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         describe("installs a view provider which", function () { | ||||
|             let provider; | ||||
|         it("does not apply to other types", () => { | ||||
|             expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|         }); | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 provider = | ||||
|                     mockmct.objectViews.addProvider.calls.mostRecent().args[0]; | ||||
|             }); | ||||
|         describe("provides a view which", () => { | ||||
|             let testKeys; | ||||
|             let testChildren; | ||||
|             let testContainer; | ||||
|             let testHistories; | ||||
|             let mockComposition; | ||||
|             let mockMetadata; | ||||
|             let mockEvaluator; | ||||
|             let mockUnsubscribes; | ||||
|             let callbacks; | ||||
|             let view; | ||||
|             let domObserver; | ||||
|  | ||||
|             it("applies its view to the type from options", function () { | ||||
|                 expect(provider.canView(testObject)).toBe(true); | ||||
|             }); | ||||
|             function waitsForChange() { | ||||
|                 return new Promise(function (resolve) { | ||||
|                     window.requestAnimationFrame(resolve); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             it("does not apply to other types", function () { | ||||
|                 expect(provider.canView({ type: 'foo' })).toBe(false); | ||||
|             }); | ||||
|             function emitEvent(mockEmitter, type, event) { | ||||
|                 mockEmitter.on.calls.all().forEach((call) => { | ||||
|                     if (call.args[0] === type) { | ||||
|                         call.args[1](event); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             describe("provides a view which", function () { | ||||
|                 let testKeys; | ||||
|                 let testChildren; | ||||
|                 let testContainer; | ||||
|                 let testHistories; | ||||
|                 let mockComposition; | ||||
|                 let mockMetadata; | ||||
|                 let mockEvaluator; | ||||
|                 let mockUnsubscribes; | ||||
|                 let callbacks; | ||||
|                 let view; | ||||
|                 let domObserver; | ||||
|             beforeEach((done) => { | ||||
|                 callbacks = {}; | ||||
|  | ||||
|                 function waitsForChange() { | ||||
|                     return new Promise(function (resolve) { | ||||
|                         window.requestAnimationFrame(resolve); | ||||
|                 spyOnBuiltins(['requestAnimationFrame']); | ||||
|                 window.requestAnimationFrame.and.callFake((callBack) => { | ||||
|                     callBack(); | ||||
|                 }); | ||||
|  | ||||
|                 testObject = { type: 'some-type' }; | ||||
|                 testKeys = ['abc', 'def', 'xyz']; | ||||
|                 testChildren = testKeys.map((key) => { | ||||
|                     return { | ||||
|                         identifier: { | ||||
|                             namespace: "test", | ||||
|                             key: key | ||||
|                         }, | ||||
|                         name: "Object " + key | ||||
|                     }; | ||||
|                 }); | ||||
|                 testContainer = $('<div>')[0]; | ||||
|                 domObserver = new DOMObserver(testContainer); | ||||
|  | ||||
|                 testHistories = testKeys.reduce((histories, key, index) => { | ||||
|                     histories[key] = { | ||||
|                         key: key, | ||||
|                         range: index + 10, | ||||
|                         domain: key + index | ||||
|                     }; | ||||
|  | ||||
|                     return histories; | ||||
|                 }, {}); | ||||
|  | ||||
|                 mockComposition = | ||||
|                     jasmine.createSpyObj('composition', ['load', 'on', 'off']); | ||||
|                 mockMetadata = | ||||
|                     jasmine.createSpyObj('metadata', ['valuesForHints']); | ||||
|  | ||||
|                 mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); | ||||
|                 mockUnsubscribes = testKeys.reduce((map, key) => { | ||||
|                     map[key] = jasmine.createSpy('unsubscribe-' + key); | ||||
|  | ||||
|                     return map; | ||||
|                 }, {}); | ||||
|  | ||||
|                 mockmct.composition.get.and.returnValue(mockComposition); | ||||
|                 mockComposition.load.and.callFake(() => { | ||||
|                     testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); | ||||
|  | ||||
|                     return Promise.resolve(testChildren); | ||||
|                 }); | ||||
|  | ||||
|                 mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); | ||||
|                 mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => { | ||||
|                     const mockFormatter = jasmine.createSpyObj('formatter', ['format']); | ||||
|                     mockFormatter.format.and.callFake((datum) => { | ||||
|                         return datum[metadatum.hint]; | ||||
|                     }); | ||||
|  | ||||
|                     return mockFormatter; | ||||
|                 }); | ||||
|                 mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); | ||||
|                 mockmct.telemetry.subscribe.and.callFake((obj, callback) => { | ||||
|                     const key = obj.identifier.key; | ||||
|                     callbacks[key] = callback; | ||||
|  | ||||
|                     return mockUnsubscribes[key]; | ||||
|                 }); | ||||
|                 mockmct.telemetry.request.and.callFake((obj, request) => { | ||||
|                     const key = obj.identifier.key; | ||||
|  | ||||
|                     return Promise.resolve([testHistories[key]]); | ||||
|                 }); | ||||
|                 mockMetadata.valuesForHints.and.callFake((hints) => { | ||||
|                     return [{ hint: hints[0] }]; | ||||
|                 }); | ||||
|  | ||||
|                 view = provider.view(testObject); | ||||
|                 view.show(testContainer); | ||||
|  | ||||
|                 return done(); | ||||
|             }); | ||||
|  | ||||
|             afterEach(() => { | ||||
|                 domObserver.destroy(); | ||||
|             }); | ||||
|  | ||||
|             it("populates its container", () => { | ||||
|                 expect(testContainer.children.length > 0).toBe(true); | ||||
|             }); | ||||
|  | ||||
|             describe("when rows have been populated", () => { | ||||
|                 function rowsMatch() { | ||||
|                     const rows = $(testContainer).find(".l-autoflow-row").length; | ||||
|  | ||||
|                     return rows === testChildren.length; | ||||
|                 } | ||||
|  | ||||
|                 function emitEvent(mockEmitter, type, event) { | ||||
|                     mockEmitter.on.calls.all().forEach(function (call) { | ||||
|                         if (call.args[0] === type) { | ||||
|                             call.args[1](event); | ||||
|                         } | ||||
|                     }); | ||||
|                 it("shows one row per child object", () => { | ||||
|                     return domObserver.when(rowsMatch); | ||||
|                 }); | ||||
|  | ||||
|                 // it("adds rows on composition change", () => { | ||||
|                 //     const child = { | ||||
|                 //         identifier: { | ||||
|                 //             namespace: "test", | ||||
|                 //             key: "123" | ||||
|                 //         }, | ||||
|                 //         name: "Object 123" | ||||
|                 //     }; | ||||
|                 //     testChildren.push(child); | ||||
|                 //     emitEvent(mockComposition, 'add', child); | ||||
|  | ||||
|                 //     return domObserver.when(rowsMatch); | ||||
|                 // }); | ||||
|  | ||||
|                 it("removes rows on composition change", () => { | ||||
|                     const child = testChildren.pop(); | ||||
|                     emitEvent(mockComposition, 'remove', child.identifier); | ||||
|  | ||||
|                     return domObserver.when(rowsMatch); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("removes subscriptions when destroyed", () => { | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|                 view.destroy(); | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(mockUnsubscribes[key]).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("provides a button to change column width", () => { | ||||
|                 const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|                 const nextWidth = | ||||
|                     initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|  | ||||
|                 expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                     .toEqual(initialWidth + 'px'); | ||||
|  | ||||
|                 $(testContainer).find('.change-column-width').click(); | ||||
|  | ||||
|                 function widthHasChanged() { | ||||
|                     const width = $(testContainer).find('.l-autoflow-col').css('width'); | ||||
|  | ||||
|                     return width !== initialWidth + 'px'; | ||||
|                 } | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     callbacks = {}; | ||||
|  | ||||
|                     testObject = { type: 'some-type' }; | ||||
|                     testKeys = ['abc', 'def', 'xyz']; | ||||
|                     testChildren = testKeys.map(function (key) { | ||||
|                         return { | ||||
|                             identifier: { | ||||
|                                 namespace: "test", | ||||
|                                 key: key | ||||
|                             }, | ||||
|                             name: "Object " + key | ||||
|                         }; | ||||
|                 return domObserver.when(widthHasChanged) | ||||
|                     .then(() => { | ||||
|                         expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                             .toEqual(nextWidth + 'px'); | ||||
|                     }); | ||||
|                     testContainer = $('<div>')[0]; | ||||
|                     domObserver = new DOMObserver(testContainer); | ||||
|             }); | ||||
|  | ||||
|                     testHistories = testKeys.reduce(function (histories, key, index) { | ||||
|                         histories[key] = { | ||||
|                             key: key, | ||||
|                             range: index + 10, | ||||
|                             domain: key + index | ||||
|                         }; | ||||
|             it("subscribes to all child objects", () => { | ||||
|                 testKeys.forEach((key) => { | ||||
|                     expect(callbacks[key]).toEqual(jasmine.any(Function)); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                         return histories; | ||||
|                     }, {}); | ||||
|             it("displays historical telemetry", () => { | ||||
|                 function rowTextDefined() { | ||||
|                     return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== ""; | ||||
|                 } | ||||
|  | ||||
|                     mockComposition = | ||||
|                         jasmine.createSpyObj('composition', ['load', 'on', 'off']); | ||||
|                     mockMetadata = | ||||
|                         jasmine.createSpyObj('metadata', ['valuesForHints']); | ||||
|  | ||||
|                     mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']); | ||||
|                     mockUnsubscribes = testKeys.reduce(function (map, key) { | ||||
|                         map[key] = jasmine.createSpy('unsubscribe-' + key); | ||||
|  | ||||
|                         return map; | ||||
|                     }, {}); | ||||
|  | ||||
|                     mockmct.composition.get.and.returnValue(mockComposition); | ||||
|                     mockComposition.load.and.callFake(function () { | ||||
|                         testChildren.forEach(emitEvent.bind(null, mockComposition, 'add')); | ||||
|  | ||||
|                         return Promise.resolve(testChildren); | ||||
|                 return domObserver.when(rowTextDefined).then(() => { | ||||
|                     testKeys.forEach((key, index) => { | ||||
|                         const datum = testHistories[key]; | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.text()).toEqual(String(datum.range)); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                     mockmct.telemetry.getMetadata.and.returnValue(mockMetadata); | ||||
|                     mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) { | ||||
|                         const mockFormatter = jasmine.createSpyObj('formatter', ['format']); | ||||
|                         mockFormatter.format.and.callFake(function (datum) { | ||||
|                             return datum[metadatum.hint]; | ||||
|                         }); | ||||
|  | ||||
|                         return mockFormatter; | ||||
|                     }); | ||||
|                     mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator); | ||||
|                     mockmct.telemetry.subscribe.and.callFake(function (obj, callback) { | ||||
|                         const key = obj.identifier.key; | ||||
|                         callbacks[key] = callback; | ||||
|  | ||||
|                         return mockUnsubscribes[key]; | ||||
|                     }); | ||||
|                     mockmct.telemetry.request.and.callFake(function (obj, request) { | ||||
|                         const key = obj.identifier.key; | ||||
|  | ||||
|                         return Promise.resolve([testHistories[key]]); | ||||
|                     }); | ||||
|                     mockMetadata.valuesForHints.and.callFake(function (hints) { | ||||
|                         return [{ hint: hints[0] }]; | ||||
|                     }); | ||||
|  | ||||
|                     view = provider.view(testObject); | ||||
|                     view.show(testContainer); | ||||
|  | ||||
|                     return waitsForChange(); | ||||
|             it("displays incoming telemetry", () => { | ||||
|                 const testData = testKeys.map((key, index) => { | ||||
|                     return { | ||||
|                         key: key, | ||||
|                         range: index * 100, | ||||
|                         domain: key + index | ||||
|                     }; | ||||
|                 }); | ||||
|  | ||||
|                 afterEach(function () { | ||||
|                     domObserver.destroy(); | ||||
|                 testData.forEach((datum) => { | ||||
|                     callbacks[datum.key](datum); | ||||
|                 }); | ||||
|  | ||||
|                 it("populates its container", function () { | ||||
|                     expect(testContainer.children.length > 0).toBe(true); | ||||
|                 return waitsForChange().then(() => { | ||||
|                     testData.forEach((datum, index) => { | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.text()).toEqual(String(datum.range)); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 describe("when rows have been populated", function () { | ||||
|                     function rowsMatch() { | ||||
|                         const rows = $(testContainer).find(".l-autoflow-row").length; | ||||
|  | ||||
|                         return rows === testChildren.length; | ||||
|                     } | ||||
|  | ||||
|                     it("shows one row per child object", function () { | ||||
|                         return domObserver.when(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("adds rows on composition change", function () { | ||||
|                         const child = { | ||||
|                             identifier: { | ||||
|                                 namespace: "test", | ||||
|                                 key: "123" | ||||
|                             }, | ||||
|                             name: "Object 123" | ||||
|                         }; | ||||
|                         testChildren.push(child); | ||||
|                         emitEvent(mockComposition, 'add', child); | ||||
|  | ||||
|                         return domObserver.when(rowsMatch); | ||||
|                     }); | ||||
|  | ||||
|                     it("removes rows on composition change", function () { | ||||
|                         const child = testChildren.pop(); | ||||
|                         emitEvent(mockComposition, 'remove', child.identifier); | ||||
|  | ||||
|                         return domObserver.when(rowsMatch); | ||||
|             it("updates classes for limit violations", () => { | ||||
|                 const testClass = "some-limit-violation"; | ||||
|                 mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); | ||||
|                 testKeys.forEach((key) => { | ||||
|                     callbacks[key]({ | ||||
|                         range: 'foo', | ||||
|                         domain: 'bar' | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("removes subscriptions when destroyed", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).not.toHaveBeenCalled(); | ||||
|                     }); | ||||
|                     view.destroy(); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(mockUnsubscribes[key]).toHaveBeenCalled(); | ||||
|                 return waitsForChange().then(() => { | ||||
|                     testKeys.forEach((datum, index) => { | ||||
|                         const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                         expect($cell.hasClass(testClass)).toBe(true); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 it("provides a button to change column width", function () { | ||||
|                     const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH; | ||||
|                     const nextWidth = | ||||
|                         initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; | ||||
|             it("automatically flows to new columns", () => { | ||||
|                 const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|                 const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|                 const count = testKeys.length; | ||||
|                 const $container = $(testContainer); | ||||
|                 let promiseChain = Promise.resolve(); | ||||
|  | ||||
|                     expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                         .toEqual(initialWidth + 'px'); | ||||
|                 function columnsHaveAutoflowed() { | ||||
|                     const itemsHeight = $container.find('.l-autoflow-items').height(); | ||||
|                     const availableHeight = itemsHeight - sliderHeight; | ||||
|                     const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); | ||||
|                     const columns = Math.ceil(count / availableRows); | ||||
|  | ||||
|                     $(testContainer).find('.change-column-width').click(); | ||||
|                     return $container.find('.l-autoflow-col').length === columns; | ||||
|                 } | ||||
|  | ||||
|                     function widthHasChanged() { | ||||
|                         const width = $(testContainer).find('.l-autoflow-col').css('width'); | ||||
|  | ||||
|                         return width !== initialWidth + 'px'; | ||||
|                     } | ||||
|  | ||||
|                     return domObserver.when(widthHasChanged) | ||||
|                         .then(function () { | ||||
|                             expect($(testContainer).find('.l-autoflow-col').css('width')) | ||||
|                                 .toEqual(nextWidth + 'px'); | ||||
|                         }); | ||||
|                 $container.find('.abs').css({ | ||||
|                     position: 'absolute', | ||||
|                     left: '0px', | ||||
|                     right: '0px', | ||||
|                     top: '0px', | ||||
|                     bottom: '0px' | ||||
|                 }); | ||||
|                 $container.css({ position: 'absolute' }); | ||||
|  | ||||
|                 it("subscribes to all child objects", function () { | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         expect(callbacks[key]).toEqual(jasmine.any(Function)); | ||||
|                     }); | ||||
|                 $container.appendTo(document.body); | ||||
|  | ||||
|                 function setHeight(height) { | ||||
|                     $container.css('height', height + 'px'); | ||||
|  | ||||
|                     return domObserver.when(columnsHaveAutoflowed); | ||||
|                 } | ||||
|  | ||||
|                 for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { | ||||
|                     // eslint-disable-next-line no-invalid-this | ||||
|                     promiseChain = promiseChain.then(setHeight.bind(this, height)); | ||||
|                 } | ||||
|  | ||||
|                 return promiseChain.then(() => { | ||||
|                     $container.remove(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|                 it("displays historical telemetry", function () { | ||||
|                     function rowTextDefined() { | ||||
|                         return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== ""; | ||||
|                     } | ||||
|  | ||||
|                     return domObserver.when(rowTextDefined).then(function () { | ||||
|                         testKeys.forEach(function (key, index) { | ||||
|                             const datum = testHistories[key]; | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("displays incoming telemetry", function () { | ||||
|                     const testData = testKeys.map(function (key, index) { | ||||
|                         return { | ||||
|                             key: key, | ||||
|                             range: index * 100, | ||||
|                             domain: key + index | ||||
|                         }; | ||||
|                     }); | ||||
|  | ||||
|                     testData.forEach(function (datum) { | ||||
|                         callbacks[datum.key](datum); | ||||
|                     }); | ||||
|  | ||||
|                     return waitsForChange().then(function () { | ||||
|                         testData.forEach(function (datum, index) { | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.text()).toEqual(String(datum.range)); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates classes for limit violations", function () { | ||||
|                     const testClass = "some-limit-violation"; | ||||
|                     mockEvaluator.evaluate.and.returnValue({ cssClass: testClass }); | ||||
|                     testKeys.forEach(function (key) { | ||||
|                         callbacks[key]({ | ||||
|                             range: 'foo', | ||||
|                             domain: 'bar' | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     return waitsForChange().then(function () { | ||||
|                         testKeys.forEach(function (datum, index) { | ||||
|                             const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r"); | ||||
|                             expect($cell.hasClass(testClass)).toBe(true); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("automatically flows to new columns", function () { | ||||
|                     const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; | ||||
|                     const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; | ||||
|                     const count = testKeys.length; | ||||
|                     const $container = $(testContainer); | ||||
|                     let promiseChain = Promise.resolve(); | ||||
|  | ||||
|                     function columnsHaveAutoflowed() { | ||||
|                         const itemsHeight = $container.find('.l-autoflow-items').height(); | ||||
|                         const availableHeight = itemsHeight - sliderHeight; | ||||
|                         const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); | ||||
|                         const columns = Math.ceil(count / availableRows); | ||||
|  | ||||
|                         return $container.find('.l-autoflow-col').length === columns; | ||||
|                     } | ||||
|  | ||||
|                     $container.find('.abs').css({ | ||||
|                         position: 'absolute', | ||||
|                         left: '0px', | ||||
|                         right: '0px', | ||||
|                         top: '0px', | ||||
|                         bottom: '0px' | ||||
|                     }); | ||||
|                     $container.css({ position: 'absolute' }); | ||||
|  | ||||
|                     $container.appendTo(document.body); | ||||
|  | ||||
|                     function setHeight(height) { | ||||
|                         $container.css('height', height + 'px'); | ||||
|  | ||||
|                         return domObserver.when(columnsHaveAutoflowed); | ||||
|                     } | ||||
|  | ||||
|                     for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) { | ||||
|                         // eslint-disable-next-line no-invalid-this | ||||
|                         promiseChain = promiseChain.then(setHeight.bind(this, height)); | ||||
|                     } | ||||
|  | ||||
|                     return promiseChain.then(function () { | ||||
|                         $container.remove(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("loads composition exactly once", function () { | ||||
|                     const testObj = testChildren.pop(); | ||||
|                     emitEvent(mockComposition, 'remove', testObj.identifier); | ||||
|                     testChildren.push(testObj); | ||||
|                     emitEvent(mockComposition, 'add', testObj); | ||||
|                     expect(mockComposition.load.calls.count()).toEqual(1); | ||||
|                 }); | ||||
|             it("loads composition exactly once", () => { | ||||
|                 const testObj = testChildren.pop(); | ||||
|                 emitEvent(mockComposition, 'remove', testObj.identifier); | ||||
|                 testChildren.push(testObj); | ||||
|                 emitEvent(mockComposition, 'add', testObj); | ||||
|                 expect(mockComposition.load.calls.count()).toEqual(1); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -23,6 +23,7 @@ | ||||
| export default class ClearDataAction { | ||||
|     constructor(openmct, appliesToObjects) { | ||||
|         this.name = 'Clear Data for Object'; | ||||
|         this.key = 'clear-data-action'; | ||||
|         this.description = 'Clears current data for object, unsubscribes and resubscribes to data'; | ||||
|         this.cssClass = 'icon-clear-data'; | ||||
|  | ||||
|   | ||||
| @@ -53,7 +53,7 @@ define([ | ||||
|                 openmct.indicators.add(indicator); | ||||
|             } | ||||
|  | ||||
|             openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|             openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects)); | ||||
|         }; | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js'; | ||||
| describe('When the Clear Data Plugin is installed,', function () { | ||||
|     const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); | ||||
|     const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); | ||||
|     const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']); | ||||
|     const mockActionsProvider = jasmine.createSpyObj('actions', ['register']); | ||||
|  | ||||
|     const openmct = { | ||||
|         objectViews: mockObjectViews, | ||||
|         indicators: mockIndicatorProvider, | ||||
|         contextMenu: mockContextMenuProvider, | ||||
|         actions: mockActionsProvider, | ||||
|         install: function (plugin) { | ||||
|             plugin(this); | ||||
|         } | ||||
| @@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () { | ||||
|     it('Clear Data context menu action is installed', function () { | ||||
|         openmct.install(ClearDataActionPlugin([])); | ||||
|  | ||||
|         expect(mockContextMenuProvider.registerAction).toHaveBeenCalled(); | ||||
|         expect(mockActionsProvider.register).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('clear data action emits a clearData event when invoked', function () { | ||||
|   | ||||
| @@ -50,6 +50,7 @@ | ||||
| .c-cs { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex: 1 1 auto; | ||||
|     height: 100%; | ||||
|     overflow: hidden; | ||||
|  | ||||
|   | ||||
| @@ -21,21 +21,22 @@ | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-style"> | ||||
|     <span :class="[ | ||||
|               { 'is-style-invisible': styleItem.style.isStyleInvisible }, | ||||
|               { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } | ||||
|           ]" | ||||
|           :style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]" | ||||
|           class="c-style-thumb" | ||||
|     > | ||||
|         <span class="c-style-thumb__text" | ||||
|               :class="{ 'hide-nice': !hasProperty(styleItem.style.color) }" | ||||
| <div class="c-style has-local-controls c-toolbar"> | ||||
|     <div class="c-style__controls"> | ||||
|         <div :class="[ | ||||
|                  { 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible }, | ||||
|                  { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } | ||||
|              ]" | ||||
|              :style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]" | ||||
|              class="c-style-thumb" | ||||
|         > | ||||
|             ABC | ||||
|         </span> | ||||
|     </span> | ||||
|     <span class="c-toolbar"> | ||||
|             <span class="c-style-thumb__text" | ||||
|                   :class="{ 'hide-nice': !hasProperty(styleItem.style.color) }" | ||||
|             > | ||||
|                 ABC | ||||
|             </span> | ||||
|         </div> | ||||
|  | ||||
|         <toolbar-color-picker v-if="hasProperty(styleItem.style.border)" | ||||
|                               class="c-style__toolbar-button--border-color u-menu-to--center" | ||||
|                               :options="borderColorOption" | ||||
| @@ -61,7 +62,14 @@ | ||||
|                                :options="isStyleInvisibleOption" | ||||
|                                @change="updateStyleValue" | ||||
|         /> | ||||
|     </span> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Save Styles --> | ||||
|     <toolbar-button v-if="canSaveStyle" | ||||
|                     class="c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major" | ||||
|                     :options="saveOptions" | ||||
|                     @click="saveItemStyle()" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -80,12 +88,11 @@ export default { | ||||
|         ToolbarColorPicker, | ||||
|         ToolbarToggleButton | ||||
|     }, | ||||
|     inject: [ | ||||
|         'openmct' | ||||
|     ], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         isEditing: { | ||||
|             type: Boolean | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         }, | ||||
|         mixedStyles: { | ||||
|             type: Array, | ||||
| @@ -93,6 +100,10 @@ export default { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         nonSpecificFontProperties: { | ||||
|             type: Array, | ||||
|             required: true | ||||
|         }, | ||||
|         styleItem: { | ||||
|             type: Object, | ||||
|             required: true | ||||
| @@ -182,7 +193,16 @@ export default { | ||||
|                     } | ||||
|                 ] | ||||
|             }; | ||||
|  | ||||
|         }, | ||||
|         saveOptions() { | ||||
|             return { | ||||
|                 icon: 'icon-save', | ||||
|                 title: 'Save style', | ||||
|                 isEditing: this.isEditing | ||||
|             }; | ||||
|         }, | ||||
|         canSaveStyle() { | ||||
|             return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
| @@ -216,6 +236,9 @@ export default { | ||||
|             } | ||||
|  | ||||
|             this.$emit('persist', this.styleItem, item.property); | ||||
|         }, | ||||
|         saveItemStyle() { | ||||
|             this.$emit('save-style', this.itemStyle); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -31,6 +31,11 @@ | ||||
|         <div class="c-inspect-styles__header"> | ||||
|             Object Style | ||||
|         </div> | ||||
|         <FontStyleEditor | ||||
|             v-if="canStyleFont" | ||||
|             :font-style="consolidatedFontStyle" | ||||
|             @set-font-property="setFontProperty" | ||||
|         /> | ||||
|         <div class="c-inspect-styles__content"> | ||||
|             <div v-if="staticStyle" | ||||
|                  class="c-inspect-styles__style" | ||||
| @@ -39,7 +44,9 @@ | ||||
|                               :style-item="staticStyle" | ||||
|                               :is-editing="allowEditing" | ||||
|                               :mixed-styles="mixedStyles" | ||||
|                               :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                               @persist="updateStaticStyle" | ||||
|                               @save-style="saveStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|             <button | ||||
| @@ -58,10 +65,11 @@ | ||||
|         </div> | ||||
|         <div class="c-inspect-styles__content c-inspect-styles__condition-set"> | ||||
|             <a v-if="conditionSetDomainObject" | ||||
|                class="c-object-label icon-conditional" | ||||
|                class="c-object-label" | ||||
|                :href="navigateToPath" | ||||
|                @click="navigateOrPreview" | ||||
|             > | ||||
|                 <span class="c-object-label__type-icon icon-conditional"></span> | ||||
|                 <span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span> | ||||
|             </a> | ||||
|             <template v-if="allowEditing"> | ||||
| @@ -80,6 +88,12 @@ | ||||
|             </template> | ||||
|         </div> | ||||
|  | ||||
|         <FontStyleEditor | ||||
|             v-if="canStyleFont" | ||||
|             :font-style="consolidatedFontStyle" | ||||
|             @set-font-property="setFontProperty" | ||||
|         /> | ||||
|  | ||||
|         <div v-if="conditionsLoaded" | ||||
|              class="c-inspect-styles__conditions" | ||||
|         > | ||||
| @@ -97,8 +111,10 @@ | ||||
|                 /> | ||||
|                 <style-editor class="c-inspect-styles__editor" | ||||
|                               :style-item="conditionStyle" | ||||
|                               :non-specific-font-properties="nonSpecificFontProperties" | ||||
|                               :is-editing="allowEditing" | ||||
|                               @persist="updateConditionalStyle" | ||||
|                               @save-style="saveStyle" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -108,6 +124,7 @@ | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue'; | ||||
| import StyleEditor from "./StyleEditor.vue"; | ||||
| import PreviewAction from "@/ui/preview/PreviewAction.js"; | ||||
| import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils"; | ||||
| @@ -116,16 +133,30 @@ import ConditionError from "@/plugins/condition/components/ConditionError.vue"; | ||||
| import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue"; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| const NON_SPECIFIC = '??'; | ||||
| const NON_STYLEABLE_CONTAINER_TYPES = [ | ||||
|     'layout', | ||||
|     'flexible-layout', | ||||
|     'tabs' | ||||
| ]; | ||||
| const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [ | ||||
|     'line-view', | ||||
|     'box-view', | ||||
|     'image-view' | ||||
| ]; | ||||
|  | ||||
| export default { | ||||
|     name: 'StylesView', | ||||
|     components: { | ||||
|         FontStyleEditor, | ||||
|         StyleEditor, | ||||
|         ConditionError, | ||||
|         ConditionDescription | ||||
|     }, | ||||
|     inject: [ | ||||
|         'openmct', | ||||
|         'selection' | ||||
|         'selection', | ||||
|         'stylesManager' | ||||
|     ], | ||||
|     data() { | ||||
|         return { | ||||
| @@ -139,19 +170,80 @@ export default { | ||||
|             conditionsLoaded: false, | ||||
|             navigateToPath: '', | ||||
|             selectedConditionId: '', | ||||
|             locked: false | ||||
|             items: [], | ||||
|             domainObject: undefined, | ||||
|             consolidatedFontStyle: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         locked() { | ||||
|             return this.selection.some(selectionPath => { | ||||
|                 const self = selectionPath[0].context.item; | ||||
|                 const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined; | ||||
|  | ||||
|                 return (self && self.locked) || (parent && parent.locked); | ||||
|             }); | ||||
|         }, | ||||
|         allowEditing() { | ||||
|             return this.isEditing && !this.locked; | ||||
|         }, | ||||
|         styleableFontItems() { | ||||
|             return this.selection.filter(selectionPath => { | ||||
|                 const item = selectionPath[0].context.item; | ||||
|                 const itemType = item && item.type; | ||||
|                 const layoutItem = selectionPath[0].context.layoutItem; | ||||
|                 const layoutItemType = layoutItem && layoutItem.type; | ||||
|  | ||||
|                 if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 return true; | ||||
|             }); | ||||
|         }, | ||||
|         computedconsolidatedFontStyle() { | ||||
|             let consolidatedFontStyle; | ||||
|             const styles = []; | ||||
|  | ||||
|             this.styleableFontItems.forEach(styleable => { | ||||
|                 const fontStyle = this.getFontStyle(styleable[0]); | ||||
|  | ||||
|                 styles.push(fontStyle); | ||||
|             }); | ||||
|  | ||||
|             if (styles.length) { | ||||
|                 const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize); | ||||
|                 const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font); | ||||
|  | ||||
|                 consolidatedFontStyle = { | ||||
|                     fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC, | ||||
|                     font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return consolidatedFontStyle; | ||||
|         }, | ||||
|         nonSpecificFontProperties() { | ||||
|             if (!this.consolidatedFontStyle) { | ||||
|                 return []; | ||||
|             } | ||||
|  | ||||
|             return Object.keys(this.consolidatedFontStyle).filter(property => this.consolidatedFontStyle[property] === NON_SPECIFIC); | ||||
|         }, | ||||
|         canStyleFont() { | ||||
|             return this.styleableFontItems.length && this.allowEditing; | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.removeListeners(); | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|         this.stylesManager.off('styleSelected', this.applyStyleToSelection); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.items = []; | ||||
|         this.previewAction = new PreviewAction(this.openmct); | ||||
|         this.isMultipleSelection = this.selection.length > 1; | ||||
|         this.getObjectsAndItemsFromSelection(); | ||||
| @@ -166,7 +258,10 @@ export default { | ||||
|             this.initializeStaticStyle(); | ||||
|         } | ||||
|  | ||||
|         this.setConsolidatedFontStyle(); | ||||
|  | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|         this.stylesManager.on('styleSelected', this.applyStyleToSelection); | ||||
|     }, | ||||
|     methods: { | ||||
|         getObjectStyles() { | ||||
| @@ -178,10 +273,10 @@ export default { | ||||
|                 } | ||||
|             } else if (this.items.length) { | ||||
|                 const itemId = this.items[0].id; | ||||
|                 if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) { | ||||
|                 if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) { | ||||
|                     objectStyles = this.domainObject.configuration.objectStyles[itemId]; | ||||
|                 } | ||||
|             } else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) { | ||||
|             } else if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles) { | ||||
|                 objectStyles = this.domainObject.configuration.objectStyles; | ||||
|             } | ||||
|  | ||||
| @@ -219,6 +314,18 @@ export default { | ||||
|         isItemType(type, item) { | ||||
|             return item && (item.type === type); | ||||
|         }, | ||||
|         canPersistObject(item) { | ||||
|             // for now the only way to tell if an object can be persisted is if it is creatable. | ||||
|             let creatable = false; | ||||
|             if (item) { | ||||
|                 const type = this.openmct.types.get(item.type); | ||||
|                 if (type && type.definition) { | ||||
|                     creatable = (type.definition.creatable === true); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return creatable; | ||||
|         }, | ||||
|         hasConditionalStyle(domainObject, layoutItem) { | ||||
|             const id = layoutItem ? layoutItem.id : undefined; | ||||
|  | ||||
| @@ -235,13 +342,8 @@ export default { | ||||
|             this.selection.forEach((selectionItem) => { | ||||
|                 const item = selectionItem[0].context.item; | ||||
|                 const layoutItem = selectionItem[0].context.layoutItem; | ||||
|                 const layoutDomainObject = selectionItem[0].context.item; | ||||
|                 const isChildItem = selectionItem.length > 1; | ||||
|  | ||||
|                 if (layoutDomainObject && layoutDomainObject.locked) { | ||||
|                     this.locked = true; | ||||
|                 } | ||||
|  | ||||
|                 if (!isChildItem) { | ||||
|                     domainObject = item; | ||||
|                     itemStyle = getApplicableStylesForItem(item); | ||||
| @@ -251,7 +353,7 @@ export default { | ||||
|                 } else { | ||||
|                     this.canHide = true; | ||||
|                     domainObject = selectionItem[1].context.item; | ||||
|                     if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) { | ||||
|                     if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) { | ||||
|                         subObjects.push(item); | ||||
|                         itemStyle = getApplicableStylesForItem(item); | ||||
|                         if (this.hasConditionalStyle(item)) { | ||||
| @@ -275,7 +377,7 @@ export default { | ||||
|             const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles); | ||||
|             this.initialStyles = styles; | ||||
|             this.mixedStyles = mixedStyles; | ||||
|  | ||||
|             // main layout | ||||
|             this.domainObject = domainObject; | ||||
|             this.removeListeners(); | ||||
|             if (this.domainObject) { | ||||
| @@ -298,6 +400,7 @@ export default { | ||||
|         isKeyItemId(key) { | ||||
|             return (key !== 'styles') | ||||
|                 && (key !== 'staticStyle') | ||||
|                 && (key !== 'fontStyle') | ||||
|                 && (key !== 'defaultConditionId') | ||||
|                 && (key !== 'selectedConditionId') | ||||
|                 && (key !== 'conditionSetIdentifier'); | ||||
| @@ -637,6 +740,124 @@ export default { | ||||
|         }, | ||||
|         persist(domainObject, style) { | ||||
|             this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style); | ||||
|         }, | ||||
|         applyStyleToSelection(style) { | ||||
|             if (!this.allowEditing) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.updateSelectionFontStyle(style); | ||||
|             this.updateSelectionStyle(style); | ||||
|         }, | ||||
|         updateSelectionFontStyle(style) { | ||||
|             const fontSizeProperty = { | ||||
|                 fontSize: style.fontSize | ||||
|             }; | ||||
|             const fontProperty = { | ||||
|                 font: style.font | ||||
|             }; | ||||
|  | ||||
|             this.setFontProperty(fontSizeProperty); | ||||
|             this.setFontProperty(fontProperty); | ||||
|         }, | ||||
|         updateSelectionStyle(style) { | ||||
|             const foundStyle = this.findStyleByConditionId(this.selectedConditionId); | ||||
|  | ||||
|             if (foundStyle && !this.isStaticAndConditionalStyles) { | ||||
|                 Object.entries(style).forEach(([property, value]) => { | ||||
|                     if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) { | ||||
|                         foundStyle.style[property] = value; | ||||
|                     } | ||||
|                 }); | ||||
|                 this.getAndPersistStyles(); | ||||
|             } else { | ||||
|                 this.removeConditionSet(); | ||||
|                 Object.entries(style).forEach(([property, value]) => { | ||||
|                     if (this.staticStyle.style[property] !== undefined && this.staticStyle.style[property] !== value) { | ||||
|                         this.staticStyle.style[property] = value; | ||||
|                         this.getAndPersistStyles(property); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         saveStyle(style) { | ||||
|             const styleToSave = { | ||||
|                 ...style, | ||||
|                 ...this.consolidatedFontStyle | ||||
|             }; | ||||
|  | ||||
|             this.stylesManager.save(styleToSave); | ||||
|         }, | ||||
|         setConsolidatedFontStyle() { | ||||
|             const styles = []; | ||||
|  | ||||
|             this.styleableFontItems.forEach(styleable => { | ||||
|                 const fontStyle = this.getFontStyle(styleable[0]); | ||||
|  | ||||
|                 styles.push(fontStyle); | ||||
|             }); | ||||
|  | ||||
|             if (styles.length) { | ||||
|                 const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize); | ||||
|                 const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font); | ||||
|  | ||||
|                 const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC; | ||||
|                 const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC; | ||||
|  | ||||
|                 this.$set(this.consolidatedFontStyle, 'fontSize', fontSize); | ||||
|                 this.$set(this.consolidatedFontStyle, 'font', font); | ||||
|             } | ||||
|         }, | ||||
|         getFontStyle(selectionPath) { | ||||
|             const item = selectionPath.context.item; | ||||
|             const layoutItem = selectionPath.context.layoutItem; | ||||
|             let fontStyle = item && item.configuration && item.configuration.fontStyle; | ||||
|  | ||||
|             // support for legacy where font styling in layouts only | ||||
|             if (!fontStyle) { | ||||
|                 fontStyle = { | ||||
|                     fontSize: layoutItem && layoutItem.fontSize || 'default', | ||||
|                     font: layoutItem && layoutItem.font || 'default' | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return fontStyle; | ||||
|         }, | ||||
|         setFontProperty(fontStyleObject) { | ||||
|             let layoutDomainObject; | ||||
|             const [property, value] = Object.entries(fontStyleObject)[0]; | ||||
|  | ||||
|             this.styleableFontItems.forEach(styleable => { | ||||
|                 if (!this.isLayoutObject(styleable)) { | ||||
|                     const fontStyle = this.getFontStyle(styleable[0]); | ||||
|                     fontStyle[property] = value; | ||||
|  | ||||
|                     this.openmct.objects.mutate(styleable[0].context.item, 'configuration.fontStyle', fontStyle); | ||||
|                 } else { | ||||
|                     // all layoutItems in this context will share same parent layout | ||||
|                     if (!layoutDomainObject) { | ||||
|                         layoutDomainObject = styleable[1].context.item; | ||||
|                     } | ||||
|  | ||||
|                     // save layout item font style to parent layout configuration | ||||
|                     const layoutItemIndex = styleable[0].context.index; | ||||
|                     const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex]; | ||||
|  | ||||
|                     layoutItemConfiguration[property] = value; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             if (layoutDomainObject) { | ||||
|                 this.openmct.objects.mutate(layoutDomainObject, 'configuration.items', layoutDomainObject.configuration.items); | ||||
|             } | ||||
|  | ||||
|             // sync vue component on font update | ||||
|             this.$set(this.consolidatedFontStyle, property, value); | ||||
|         }, | ||||
|         isLayoutObject(selectionPath) { | ||||
|             const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type; | ||||
|  | ||||
|             return layoutItemType && layoutItemType !== 'subobject-view'; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -40,9 +40,11 @@ | ||||
|     } | ||||
|  | ||||
|     &__condition-set { | ||||
|         align-items: baseline; | ||||
|         border-bottom: 1px solid $colorInteriorBorder; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         padding-bottom: $interiorMargin; | ||||
|  | ||||
|         .c-object-label { | ||||
|             flex: 1 1 auto; | ||||
| @@ -53,7 +55,10 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__style, | ||||
|     &__style { | ||||
|         padding-bottom: $interiorMargin; | ||||
|     } | ||||
|  | ||||
|     &__condition { | ||||
|         padding: $interiorMargin; | ||||
|     } | ||||
|   | ||||
| @@ -146,6 +146,8 @@ describe('the plugin', function () { | ||||
|         let displayLayoutItem; | ||||
|         let lineLayoutItem; | ||||
|         let boxLayoutItem; | ||||
|         let notCreatableObjectItem; | ||||
|         let notCreatableObject; | ||||
|         let selection; | ||||
|         let component; | ||||
|         let styleViewComponentObject; | ||||
| @@ -264,6 +266,19 @@ describe('the plugin', function () { | ||||
|                             "stroke": "#717171", | ||||
|                             "type": "line-view", | ||||
|                             "id": "57d49a28-7863-43bd-9593-6570758916f0" | ||||
|                         }, | ||||
|                         { | ||||
|                             "width": 32, | ||||
|                             "height": 18, | ||||
|                             "x": 36, | ||||
|                             "y": 8, | ||||
|                             "identifier": { | ||||
|                                 "key": "~TEST~image", | ||||
|                                 "namespace": "test-space" | ||||
|                             }, | ||||
|                             "hasFrame": true, | ||||
|                             "type": "subobject-view", | ||||
|                             "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" | ||||
|                         } | ||||
|                     ], | ||||
|                     "layoutGrid": [ | ||||
| @@ -297,6 +312,52 @@ describe('the plugin', function () { | ||||
|                 "type": "box-view", | ||||
|                 "id": "89b88746-d325-487b-aec4-11b79afff9e8" | ||||
|             }; | ||||
|             notCreatableObjectItem = { | ||||
|                 "width": 32, | ||||
|                 "height": 18, | ||||
|                 "x": 36, | ||||
|                 "y": 8, | ||||
|                 "identifier": { | ||||
|                     "key": "~TEST~image", | ||||
|                     "namespace": "test-space" | ||||
|                 }, | ||||
|                 "hasFrame": true, | ||||
|                 "type": "subobject-view", | ||||
|                 "id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85" | ||||
|             }; | ||||
|             notCreatableObject = { | ||||
|                 "identifier": { | ||||
|                     "key": "~TEST~image", | ||||
|                     "namespace": "test-space" | ||||
|                 }, | ||||
|                 "name": "test~image", | ||||
|                 "location": "test-space:~TEST", | ||||
|                 "type": "test.image", | ||||
|                 "telemetry": { | ||||
|                     "values": [ | ||||
|                         { | ||||
|                             "key": "value", | ||||
|                             "name": "Value", | ||||
|                             "hints": { | ||||
|                                 "image": 1, | ||||
|                                 "priority": 0 | ||||
|                             }, | ||||
|                             "format": "image", | ||||
|                             "source": "value" | ||||
|                         }, | ||||
|                         { | ||||
|                             "key": "utc", | ||||
|                             "source": "timestamp", | ||||
|                             "name": "Timestamp", | ||||
|                             "format": "iso", | ||||
|                             "hints": { | ||||
|                                 "domain": 1, | ||||
|                                 "priority": 1 | ||||
|                             } | ||||
|                         } | ||||
|                     ] | ||||
|                 } | ||||
|             }; | ||||
|             selection = [ | ||||
|                 [{ | ||||
|                     context: { | ||||
| @@ -316,6 +377,19 @@ describe('the plugin', function () { | ||||
|                         "index": 0 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     context: { | ||||
|                         item: displayLayoutItem, | ||||
|                         "supportsMultiSelect": true | ||||
|                     } | ||||
|                 }], | ||||
|                 [{ | ||||
|                     context: { | ||||
|                         "item": notCreatableObject, | ||||
|                         "layoutItem": notCreatableObjectItem, | ||||
|                         "index": 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     context: { | ||||
|                         item: displayLayoutItem, | ||||
| @@ -344,7 +418,7 @@ describe('the plugin', function () { | ||||
|         }); | ||||
|  | ||||
|         it('initializes the items in the view', () => { | ||||
|             expect(styleViewComponentObject.items.length).toBe(2); | ||||
|             expect(styleViewComponentObject.items.length).toBe(3); | ||||
|         }); | ||||
|  | ||||
|         it('initializes conditional styles', () => { | ||||
| @@ -363,7 +437,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|             return Vue.nextTick().then(() => { | ||||
|                 expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); | ||||
|                 [boxLayoutItem, lineLayoutItem].forEach((item) => { | ||||
|                 [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { | ||||
|                     const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles; | ||||
|                     expect(itemStyles.length).toBe(2); | ||||
|                     const foundStyle = itemStyles.find((style) => { | ||||
| @@ -385,7 +459,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|             return Vue.nextTick().then(() => { | ||||
|                 expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined(); | ||||
|                 [boxLayoutItem, lineLayoutItem].forEach((item) => { | ||||
|                 [boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => { | ||||
|                     const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle; | ||||
|                     expect(itemStyle).toBeDefined(); | ||||
|                     const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| <template> | ||||
| <component :is="urlDefined ? 'a' : 'span'" | ||||
|            class="c-condition-widget" | ||||
|            class="c-condition-widget u-style-receiver js-style-receiver" | ||||
|            :href="urlDefined ? internalDomainObject.url : null" | ||||
| > | ||||
|     <div class="c-condition-widget__label"> | ||||
|   | ||||
| @@ -64,9 +64,16 @@ define([ | ||||
|                             components: { | ||||
|                                 AlphanumericFormatView: AlphanumericFormatView.default | ||||
|                             }, | ||||
|                             template: '<alphanumeric-format-view></alphanumeric-format-view>' | ||||
|                             template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     getViewContext() { | ||||
|                         if (component) { | ||||
|                             return component.$refs.alphanumericFormatView.getViewContext(); | ||||
|                         } else { | ||||
|                             return {}; | ||||
|                         } | ||||
|                     }, | ||||
|                     destroy: function () { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|   | ||||
							
								
								
									
										38
									
								
								src/plugins/displayLayout/CustomStringFormatter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/plugins/displayLayout/CustomStringFormatter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import printj from 'printj'; | ||||
|  | ||||
| export default class CustomStringFormatter { | ||||
|     constructor(openmct, valueMetadata, itemFormat) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.itemFormat = itemFormat; | ||||
|         this.valueMetadata = valueMetadata; | ||||
|     } | ||||
|  | ||||
|     format(datum) { | ||||
|         if (!this.itemFormat) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!this.itemFormat.startsWith('&')) { | ||||
|             return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             const key = this.itemFormat.slice(1); | ||||
|             const customFormatter = this.openmct.telemetry.getFormatter(key); | ||||
|             if (!customFormatter) { | ||||
|                 throw new Error('Custom Formatter not found'); | ||||
|             } | ||||
|  | ||||
|             return customFormatter.format(datum[this.valueMetadata.key]); | ||||
|         } catch (e) { | ||||
|             console.error(e); | ||||
|  | ||||
|             return datum[this.valueMetadata.key]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     setFormat(itemFormat) { | ||||
|         this.itemFormat = itemFormat; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										82
									
								
								src/plugins/displayLayout/CustomStringFormatterSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/plugins/displayLayout/CustomStringFormatterSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import CustomStringFormatter from './CustomStringFormatter'; | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
|  | ||||
| const CUSTOM_FORMATS = [ | ||||
|     { | ||||
|         key: 'sclk', | ||||
|         format: (value) => 2 * value | ||||
|     }, | ||||
|     { | ||||
|         key: 'lts', | ||||
|         format: (value) => 3 * value | ||||
|     } | ||||
| ]; | ||||
|  | ||||
| const valueMetadata = { | ||||
|     key: "sin", | ||||
|     name: "Sine", | ||||
|     unit: "Hz", | ||||
|     formatString: "%0.2f", | ||||
|     hints: { | ||||
|         range: 1, | ||||
|         priority: 3 | ||||
|     }, | ||||
|     source: "sin" | ||||
| }; | ||||
|  | ||||
| const datum = { | ||||
|     name: "1 Sine Wave Generator", | ||||
|     utc: 1603930354000, | ||||
|     yesterday: 1603843954000, | ||||
|     sin: 0.587785209686822, | ||||
|     cos: -0.8090170253297632 | ||||
| }; | ||||
|  | ||||
| describe('CustomStringFormatter', function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let customStringFormatter; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         element.appendChild(child); | ||||
|         CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct})); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'getFormatter'); | ||||
|         openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key)); | ||||
|  | ||||
|         customStringFormatter = new CustomStringFormatter(openmct, valueMetadata); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('adds custom format sclk', () => { | ||||
|         const format = openmct.telemetry.getFormatter('sclk'); | ||||
|         expect(format.key).toEqual('sclk'); | ||||
|     }); | ||||
|  | ||||
|     it('adds custom format lts', () => { | ||||
|         const format = openmct.telemetry.getFormatter('lts'); | ||||
|         expect(format.key).toEqual('lts'); | ||||
|     }); | ||||
|  | ||||
|     it('returns correct value for custom format sclk', () => { | ||||
|         customStringFormatter.setFormat('&sclk'); | ||||
|         const value = customStringFormatter.format(datum, valueMetadata); | ||||
|         expect(datum.sin * 2).toEqual(value); | ||||
|     }); | ||||
|  | ||||
|     it('returns correct value for custom format lts', () => { | ||||
|         customStringFormatter.setFormat('<s'); | ||||
|         const value = customStringFormatter.format(datum, valueMetadata); | ||||
|         expect(datum.sin * 3).toEqual(value); | ||||
|     }); | ||||
| }); | ||||
| @@ -73,7 +73,6 @@ define(['lodash'], function (_) { | ||||
|                         ] | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 const VIEW_TYPES = { | ||||
|                     'telemetry-view': { | ||||
|                         value: 'telemetry-view', | ||||
| @@ -96,7 +95,6 @@ define(['lodash'], function (_) { | ||||
|                         class: 'icon-tabular-realtime' | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 const APPLICABLE_VIEWS = { | ||||
|                     'telemetry-view': [ | ||||
|                         VIEW_TYPES['telemetry.plot.overlay'], | ||||
| @@ -390,29 +388,6 @@ define(['lodash'], function (_) { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function getTextSizeMenu(selectedParent, selection) { | ||||
|                     const TEXT_SIZE = [8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96, 128]; | ||||
|  | ||||
|                     return { | ||||
|                         control: "select-menu", | ||||
|                         domainObject: selectedParent, | ||||
|                         applicableSelectedItems: selection.filter(selectionPath => { | ||||
|                             let type = selectionPath[0].context.layoutItem.type; | ||||
|  | ||||
|                             return type === 'text-view' || type === 'telemetry-view'; | ||||
|                         }), | ||||
|                         property: function (selectionPath) { | ||||
|                             return getPath(selectionPath) + ".size"; | ||||
|                         }, | ||||
|                         title: "Set text size", | ||||
|                         options: TEXT_SIZE.map(size => { | ||||
|                             return { | ||||
|                                 value: size + "px" | ||||
|                             }; | ||||
|                         }) | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 function getTextButton(selectedParent, selection) { | ||||
|                     return { | ||||
|                         control: "button", | ||||
| @@ -423,7 +398,7 @@ define(['lodash'], function (_) { | ||||
|                         property: function (selectionPath) { | ||||
|                             return getPath(selectionPath); | ||||
|                         }, | ||||
|                         icon: "icon-font", | ||||
|                         icon: "icon-pencil", | ||||
|                         title: "Edit text properties", | ||||
|                         dialog: DIALOG_FORM.text | ||||
|                     }; | ||||
| @@ -623,6 +598,33 @@ define(['lodash'], function (_) { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 function getToggleGridButton(selection, selectionPath) { | ||||
|                     const ICON_GRID_SHOW = 'icon-grid-on'; | ||||
|                     const ICON_GRID_HIDE = 'icon-grid-off'; | ||||
|  | ||||
|                     let displayLayoutContext; | ||||
|  | ||||
|                     if (selection.length === 1 && selectionPath === undefined) { | ||||
|                         displayLayoutContext = selection[0][0].context; | ||||
|                     } else { | ||||
|                         displayLayoutContext = selectionPath[1].context; | ||||
|                     } | ||||
|  | ||||
|                     return { | ||||
|                         control: "button", | ||||
|                         domainObject: displayLayoutContext.item, | ||||
|                         icon: ICON_GRID_SHOW, | ||||
|                         method: function () { | ||||
|                             displayLayoutContext.toggleGrid(); | ||||
|  | ||||
|                             this.icon = this.icon === ICON_GRID_SHOW | ||||
|                                 ? ICON_GRID_HIDE | ||||
|                                 : ICON_GRID_SHOW; | ||||
|                         }, | ||||
|                         secondary: true | ||||
|                     }; | ||||
|                 } | ||||
|  | ||||
|                 function getSeparator() { | ||||
|                     return { | ||||
|                         control: "separator" | ||||
| @@ -637,7 +639,9 @@ define(['lodash'], function (_) { | ||||
|                 } | ||||
|  | ||||
|                 if (isMainLayoutSelected(selectedObjects[0])) { | ||||
|                     return [getAddButton(selectedObjects)]; | ||||
|                     return [ | ||||
|                         getToggleGridButton(selectedObjects), | ||||
|                         getAddButton(selectedObjects)]; | ||||
|                 } | ||||
|  | ||||
|                 let toolbar = { | ||||
| @@ -649,11 +653,11 @@ define(['lodash'], function (_) { | ||||
|                     'display-mode': [], | ||||
|                     'telemetry-value': [], | ||||
|                     'style': [], | ||||
|                     'text-style': [], | ||||
|                     'position': [], | ||||
|                     'duplicate': [], | ||||
|                     'unit-toggle': [], | ||||
|                     'remove': [] | ||||
|                     'remove': [], | ||||
|                     'toggle-grid': [] | ||||
|                 }; | ||||
|  | ||||
|                 selectedObjects.forEach(selectionPath => { | ||||
| @@ -699,12 +703,6 @@ define(['lodash'], function (_) { | ||||
|                             toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)]; | ||||
|                         } | ||||
|  | ||||
|                         if (toolbar['text-style'].length === 0) { | ||||
|                             toolbar['text-style'] = [ | ||||
|                                 getTextSizeMenu(selectedParent, selectedObjects) | ||||
|                             ]; | ||||
|                         } | ||||
|  | ||||
|                         if (toolbar.position.length === 0) { | ||||
|                             toolbar.position = [ | ||||
|                                 getStackOrder(selectedParent, selectionPath), | ||||
| @@ -730,12 +728,6 @@ define(['lodash'], function (_) { | ||||
|                             } | ||||
|                         } | ||||
|                     } else if (layoutItem.type === 'text-view') { | ||||
|                         if (toolbar['text-style'].length === 0) { | ||||
|                             toolbar['text-style'] = [ | ||||
|                                 getTextSizeMenu(selectedParent, selectedObjects) | ||||
|                             ]; | ||||
|                         } | ||||
|  | ||||
|                         if (toolbar.position.length === 0) { | ||||
|                             toolbar.position = [ | ||||
|                                 getStackOrder(selectedParent, selectionPath), | ||||
| @@ -800,6 +792,10 @@ define(['lodash'], function (_) { | ||||
|                     if (toolbar.duplicate.length === 0) { | ||||
|                         toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)]; | ||||
|                     } | ||||
|  | ||||
|                     if (toolbar['toggle-grid'].length === 0) { | ||||
|                         toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 let toolbarArray = Object.values(toolbar); | ||||
|   | ||||
| @@ -56,6 +56,28 @@ define(function () { | ||||
|                         1 | ||||
|                     ], | ||||
|                     required: true | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Horizontal size (px)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     property: [ | ||||
|                         "configuration", | ||||
|                         "layoutDimensions", | ||||
|                         0 | ||||
|                     ], | ||||
|                     required: false | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Vertical size (px)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     property: [ | ||||
|                         "configuration", | ||||
|                         "layoutDimensions", | ||||
|                         1 | ||||
|                     ], | ||||
|                     required: false | ||||
|                 } | ||||
|             ] | ||||
|         }; | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/plugins/displayLayout/actions/CopyToClipboardAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import clipboard from '@/utils/clipboard'; | ||||
|  | ||||
| export default class CopyToClipboardAction { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy value to clipboard'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToClipboard'; | ||||
|         this.name = 'Copy to Clipboard'; | ||||
|         this.priority = 1; | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         const viewContext = view.getViewContext && view.getViewContext(); | ||||
|         const formattedValue = viewContext.formattedValueForCopy(); | ||||
|  | ||||
|         clipboard.updateClipboard(formattedValue) | ||||
|             .then(() => { | ||||
|                 this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `); | ||||
|             }) | ||||
|             .catch(() => { | ||||
|                 this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
| @@ -29,7 +29,7 @@ | ||||
|     @endMove="() => $emit('endMove')" | ||||
| > | ||||
|     <div | ||||
|         class="c-box-view" | ||||
|         class="c-box-view u-style-receiver js-style-receiver" | ||||
|         :class="[styleClass]" | ||||
|         :style="style" | ||||
|     ></div> | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="l-layout" | ||||
|     class="l-layout u-style-receiver js-style-receiver" | ||||
|     :class="{ | ||||
|         'is-multi-selected': selectedLayoutItems.length > 1, | ||||
|         'allow-editing': isEditing | ||||
| @@ -31,21 +31,19 @@ | ||||
|     @click.capture="bypassSelection" | ||||
|     @drop="handleDrop" | ||||
| > | ||||
|     <!-- Background grid --> | ||||
|     <div | ||||
|     <display-layout-grid | ||||
|         v-if="isEditing" | ||||
|         class="l-layout__grid-holder c-grid" | ||||
|         :grid-size="gridSize" | ||||
|         :show-grid="showGrid" | ||||
|     /> | ||||
|     <div | ||||
|         v-if="shouldDisplayLayoutDimensions" | ||||
|         class="l-layout__dimensions" | ||||
|         :style="layoutDimensionsStyle" | ||||
|     > | ||||
|         <div | ||||
|             v-if="gridSize[0] >= 3" | ||||
|             class="c-grid__x l-grid l-grid-x" | ||||
|             :style="[{ backgroundSize: gridSize[0] + 'px 100%' }]" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="gridSize[1] >= 3" | ||||
|             class="c-grid__y l-grid l-grid-y" | ||||
|             :style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]" | ||||
|         ></div> | ||||
|         <div class="l-layout__dimensions-vals"> | ||||
|             {{ layoutDimensions[0] }},{{ layoutDimensions[1] }} | ||||
|         </div> | ||||
|     </div> | ||||
|     <component | ||||
|         :is="item.type" | ||||
| @@ -81,6 +79,7 @@ import TextView from './TextView.vue'; | ||||
| import LineView from './LineView.vue'; | ||||
| import ImageView from './ImageView.vue'; | ||||
| import EditMarquee from './EditMarquee.vue'; | ||||
| import DisplayLayoutGrid from './DisplayLayoutGrid.vue'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| const TELEMETRY_IDENTIFIER_FUNCTIONS = { | ||||
| @@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3; | ||||
|  | ||||
| let components = ITEM_TYPE_VIEW_MAP; | ||||
| components['edit-marquee'] = EditMarquee; | ||||
| components['display-layout-grid'] = DisplayLayoutGrid; | ||||
|  | ||||
| function getItemDefinition(itemType, ...options) { | ||||
|     let itemView = ITEM_TYPE_VIEW_MAP[itemType]; | ||||
| @@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) { | ||||
|  | ||||
| export default { | ||||
|     components: components, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -156,7 +157,8 @@ export default { | ||||
|         return { | ||||
|             internalDomainObject: domainObject, | ||||
|             initSelectIndex: undefined, | ||||
|             selection: [] | ||||
|             selection: [], | ||||
|             showGrid: true | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -171,6 +173,23 @@ export default { | ||||
|                 return this.itemIsInCurrentSelection(item); | ||||
|             }); | ||||
|         }, | ||||
|         layoutDimensions() { | ||||
|             return this.internalDomainObject.configuration.layoutDimensions; | ||||
|         }, | ||||
|         shouldDisplayLayoutDimensions() { | ||||
|             return this.layoutDimensions | ||||
|                 && this.layoutDimensions[0] > 0 | ||||
|                 && this.layoutDimensions[1] > 0; | ||||
|         }, | ||||
|         layoutDimensionsStyle() { | ||||
|             const width = `${this.layoutDimensions[0]}px`; | ||||
|             const height = `${this.layoutDimensions[1]}px`; | ||||
|  | ||||
|             return { | ||||
|                 width, | ||||
|                 height | ||||
|             }; | ||||
|         }, | ||||
|         showMarquee() { | ||||
|             let selectionPath = this.selection[0]; | ||||
|             let singleSelectedLine = this.selection.length === 1 | ||||
| @@ -179,7 +198,13 @@ export default { | ||||
|             return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine; | ||||
|         } | ||||
|     }, | ||||
|     inject: ['openmct', 'options', 'objectPath'], | ||||
|     watch: { | ||||
|         isEditing(value) { | ||||
|             if (value) { | ||||
|                 this.showGrid = value; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) { | ||||
|             this.internalDomainObject = JSON.parse(JSON.stringify(obj)); | ||||
| @@ -798,6 +823,9 @@ export default { | ||||
|  | ||||
|             this.removeItem(selection); | ||||
|             this.initSelectIndex = this.layoutItems.length - 1; //restore selection | ||||
|         }, | ||||
|         toggleGrid() { | ||||
|             this.showGrid = !this.showGrid; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										34
									
								
								src/plugins/displayLayout/components/DisplayLayoutGrid.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/plugins/displayLayout/components/DisplayLayoutGrid.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="l-layout__grid-holder" | ||||
|     :class="{ 'c-grid': showGrid }" | ||||
| > | ||||
|     <div | ||||
|         v-if="gridSize[0] >= 3" | ||||
|         class="c-grid__x l-grid l-grid-x" | ||||
|         :style="[{ backgroundSize: gridSize[0] + 'px 100%' }]" | ||||
|     ></div> | ||||
|     <div | ||||
|         v-if="gridSize[1] >= 3" | ||||
|         class="c-grid__y l-grid l-grid-y" | ||||
|         :style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]" | ||||
|     ></div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     props: { | ||||
|         gridSize: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             validator: (arr) => arr && arr.length === 2 | ||||
|                 && arr.every(el => typeof el === 'number') | ||||
|         }, | ||||
|         showGrid: { | ||||
|             type: Boolean, | ||||
|             required: true | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -81,6 +81,7 @@ export default { | ||||
|         style() { | ||||
|             let backgroundImage = 'url(' + this.item.url + ')'; | ||||
|             let border = '1px solid ' + this.item.stroke; | ||||
|  | ||||
|             if (this.itemStyle) { | ||||
|                 if (this.itemStyle.imageUrl !== undefined) { | ||||
|                     backgroundImage = 'url(' + this.itemStyle.imageUrl + ')'; | ||||
|   | ||||
| @@ -35,6 +35,8 @@ | ||||
|         :object-path="currentObjectPath" | ||||
|         :has-frame="item.hasFrame" | ||||
|         :show-edit-view="false" | ||||
|         :layout-font-size="item.fontSize" | ||||
|         :layout-font="item.font" | ||||
|     /> | ||||
| </layout-frame> | ||||
| </template> | ||||
| @@ -73,6 +75,8 @@ export default { | ||||
|             y: position[1], | ||||
|             identifier: domainObject.identifier, | ||||
|             hasFrame: hasFrameByDefault(domainObject.type), | ||||
|             fontSize: 'default', | ||||
|             font: 'default', | ||||
|             viewKey | ||||
|         }; | ||||
|     }, | ||||
| @@ -138,14 +142,18 @@ export default { | ||||
|             this.domainObject = domainObject; | ||||
|             this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); | ||||
|             this.$nextTick(() => { | ||||
|                 let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                 childContext.item = domainObject; | ||||
|                 childContext.layoutItem = this.item; | ||||
|                 childContext.index = this.index; | ||||
|                 this.context = childContext; | ||||
|                 this.removeSelectable = this.openmct.selection.selectable( | ||||
|                     this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                 delete this.immediatelySelect; | ||||
|                 let reference = this.$refs.objectFrame; | ||||
|  | ||||
|                 if (reference) { | ||||
|                     let childContext = this.$refs.objectFrame.getSelectionContext(); | ||||
|                     childContext.item = domainObject; | ||||
|                     childContext.layoutItem = this.item; | ||||
|                     childContext.index = this.index; | ||||
|                     this.context = childContext; | ||||
|                     this.removeSelectable = this.openmct.selection.selectable( | ||||
|                         this.$el, this.context, this.immediatelySelect || this.initSelect); | ||||
|                     delete this.immediatelySelect; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -31,15 +31,14 @@ | ||||
|     <div | ||||
|         v-if="domainObject" | ||||
|         class="c-telemetry-view" | ||||
|         :class="{ | ||||
|             styleClass, | ||||
|             'is-missing': domainObject.status === 'missing' | ||||
|         }" | ||||
|         :class="[statusClass]" | ||||
|         :style="styleObject" | ||||
|         :data-font-size="item.fontSize" | ||||
|         :data-font="item.font" | ||||
|         @contextmenu.prevent="showContextMenu" | ||||
|     > | ||||
|         <div class="is-missing__indicator" | ||||
|              title="This item is missing" | ||||
|         <div class="is-status__indicator" | ||||
|              :title="`This item is ${status}`" | ||||
|         ></div> | ||||
|         <div | ||||
|             v-if="showLabel" | ||||
| @@ -72,12 +71,12 @@ | ||||
|  | ||||
| <script> | ||||
| import LayoutFrame from './LayoutFrame.vue'; | ||||
| import printj from 'printj'; | ||||
| import conditionalStylesMixin from "../mixins/objectStyles-mixin"; | ||||
| import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js'; | ||||
|  | ||||
| const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; | ||||
| const DEFAULT_POSITION = [1, 1]; | ||||
| const CONTEXT_MENU_ACTIONS = ['viewHistoricalData']; | ||||
| const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData']; | ||||
|  | ||||
| export default { | ||||
|     makeDefinition(openmct, gridSize, domainObject, position) { | ||||
| @@ -95,7 +94,8 @@ export default { | ||||
|             stroke: "", | ||||
|             fill: "", | ||||
|             color: "", | ||||
|             size: "13px" | ||||
|             fontSize: 'default', | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct', 'objectPath'], | ||||
| @@ -126,13 +126,18 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentObjectPath: undefined, | ||||
|             datum: undefined, | ||||
|             formats: undefined, | ||||
|             domainObject: undefined, | ||||
|             currentObjectPath: undefined | ||||
|             formats: undefined, | ||||
|             viewKey: `alphanumeric-format-${Math.random()}`, | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         statusClass() { | ||||
|             return (this.status) ? `is-status--${this.status}` : ''; | ||||
|         }, | ||||
|         showLabel() { | ||||
|             let displayMode = this.item.displayMode; | ||||
|  | ||||
| @@ -150,10 +155,15 @@ export default { | ||||
|             return unit; | ||||
|         }, | ||||
|         styleObject() { | ||||
|             return Object.assign({}, { | ||||
|                 fontSize: this.item.size | ||||
|             }, this.itemStyle); | ||||
|             let size; | ||||
|             //for legacy size support | ||||
|             if (!this.item.fontSize) { | ||||
|                 size = this.item.size; | ||||
|             } | ||||
|  | ||||
|             return Object.assign({}, { | ||||
|                 size | ||||
|             }, this.itemStyle); | ||||
|         }, | ||||
|         fieldName() { | ||||
|             return this.valueMetadata && this.valueMetadata.name; | ||||
| @@ -161,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() { | ||||
| @@ -169,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) { | ||||
| @@ -205,9 +215,13 @@ export default { | ||||
|         this.openmct.objects.get(this.item.identifier) | ||||
|             .then(this.setObject); | ||||
|         this.openmct.time.on("bounds", this.refreshData); | ||||
|  | ||||
|         this.status = this.openmct.status.get(this.item.identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.removeSubscription(); | ||||
|         this.removeStatusListener(); | ||||
|  | ||||
|         if (this.removeSelectable) { | ||||
|             this.removeSelectable(); | ||||
| @@ -216,6 +230,12 @@ export default { | ||||
|         this.openmct.time.off("bounds", this.refreshData); | ||||
|     }, | ||||
|     methods: { | ||||
|         formattedValueForCopy() { | ||||
|             const timeFormatterKey = this.openmct.time.timeSystem().key; | ||||
|             const timeFormatter = this.formats[timeFormatterKey]; | ||||
|  | ||||
|             return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`; | ||||
|         }, | ||||
|         requestHistoricalData() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             let options = { | ||||
| @@ -253,12 +273,26 @@ export default { | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|         getView() { | ||||
|             return { | ||||
|                 getViewContext: () => { | ||||
|                     return { | ||||
|                         viewHistoricalData: true, | ||||
|                         formattedValueForCopy: this.formattedValueForCopy | ||||
|                     }; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         setObject(domainObject) { | ||||
|             this.domainObject = domainObject; | ||||
|             this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|             this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); | ||||
|             this.formats = this.openmct.telemetry.getFormatMap(this.metadata); | ||||
|  | ||||
|             const valueMetadata = this.metadata.value(this.item.value); | ||||
|             this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format); | ||||
|  | ||||
|             this.requestHistoricalData(); | ||||
|             this.subscribeToObject(); | ||||
|  | ||||
| @@ -278,10 +312,37 @@ export default { | ||||
|             delete this.immediatelySelect; | ||||
|         }, | ||||
|         updateTelemetryFormat(format) { | ||||
|             this.customStringformatter.setFormat(format); | ||||
|  | ||||
|             this.$emit('formatChanged', this.item, format); | ||||
|         }, | ||||
|         showContextMenu(event) { | ||||
|             this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); | ||||
|         async getContextMenuActions() { | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier); | ||||
|             const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView()); | ||||
|             const actionsObject = actionCollection.getActionsObject(); | ||||
|  | ||||
|             let copyToNotebookAction = actionsObject.copyToNotebook; | ||||
|  | ||||
|             if (defaultNotebook) { | ||||
|                 const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`; | ||||
|                 copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`; | ||||
|             } else { | ||||
|                 actionsObject.copyToNotebook = undefined; | ||||
|                 delete actionsObject.copyToNotebook; | ||||
|             } | ||||
|  | ||||
|             return CONTEXT_MENU_ACTIONS.map(actionKey => { | ||||
|                 return actionsObject[actionKey]; | ||||
|             }).filter(action => action !== undefined); | ||||
|         }, | ||||
|         async showContextMenu(event) { | ||||
|             const contextMenuActions = await this.getContextMenuActions(); | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, contextMenuActions); | ||||
|         }, | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -29,7 +29,9 @@ | ||||
|     @endMove="() => $emit('endMove')" | ||||
| > | ||||
|     <div | ||||
|         class="c-text-view" | ||||
|         class="c-text-view u-style-receiver js-style-receiver" | ||||
|         :data-font-size="item.fontSize" | ||||
|         :data-font="item.font" | ||||
|         :class="[styleClass]" | ||||
|         :style="style" | ||||
|     > | ||||
| @@ -47,13 +49,14 @@ export default { | ||||
|         return { | ||||
|             fill: '', | ||||
|             stroke: '', | ||||
|             size: '13px', | ||||
|             color: '', | ||||
|             x: 1, | ||||
|             y: 1, | ||||
|             width: 10, | ||||
|             height: 5, | ||||
|             text: element.text | ||||
|             text: element.text, | ||||
|             fontSize: 'default', | ||||
|             font: 'default' | ||||
|         }; | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
| @@ -84,8 +87,14 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         style() { | ||||
|             let size; | ||||
|             //legacy size support | ||||
|             if (!this.item.fontSize) { | ||||
|                 size = this.item.size; | ||||
|             } | ||||
|  | ||||
|             return Object.assign({ | ||||
|                 fontSize: this.item.size | ||||
|                 size | ||||
|             }, this.itemStyle); | ||||
|         } | ||||
|     }, | ||||
|   | ||||
| @@ -17,10 +17,29 @@ | ||||
|     flex-direction: column; | ||||
|     overflow: auto; | ||||
|  | ||||
|     &__grid-holder { | ||||
|     &__grid-holder, | ||||
|     &__dimensions { | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     &__dimensions { | ||||
|         $b: 1px dashed $editDimensionsColor; | ||||
|         border-right: $b; | ||||
|         border-bottom: $b; | ||||
|         pointer-events: none; | ||||
|         position: absolute; | ||||
|  | ||||
|         &-vals { | ||||
|             $p: 2px; | ||||
|             color: $editDimensionsColor; | ||||
|             display: inline-block; | ||||
|             font-style: italic; | ||||
|             position: absolute; | ||||
|             bottom: $p; right: $p; | ||||
|             opacity: 0.7; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__frame { | ||||
|         position: absolute; | ||||
|     } | ||||
| @@ -34,6 +53,10 @@ | ||||
|             > .l-layout { | ||||
|                 background: $editUIGridColorBg; | ||||
|  | ||||
|                 > [class*="__dimensions"] { | ||||
|                     display: block; | ||||
|                 } | ||||
|  | ||||
|                 > [class*="__grid-holder"] { | ||||
|                     display: block; | ||||
|                 } | ||||
| @@ -42,12 +65,16 @@ | ||||
|     } | ||||
|  | ||||
|     .l-layout__frame { | ||||
|         &[s-selected], | ||||
|         &[s-selected]:not([multi-select="true"]), | ||||
|         &[s-selected-parent] { | ||||
|             // Display grid and allow edit marquee to display in nested layouts when editing | ||||
|             > * > * > .l-layout + .allow-editing { | ||||
|             > * > * > .l-layout.allow-editing { | ||||
|                 box-shadow: inset $editUIGridColorFg 0 0 2px 1px; | ||||
|  | ||||
|                 > [class*="__dimensions"] { | ||||
|                     display: block; | ||||
|                 } | ||||
|  | ||||
|                 > [class*='grid-holder'] { | ||||
|                     display: block; | ||||
|                 } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|         flex: 1 1 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|        // justify-content: center; | ||||
|         align-items: center; | ||||
|         overflow: hidden; | ||||
|         padding: $interiorMargin; | ||||
| @@ -27,14 +26,13 @@ | ||||
|         border: 1px solid transparent; | ||||
|     } | ||||
|  | ||||
|     @include isMissing($absPos: true); | ||||
|  | ||||
|     .is-missing__indicator { | ||||
|     .is-status__indicator { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|     } | ||||
|  | ||||
|     &.is-missing { | ||||
|     &[class*='is-status'] { | ||||
|         border: $borderMissing; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -27,6 +27,7 @@ export default { | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             objectStyle: undefined, | ||||
|             itemStyle: undefined, | ||||
|             styleClass: '' | ||||
|         }; | ||||
|   | ||||
| @@ -26,9 +26,12 @@ import objectUtils from 'objectUtils'; | ||||
| import DisplayLayoutType from './DisplayLayoutType.js'; | ||||
| import DisplayLayoutToolbar from './DisplayLayoutToolbar.js'; | ||||
| import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js'; | ||||
| import CopyToClipboardAction from './actions/CopyToClipboardAction'; | ||||
|  | ||||
| export default function DisplayLayoutPlugin(options) { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new CopyToClipboardAction(openmct)); | ||||
|  | ||||
|         openmct.objectViews.addProvider({ | ||||
|             key: 'layout.view', | ||||
|             canView: function (domainObject) { | ||||
| @@ -72,7 +75,8 @@ export default function DisplayLayoutPlugin(options) { | ||||
|                             duplicateItem: component && component.$refs.displayLayout.duplicateItem, | ||||
|                             switchViewType: component && component.$refs.displayLayout.switchViewType, | ||||
|                             mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews, | ||||
|                             mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots | ||||
|                             mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots, | ||||
|                             toggleGrid: component && component.$refs.displayLayout.toggleGrid | ||||
|                         }; | ||||
|                     }, | ||||
|                     onEditModeChange: function (isEditing) { | ||||
|   | ||||
| @@ -340,6 +340,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|         it('provides controls including separators', () => { | ||||
|             const displayLayoutToolbar = openmct.toolbars.get(selection); | ||||
|  | ||||
|             expect(displayLayoutToolbar.length).toBe(9); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
							
								
								
									
										156
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: "Folder 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 locked = child.locked ? child.locked : parent && parent.locked; | ||||
|  | ||||
|         if (locked) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return parentType | ||||
|             && parentType.definition.creatable | ||||
|             && Array.isArray(parent.composition); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										270
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 in the constructor. | ||||
|      * @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.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.getId(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} files.`, | ||||
|             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} files.`; | ||||
|  | ||||
|             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)) { | ||||
|             // Clone original object | ||||
|             let clone = this.cloneObjectModel(originalObject); | ||||
|  | ||||
|             // Get children, if any | ||||
|             let composeesCollection = this.openmct.composition.get(originalObject); | ||||
|             let composees; | ||||
|  | ||||
|             if (composeesCollection) { | ||||
|                 composees = await composeesCollection.load(); | ||||
|             } | ||||
|  | ||||
|             // Recursively duplicate children | ||||
|             return this.duplicateComposees(clone, composees); | ||||
|         } | ||||
|  | ||||
|         // Not creatable, creating a link, no need to iterate children | ||||
|         return originalObject; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update identifiers in a cloned object model (or part of | ||||
|      * a cloned object model) to reflect new identifiers after | ||||
|      * duplicating. | ||||
|      * @private | ||||
|      */ | ||||
|     rewriteIdentifiers(obj, idMap) { | ||||
|         function lookupValue(value) { | ||||
|             return (typeof value === 'string' && idMap[value]) || value; | ||||
|         } | ||||
|  | ||||
|         if (Array.isArray(obj)) { | ||||
|             obj.forEach((value, index) => { | ||||
|                 obj[index] = lookupValue(value); | ||||
|                 this.rewriteIdentifiers(obj[index], idMap); | ||||
|             }); | ||||
|         } else if (obj && typeof obj === 'object') { | ||||
|             Object.keys(obj).forEach((key) => { | ||||
|                 let value = obj[key]; | ||||
|                 obj[key] = lookupValue(value); | ||||
|                 if (idMap[key]) { | ||||
|                     delete obj[key]; | ||||
|                     obj[idMap[key]] = value; | ||||
|                 } | ||||
|  | ||||
|                 this.rewriteIdentifiers(value, idMap); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given an array of objects composed by a parent, clone them, then | ||||
|      * add them to the parent. | ||||
|      * @private | ||||
|      * @returns {*} | ||||
|      */ | ||||
|     async duplicateComposees(clonedParent, composees = []) { | ||||
|         let idMap = {}; | ||||
|  | ||||
|         let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => { | ||||
|             await previousPromise; | ||||
|             let clonedComposee = await this.duplicateObject(nextComposee); | ||||
|             idMap[this.getId(nextComposee)] = this.getId(clonedComposee); | ||||
|             await this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee); | ||||
|  | ||||
|             return; | ||||
|         }, Promise.resolve()); | ||||
|  | ||||
|         await allComposeesDuplicated; | ||||
|  | ||||
|         this.rewriteIdentifiers(clonedParent, idMap); | ||||
|         this.clones.push(clonedParent); | ||||
|  | ||||
|         return clonedParent; | ||||
|     } | ||||
|  | ||||
|     async composeChild(child, parent, setLocation) { | ||||
|         const PERSIST_BOOL = false; | ||||
|         let parentComposition = this.openmct.composition.get(parent); | ||||
|         await parentComposition.load(); | ||||
|         parentComposition.add(child, PERSIST_BOOL); | ||||
|  | ||||
|         //If a location is not specified, set it. | ||||
|         if (setLocation && child.location === undefined) { | ||||
|             let parentKeyString = this.getId(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: domainObject.identifier.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; | ||||
|     } | ||||
|  | ||||
|     getId(domainObject) { | ||||
|         return this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|     } | ||||
|  | ||||
|     isCreatable(domainObject) { | ||||
|         return this.getTypeDefinition(domainObject, 'creatable'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2019, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import DuplicateAction from "./DuplicateAction"; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.actions.register(new DuplicateAction(openmct)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										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); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -3,20 +3,26 @@ | ||||
|     @include userSelectNone(); | ||||
|     background: $colorFilterBg; | ||||
|     color: $colorFilterFg; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     font-size: 0.9em; | ||||
|     margin-top: $interiorMarginSm; | ||||
|     padding: 2px; | ||||
|     text-transform: uppercase; | ||||
|  | ||||
|     &:before { | ||||
|         font-family: symbolsfont-12px; | ||||
|         content: $glyph-icon-filter; | ||||
|         display: block; | ||||
|         font-size: 12px; | ||||
|         margin-right: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     &--mixed { | ||||
|         .c-filter-indication__mixed { | ||||
|             font-style: italic; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__label { | ||||
|         + .c-filter-indication__label { | ||||
|             &:before { | ||||
|                 content: ', '; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-filter-tree-item { | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| <template> | ||||
| <a | ||||
|     class="l-grid-view__item c-grid-item" | ||||
|     :class="{ | ||||
|     :class="[{ | ||||
|         'is-alias': item.isAlias === true, | ||||
|         'is-missing': item.model.status === 'missing', | ||||
|         'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 | ||||
|     }" | ||||
|     }, statusClass]" | ||||
|     :href="objectLink" | ||||
| > | ||||
|     <div | ||||
| @@ -27,8 +26,8 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="c-grid-item__controls"> | ||||
|         <div class="is-missing__indicator" | ||||
|              title="This item is missing" | ||||
|         <div class="is-status__indicator" | ||||
|              :title="`This item is ${status}`" | ||||
|         ></div> | ||||
|         <div | ||||
|             class="icon-people" | ||||
| @@ -46,9 +45,10 @@ | ||||
| <script> | ||||
| import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
| import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink], | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -8,18 +8,18 @@ | ||||
|         <a | ||||
|             ref="objectLink" | ||||
|             class="c-object-label" | ||||
|             :class="{ 'is-missing': item.model.status === 'missing' }" | ||||
|             :class="[statusClass]" | ||||
|             :href="objectLink" | ||||
|         > | ||||
|             <div | ||||
|                 class="c-object-label__type-icon c-list-item__type-icon" | ||||
|                 class="c-object-label__type-icon c-list-item__name__type-icon" | ||||
|                 :class="item.type.cssClass" | ||||
|             > | ||||
|                 <span class="is-missing__indicator" | ||||
|                       title="This item is missing" | ||||
|                 <span class="is-status__indicator" | ||||
|                       :title="`This item is ${status}`" | ||||
|                 ></span> | ||||
|             </div> | ||||
|             <div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div> | ||||
|             <div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div> | ||||
|         </a> | ||||
|     </td> | ||||
|     <td class="c-list-item__type"> | ||||
| @@ -39,9 +39,10 @@ | ||||
| import moment from 'moment'; | ||||
| import contextMenuGesture from '../../../ui/mixins/context-menu-gesture'; | ||||
| import objectLink from '../../../ui/mixins/object-link'; | ||||
| import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink], | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -11,6 +11,8 @@ | ||||
|  | ||||
|     body.desktop & { | ||||
|         flex-flow: row wrap; | ||||
|         align-content: flex-start; | ||||
|  | ||||
|         &__item { | ||||
|             height: $gridItemDesk; | ||||
|             width: $gridItemDesk; | ||||
| @@ -41,9 +43,20 @@ | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.is-missing { | ||||
|         @include isMissing(); | ||||
|     &.is-status--notebook-default { | ||||
|         .is-status__indicator { | ||||
|             display: block; | ||||
|  | ||||
|             &:before { | ||||
|                 color: $colorFilter; | ||||
|                 content: $glyph-icon-notebook-page; | ||||
|                 font-family: symbolsfont; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &[class*='is-status--missing'], | ||||
|     &[class*='is-status--suspect']{ | ||||
|         [class*='__type-icon'], | ||||
|         [class*='__details'] { | ||||
|             opacity: $opacityMissing; | ||||
|   | ||||
| @@ -1,11 +1,19 @@ | ||||
| /******************************* LIST ITEM */ | ||||
| .c-list-item { | ||||
|     &__type-icon { | ||||
|     &__name__type-icon { | ||||
|         color: $colorItemTreeIcon; | ||||
|     } | ||||
|  | ||||
|     &__name { | ||||
|     &__name__name { | ||||
|         @include ellipsize(); | ||||
|  | ||||
|         a & { | ||||
|             color: $colorItemFg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &:not(.c-list-item__name) { | ||||
|         color: $colorItemFgDetails; | ||||
|     } | ||||
|  | ||||
|     &.is-alias { | ||||
|   | ||||
| @@ -28,9 +28,5 @@ | ||||
|         padding-top: $p; | ||||
|         padding-bottom: $p; | ||||
|         width: 25%; | ||||
|  | ||||
|         &:not(.c-list-item__name) { | ||||
|             color: $colorItemFgDetails; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/plugins/folderView/components/status-listener.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/plugins/folderView/components/status-listener.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         statusClass() { | ||||
|             return (this.status) ? `is-status--${this.status}` : ''; | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             status: '' | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         setStatus(status) { | ||||
|             this.status = status; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         let identifier = this.item.model.identifier; | ||||
|  | ||||
|         this.status = this.openmct.status.get(identifier); | ||||
|         this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.removeStatusListener(); | ||||
|     } | ||||
| }; | ||||
| @@ -25,6 +25,8 @@ export default class GoToOriginalAction { | ||||
|         this.name = 'Go To Original'; | ||||
|         this.key = 'goToOriginal'; | ||||
|         this.description = 'Go to the original unlinked instance of this object'; | ||||
|         this.group = 'action'; | ||||
|         this.priority = 4; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|     } | ||||
|   | ||||
| @@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction'; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.contextMenu.registerAction(new GoToOriginalAction(openmct)); | ||||
|         openmct.actions.register(new GoToOriginalAction(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -1,116 +1,224 @@ | ||||
| <template> | ||||
| <div class="c-imagery"> | ||||
| <div | ||||
|     tabindex="0" | ||||
|     class="c-imagery" | ||||
|     @keyup="arrowUpHandler" | ||||
|     @keydown="arrowDownHandler" | ||||
|     @mouseover="focusElement" | ||||
| > | ||||
|     <div class="c-imagery__main-image-wrapper has-local-controls"> | ||||
|         <div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc"> | ||||
|             <span class="holder flex-elem grows c-imagery__lc__sliders"> | ||||
|                 <input v-model="filters.brightness" | ||||
|                        class="icon-brightness" | ||||
|                        type="range" | ||||
|                        min="0" | ||||
|                        max="500" | ||||
|                 > | ||||
|                 <input v-model="filters.contrast" | ||||
|                        class="icon-contrast" | ||||
|                        type="range" | ||||
|                        min="0" | ||||
|                        max="500" | ||||
|                 > | ||||
|         <div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls"> | ||||
|             <span class="c-image-controls__sliders" | ||||
|                   draggable="true" | ||||
|                   @dragstart="startDrag" | ||||
|             > | ||||
|                 <div class="c-image-controls__slider-wrapper icon-brightness"> | ||||
|                     <input v-model="filters.brightness" | ||||
|                            type="range" | ||||
|                            min="0" | ||||
|                            max="500" | ||||
|                     > | ||||
|                 </div> | ||||
|                 <div class="c-image-controls__slider-wrapper icon-contrast"> | ||||
|                     <input v-model="filters.contrast" | ||||
|                            type="range" | ||||
|                            min="0" | ||||
|                            max="500" | ||||
|                     > | ||||
|                 </div> | ||||
|             </span> | ||||
|             <span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn"> | ||||
|             <span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset"> | ||||
|                 <a class="s-icon-button icon-reset t-btn-reset" | ||||
|                    @click="filters={brightness: 100, contrast: 100}" | ||||
|                 ></a> | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="main-image s-image-main c-imagery__main-image has-local-controls" | ||||
|              :class="{'paused unnsynced': paused(),'stale':false }" | ||||
|              :style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none', | ||||
|                       'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}" | ||||
|         <div class="c-imagery__main-image__bg" | ||||
|              :class="{'paused unnsynced': isPaused,'stale':false }" | ||||
|         > | ||||
|             <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> | ||||
|                 <button class="c-nav c-nav--prev" | ||||
|                         title="Previous image" | ||||
|                         :disabled="isPrevDisabled()" | ||||
|                         @click="prevImage()" | ||||
|                 ></button> | ||||
|                 <button class="c-nav c-nav--next" | ||||
|                         title="Next image" | ||||
|                         :disabled="isNextDisabled()" | ||||
|                         @click="nextImage()" | ||||
|                 ></button> | ||||
|             </div> | ||||
|             <div class="c-imagery__main-image__image" | ||||
|                  :style="{ | ||||
|                      'background-image': imageUrl ? `url(${imageUrl})` : 'none', | ||||
|                      'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)` | ||||
|                  }" | ||||
|                  :data-openmct-image-timestamp="time" | ||||
|                  :data-openmct-object-keystring="keyString" | ||||
|             ></div> | ||||
|         </div> | ||||
|         <div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons"> | ||||
|             <button class="c-nav c-nav--prev" | ||||
|                     title="Previous image" | ||||
|                     :disabled="isPrevDisabled" | ||||
|                     @click="prevImage()" | ||||
|             ></button> | ||||
|             <button class="c-nav c-nav--next" | ||||
|                     title="Next image" | ||||
|                     :disabled="isNextDisabled" | ||||
|                     @click="nextImage()" | ||||
|             ></button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="c-imagery__control-bar"> | ||||
|             <div class="c-imagery__timestamp">{{ getTime() }}</div> | ||||
|             <div class="h-local-controls flex-elem"> | ||||
|             <div class="c-imagery__time"> | ||||
|                 <div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div> | ||||
|                 <div | ||||
|                     v-if="canTrackDuration" | ||||
|                     :class="{'c-imagery--new': isImageNew && !refreshCSS}" | ||||
|                     class="c-imagery__age icon-timer" | ||||
|                 >{{ formattedDuration }}</div> | ||||
|             </div> | ||||
|             <div class="h-local-controls"> | ||||
|                 <button | ||||
|                     class="c-button icon-pause pause-play" | ||||
|                     :class="{'is-paused': paused()}" | ||||
|                     @click="paused(!paused(), true)" | ||||
|                     :class="{'is-paused': isPaused}" | ||||
|                     @click="paused(!isPaused, 'button')" | ||||
|                 ></button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div ref="thumbsWrapper" | ||||
|          class="c-imagery__thumbs-wrapper" | ||||
|          :class="{'is-paused': paused()}" | ||||
|          :class="{'is-paused': isPaused}" | ||||
|          @scroll="handleScroll" | ||||
|     > | ||||
|         <div v-for="(imageData, index) in imageHistory" | ||||
|              :key="index" | ||||
|         <div v-for="(datum, index) in imageHistory" | ||||
|              :key="datum.url" | ||||
|              class="c-imagery__thumb c-thumb" | ||||
|              :class="{selected: imageData.selected}" | ||||
|              @click="setSelectedImage(imageData)" | ||||
|              :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|              @click="setFocusedImage(index, thumbnailClick)" | ||||
|         > | ||||
|             <img class="c-thumb__image" | ||||
|                  :src="getImageUrl(imageData)" | ||||
|                  :src="formatImageUrl(datum)" | ||||
|             > | ||||
|             <div class="c-thumb__timestamp">{{ getTime(imageData) }}</div> | ||||
|             <div class="c-thumb__timestamp">{{ formatTime(datum) }}</div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'moment'; | ||||
|  | ||||
| const DEFAULT_DURATION_FORMATTER = 'duration'; | ||||
| const REFRESH_CSS_MS = 500; | ||||
| const DURATION_TRACK_MS = 1000; | ||||
| const ARROW_DOWN_DELAY_CHECK_MS = 400; | ||||
| const ARROW_SCROLL_RATE_MS = 100; | ||||
| const THUMBNAIL_CLICKED = true; | ||||
|  | ||||
| const ONE_MINUTE = 60 * 1000; | ||||
| const FIVE_MINUTES = 5 * ONE_MINUTE; | ||||
| const ONE_HOUR = ONE_MINUTE * 60; | ||||
| const EIGHT_HOURS = 8 * ONE_HOUR; | ||||
| const TWENTYFOUR_HOURS = EIGHT_HOURS * 3; | ||||
|  | ||||
| const ARROW_RIGHT = 39; | ||||
| const ARROW_LEFT = 37; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         let timeSystem = this.openmct.time.timeSystem(); | ||||
|  | ||||
|         return { | ||||
|             autoScroll: true, | ||||
|             durationFormatter: undefined, | ||||
|             filters: { | ||||
|                 brightness: 100, | ||||
|                 contrast: 100 | ||||
|             }, | ||||
|             image: { | ||||
|                 selected: '' | ||||
|             }, | ||||
|             imageFormat: '', | ||||
|             imageHistory: [], | ||||
|             imageUrl: '', | ||||
|             thumbnailClick: THUMBNAIL_CLICKED, | ||||
|             isPaused: false, | ||||
|             metadata: {}, | ||||
|             requestCount: 0, | ||||
|             timeFormat: '' | ||||
|             timeSystem: timeSystem, | ||||
|             timeFormatter: undefined, | ||||
|             refreshCSS: false, | ||||
|             keyString: undefined, | ||||
|             focusedImageIndex: undefined, | ||||
|             numericDuration: undefined | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         bounds() { | ||||
|             return this.openmct.time.bounds(); | ||||
|         time() { | ||||
|             return this.formatTime(this.focusedImage); | ||||
|         }, | ||||
|         imageUrl() { | ||||
|             return this.formatImageUrl(this.focusedImage); | ||||
|         }, | ||||
|         isImageNew() { | ||||
|             let cutoff = FIVE_MINUTES; | ||||
|             let age = this.numericDuration; | ||||
|  | ||||
|             return age < cutoff && !this.refreshCSS; | ||||
|         }, | ||||
|         canTrackDuration() { | ||||
|             return this.openmct.time.clock() && this.timeSystem.isUTCBased; | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
|  | ||||
|             if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) { | ||||
|                 disabled = true; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         }, | ||||
|         isPrevDisabled() { | ||||
|             let disabled = false; | ||||
|  | ||||
|             if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) { | ||||
|                 disabled = true; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         }, | ||||
|         focusedImage() { | ||||
|             return this.imageHistory[this.focusedImageIndex]; | ||||
|         }, | ||||
|         parsedSelectedTime() { | ||||
|             return this.parseTime(this.focusedImage); | ||||
|         }, | ||||
|         formattedDuration() { | ||||
|             let result = 'N/A'; | ||||
|             let negativeAge = -1; | ||||
|  | ||||
|             if (this.numericDuration > TWENTYFOUR_HOURS) { | ||||
|                 negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS); | ||||
|                 result = moment.duration(negativeAge, 'days').humanize(true); | ||||
|             } else if (this.numericDuration > EIGHT_HOURS) { | ||||
|                 negativeAge *= (this.numericDuration / ONE_HOUR); | ||||
|                 result = moment.duration(negativeAge, 'hours').humanize(true); | ||||
|             } else if (this.durationFormatter) { | ||||
|                 result = this.durationFormatter.format(this.numericDuration); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         focusedImageIndex() { | ||||
|             this.trackDuration(); | ||||
|             this.resetAgeCSS(); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         // set | ||||
|         this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); | ||||
|         // initialize | ||||
|         this.timeKey = this.openmct.time.timeSystem().key; | ||||
|         this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); | ||||
|         // listen | ||||
|         this.openmct.time.on('bounds', this.boundsChange); | ||||
|         this.openmct.time.on('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.on('clock', this.clockChange); | ||||
|  | ||||
|         // set | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|         this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]); | ||||
|  | ||||
|         // initialize | ||||
|         this.timeKey = this.timeSystem.key; | ||||
|         this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|  | ||||
|         // kickoff | ||||
|         this.subscribe(); | ||||
|         this.requestHistory(); | ||||
| @@ -124,38 +232,55 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.stopDurationTracking(); | ||||
|         this.openmct.time.off('bounds', this.boundsChange); | ||||
|         this.openmct.time.off('timeSystem', this.timeSystemChange); | ||||
|         this.openmct.time.off('clock', this.clockChange); | ||||
|     }, | ||||
|     methods: { | ||||
|         focusElement() { | ||||
|             this.$el.focus(); | ||||
|         }, | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             const datumTime = this.timeFormat.format(datum); | ||||
|             const datumURL = this.imageFormat.format(datum); | ||||
|             const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]); | ||||
|             const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]); | ||||
|             const datumURL = this.formatImageUrl(datum); | ||||
|             const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); | ||||
|  | ||||
|             // datum is not valid if it matches the last datum in history, | ||||
|             // or it is before the last datum in the history | ||||
|             const datumTimeCheck = this.timeFormat.parse(datum); | ||||
|             const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL); | ||||
|             const datumTimeCheck = this.parseTime(datum); | ||||
|             const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); | ||||
|             const isStale = datumTimeCheck < historyTimeCheck; | ||||
|  | ||||
|             return matchesLast || isStale; | ||||
|         }, | ||||
|         getImageUrl(datum) { | ||||
|             return datum | ||||
|                 ? this.imageFormat.format(datum) | ||||
|                 : this.imageUrl; | ||||
|         formatImageUrl(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.imageFormatter.format(datum); | ||||
|         }, | ||||
|         getTime(datum) { | ||||
|             return datum | ||||
|                 ? this.timeFormat.format(datum) | ||||
|                 : this.time; | ||||
|         formatTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let dateTimeStr = this.timeFormatter.format(datum); | ||||
|  | ||||
|             // Replace ISO "T" with a space to allow wrapping | ||||
|             return dateTimeStr.replace("T", " "); | ||||
|         }, | ||||
|         parseTime(datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             return this.timeFormatter.parse(datum); | ||||
|         }, | ||||
|         handleScroll() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
| @@ -168,26 +293,35 @@ export default { | ||||
|                     || (scrollHeight - scrollTop) > 2 * clientHeight; | ||||
|             this.autoScroll = !disableScroll; | ||||
|         }, | ||||
|         paused(state, button = false) { | ||||
|             if (arguments.length > 0 && state !== this.isPaused) { | ||||
|                 this.unselectAllImages(); | ||||
|                 this.isPaused = state; | ||||
|                 if (state === true && button) { | ||||
|                     // If we are pausing, select the latest image in imageHistory | ||||
|                     this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]); | ||||
|                 } | ||||
|         paused(state, type) { | ||||
|  | ||||
|                 if (this.nextDatum) { | ||||
|                     this.updateValues(this.nextDatum); | ||||
|                     delete this.nextDatum; | ||||
|                 } else { | ||||
|                     this.updateValues(this.imageHistory[this.imageHistory.length - 1]); | ||||
|                 } | ||||
|             this.isPaused = state; | ||||
|  | ||||
|                 this.autoScroll = true; | ||||
|             if (type === 'button') { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|             } | ||||
|  | ||||
|             return this.isPaused; | ||||
|             if (this.nextImageIndex) { | ||||
|                 this.setFocusedImage(this.nextImageIndex); | ||||
|                 delete this.nextImageIndex; | ||||
|             } | ||||
|  | ||||
|             this.autoScroll = true; | ||||
|         }, | ||||
|         scrollToFocused() { | ||||
|             const thumbsWrapper = this.$refs.thumbsWrapper; | ||||
|             if (!thumbsWrapper) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let domThumb = thumbsWrapper.children[this.focusedImageIndex]; | ||||
|  | ||||
|             if (domThumb) { | ||||
|                 domThumb.scrollIntoView({ | ||||
|                     behavior: 'smooth', | ||||
|                     block: 'center' | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         scrollToRight() { | ||||
|             if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) { | ||||
| @@ -201,22 +335,17 @@ export default { | ||||
|  | ||||
|             setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0); | ||||
|         }, | ||||
|         setSelectedImage(image) { | ||||
|             // If we are paused and the current image IS selected, unpause | ||||
|             // Otherwise, set current image and pause | ||||
|             if (!image) { | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             if (this.isPaused && !thumbnailClick) { | ||||
|                 this.nextImageIndex = index; | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.isPaused && image.selected) { | ||||
|                 this.paused(false); | ||||
|                 this.unselectAllImages(); | ||||
|             } else { | ||||
|                 this.imageUrl = this.getImageUrl(image); | ||||
|                 this.time = this.getTime(image); | ||||
|             this.focusedImageIndex = index; | ||||
|  | ||||
|             if (thumbnailClick && !this.isPaused) { | ||||
|                 this.paused(true); | ||||
|                 this.unselectAllImages(); | ||||
|                 image.selected = true; | ||||
|             } | ||||
|         }, | ||||
|         boundsChange(bounds, isTick) { | ||||
| @@ -224,98 +353,162 @@ export default { | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         requestHistory() { | ||||
|             const requestId = ++this.requestCount; | ||||
|         async requestHistory() { | ||||
|             let bounds = this.openmct.time.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             this.imageHistory = []; | ||||
|             this.openmct.telemetry | ||||
|                 .request(this.domainObject, this.bounds) | ||||
|                 .then((values = []) => { | ||||
|                     if (this.requestCount === requestId) { | ||||
|                         // add each image to the history | ||||
|                         // update values for the very last image (set current image time and url) | ||||
|                         values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1)); | ||||
|                     } | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
|  | ||||
|             if (this.requestCount === requestId) { | ||||
|                 data.forEach((datum, index) => { | ||||
|                     this.updateHistory(datum, index === data.length - 1); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         timeSystemChange(system) { | ||||
|             // reset timesystem dependent variables | ||||
|             this.timeKey = system.key; | ||||
|             this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey)); | ||||
|             this.timeSystem = this.openmct.time.timeSystem(); | ||||
|             this.timeKey = this.timeSystem.key; | ||||
|             this.timeFormatter = this.getFormatter(this.timeKey); | ||||
|             this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         clockChange(clock) { | ||||
|             this.trackDuration(); | ||||
|         }, | ||||
|         subscribe() { | ||||
|             this.unsubscribe = this.openmct.telemetry | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.timeFormat.parse(datum); | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.openmct.time.bounds(); | ||||
|  | ||||
|                     if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) { | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         this.updateHistory(datum); | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
|         unselectAllImages() { | ||||
|             this.imageHistory.forEach(image => image.selected = false); | ||||
|         }, | ||||
|         updateHistory(datum, updateValues = true) { | ||||
|         updateHistory(datum, setFocused = true) { | ||||
|             if (this.datumIsNotValid(datum)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.imageHistory.push(datum); | ||||
|  | ||||
|             if (updateValues) { | ||||
|                 this.updateValues(datum); | ||||
|             if (setFocused) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 1); | ||||
|             } | ||||
|         }, | ||||
|         updateValues(datum) { | ||||
|             if (this.isPaused) { | ||||
|                 this.nextDatum = datum; | ||||
|         getFormatter(key) { | ||||
|             let metadataValue = this.metadata.value(key) || { format: key }; | ||||
|             let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|             return valueFormatter; | ||||
|         }, | ||||
|         trackDuration() { | ||||
|             if (this.canTrackDuration) { | ||||
|                 this.stopDurationTracking(); | ||||
|                 this.updateDuration(); | ||||
|                 this.durationTracker = window.setInterval( | ||||
|                     this.updateDuration, DURATION_TRACK_MS | ||||
|                 ); | ||||
|             } else { | ||||
|                 this.stopDurationTracking(); | ||||
|             } | ||||
|         }, | ||||
|         stopDurationTracking() { | ||||
|             window.clearInterval(this.durationTracker); | ||||
|         }, | ||||
|         updateDuration() { | ||||
|             let currentTime = this.openmct.time.clock().currentValue(); | ||||
|             this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
|             this.refreshCSS = true; | ||||
|             // unable to make this work with nextTick | ||||
|             setTimeout(() => { | ||||
|                 this.refreshCSS = false; | ||||
|             }, REFRESH_CSS_MS); | ||||
|         }, | ||||
|         nextImage() { | ||||
|             if (this.isNextDisabled) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.time = this.timeFormat.format(datum); | ||||
|             this.imageUrl = this.imageFormat.format(datum); | ||||
|         }, | ||||
|         selectedImageIndex() { | ||||
|             return this.imageHistory.findIndex(image => image.selected); | ||||
|         }, | ||||
|         setSelectedByIndex(index) { | ||||
|             this.setSelectedImage(this.imageHistory[index]); | ||||
|         }, | ||||
|         nextImage() { | ||||
|             let index = this.selectedImageIndex(); | ||||
|             this.setSelectedByIndex(++index); | ||||
|             let index = this.focusedImageIndex; | ||||
|  | ||||
|             this.setFocusedImage(++index, THUMBNAIL_CLICKED); | ||||
|             if (index === this.imageHistory.length - 1) { | ||||
|                 this.paused(false); | ||||
|             } | ||||
|         }, | ||||
|         prevImage() { | ||||
|             let index = this.selectedImageIndex(); | ||||
|             if (index === -1) { | ||||
|                 this.setSelectedByIndex(this.imageHistory.length - 2); | ||||
|             if (this.isPrevDisabled) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let index = this.focusedImageIndex; | ||||
|  | ||||
|             if (index === this.imageHistory.length - 1) { | ||||
|                 this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED); | ||||
|             } else { | ||||
|                 this.setSelectedByIndex(--index); | ||||
|                 this.setFocusedImage(--index, THUMBNAIL_CLICKED); | ||||
|             } | ||||
|         }, | ||||
|         isNextDisabled() { | ||||
|             let disabled = false; | ||||
|             let index = this.selectedImageIndex(); | ||||
|  | ||||
|             if (index === -1 || index === this.imageHistory.length - 1) { | ||||
|                 disabled = true; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|         startDrag(e) { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|         }, | ||||
|         isPrevDisabled() { | ||||
|             let disabled = false; | ||||
|             let index = this.selectedImageIndex(); | ||||
|         arrowDownHandler(event) { | ||||
|             let key = event.keyCode; | ||||
|  | ||||
|             if (index === 0 || this.imageHistory.length < 2) { | ||||
|                 disabled = true; | ||||
|             if (this.isLeftOrRightArrowKey(key)) { | ||||
|                 this.arrowDown = true; | ||||
|                 window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|                 this.arrowDownDelayTimeout = window.setTimeout(() => { | ||||
|                     this.arrowKeyScroll(this.directionByKey(key)); | ||||
|                 }, ARROW_DOWN_DELAY_CHECK_MS); | ||||
|             } | ||||
|         }, | ||||
|         arrowUpHandler(event) { | ||||
|             let key = event.keyCode; | ||||
|  | ||||
|             window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|  | ||||
|             if (this.isLeftOrRightArrowKey(key)) { | ||||
|                 this.arrowDown = false; | ||||
|                 let direction = this.directionByKey(key); | ||||
|                 this[direction + 'Image'](); | ||||
|             } | ||||
|         }, | ||||
|         arrowKeyScroll(direction) { | ||||
|             if (this.arrowDown) { | ||||
|                 this.arrowKeyScrolling = true; | ||||
|                 this[direction + 'Image'](); | ||||
|                 setTimeout(() => { | ||||
|                     this.arrowKeyScroll(direction); | ||||
|                 }, ARROW_SCROLL_RATE_MS); | ||||
|             } else { | ||||
|                 window.clearTimeout(this.arrowDownDelayTimeout); | ||||
|                 this.arrowKeyScrolling = false; | ||||
|                 this.scrollToFocused(); | ||||
|             } | ||||
|         }, | ||||
|         directionByKey(keyCode) { | ||||
|             let direction; | ||||
|  | ||||
|             if (keyCode === ARROW_LEFT) { | ||||
|                 direction = 'prev'; | ||||
|             } | ||||
|  | ||||
|             return disabled; | ||||
|             if (keyCode === ARROW_RIGHT) { | ||||
|                 direction = 'next'; | ||||
|             } | ||||
|  | ||||
|             return direction; | ||||
|         }, | ||||
|         isLeftOrRightArrowKey(keyCode) { | ||||
|             return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| .c-imagery { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex: 1 1 auto; | ||||
|     overflow: hidden; | ||||
|     height: 100%; | ||||
|  | ||||
|     &:focus { | ||||
|         outline: none; | ||||
|     } | ||||
|  | ||||
|     > * + * { | ||||
|         margin-top: $interiorMargin; | ||||
| @@ -15,24 +19,75 @@ | ||||
|     } | ||||
|  | ||||
|     &__main-image { | ||||
|         background-position: center; | ||||
|         background-repeat: no-repeat; | ||||
|         background-size: contain; | ||||
|         height: 100%; | ||||
|         &__bg { | ||||
|             background-color: $colorPlotBg; | ||||
|             border: 1px solid transparent; | ||||
|             flex: 1 1 auto; | ||||
|  | ||||
|         &.unnsynced{ | ||||
|             @include sUnsynced(); | ||||
|             &.unnsynced{ | ||||
|                 @include sUnsynced(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &__image { | ||||
|             @include abs(); // Safari fix | ||||
|             background-position: center; | ||||
|             background-repeat: no-repeat; | ||||
|             background-size: contain; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__control-bar { | ||||
|         padding: 5px 0 0 0; | ||||
|     &__control-bar, | ||||
|     &__time { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         align-items: baseline; | ||||
|  | ||||
|         > * + * { | ||||
|             margin-left: $interiorMarginSm; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     &__control-bar { | ||||
|         margin-top: 2px; | ||||
|         padding: $interiorMarginSm 0; | ||||
|         justify-content: space-between; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     &__time { | ||||
|         flex: 0 1 auto; | ||||
|         overflow: hidden; | ||||
|     } | ||||
|  | ||||
|     &__timestamp, | ||||
|     &__age { | ||||
|         @include ellipsize(); | ||||
|         flex: 0 1 auto; | ||||
|     } | ||||
|  | ||||
|     &__timestamp { | ||||
|         flex: 1 1 auto; | ||||
|         flex-shrink: 10; | ||||
|     } | ||||
|  | ||||
|     &__age { | ||||
|         border-radius: $controlCr; | ||||
|         display: flex; | ||||
|         flex-shrink: 0; | ||||
|         align-items: baseline; | ||||
|         padding: 1px $interiorMarginSm; | ||||
|  | ||||
|         &:before { | ||||
|             opacity: 0.5; | ||||
|             margin-right: $interiorMarginSm; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &--new { | ||||
|         // New imagery | ||||
|         $bgColor: $colorOk; | ||||
|         background: rgba($bgColor, 0.5); | ||||
|         @include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0)); | ||||
|     } | ||||
|  | ||||
|     &__thumbs-wrapper { | ||||
| @@ -91,11 +146,6 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .s-image-main { | ||||
|     background-color: $colorPlotBg; | ||||
|     border: 1px solid transparent; | ||||
| } | ||||
|  | ||||
| /*************************************** IMAGERY LOCAL CONTROLS*/ | ||||
| .c-imagery { | ||||
|     .h-local-controls--overlay-content { | ||||
| @@ -105,7 +155,7 @@ | ||||
|         background: $colorLocalControlOvrBg; | ||||
|         border-radius: $basicCr; | ||||
|         max-width: 200px; | ||||
|         min-width: 100px; | ||||
|         min-width: 70px; | ||||
|         width: 35%; | ||||
|         align-items: center; | ||||
|         padding: $interiorMargin $interiorMarginLg; | ||||
| @@ -126,6 +176,7 @@ | ||||
|     &__lc { | ||||
|         &__reset-btn { | ||||
|             $bc: $scrollbarTrackColorBg; | ||||
|  | ||||
|             &:before, | ||||
|             &:after { | ||||
|                 border-right: 1px solid $bc; | ||||
| @@ -148,9 +199,51 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-image-controls { | ||||
|     // Brightness/contrast | ||||
|  | ||||
|     &__controls { | ||||
|         // Sliders and reset element | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         margin-right: $interiorMargin; // Need some extra space due to proximity to close button | ||||
|     } | ||||
|  | ||||
|     &__sliders { | ||||
|         display: flex; | ||||
|         flex: 1 1 auto; | ||||
|         flex-direction: column; | ||||
|  | ||||
|         > * + * { | ||||
|             margin-top: 11px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__slider-wrapper { | ||||
|         // A wrapper is needed to add the type icon to left of each range input | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|  | ||||
|         &:before { | ||||
|             color: rgba($colorMenuFg, 0.5); | ||||
|             margin-right: $interiorMarginSm; | ||||
|         } | ||||
|  | ||||
|         input[type='range'] { | ||||
|             width: 100px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__btn-reset { | ||||
|         flex: 0 0 auto; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /*************************************** BUTTONS */ | ||||
| .c-button.pause-play { | ||||
|     // Pause icon set by default in markup | ||||
|     justify-self: end; | ||||
|  | ||||
|     &.is-paused { | ||||
|         background: $colorPausedBg !important; | ||||
|         color: $colorPausedFg; | ||||
| @@ -162,14 +255,13 @@ | ||||
| } | ||||
|  | ||||
| .c-imagery__prev-next-buttons { | ||||
|     //background: rgba(deeppink, 0.2); | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     justify-content: space-between; | ||||
|     pointer-events: none; | ||||
|     position: absolute; | ||||
|     top: 50%; | ||||
|     transform: translateY(-50%); | ||||
|     transform: translateY(-75%); | ||||
|  | ||||
|     .c-nav { | ||||
|         pointer-events: all; | ||||
|   | ||||
| @@ -28,6 +28,8 @@ export default class NewFolderAction { | ||||
|         this.key = 'newFolder'; | ||||
|         this.description = 'Create a new folder'; | ||||
|         this.cssClass = 'icon-folder-new'; | ||||
|         this.group = "action"; | ||||
|         this.priority = 9; | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._dialogForm = { | ||||
|   | ||||
| @@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction'; | ||||
|  | ||||
| export default function () { | ||||
|     return function (openmct) { | ||||
|         openmct.contextMenu.registerAction(new NewFolderAction(openmct)); | ||||
|         openmct.actions.register(new NewFolderAction(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -40,9 +40,7 @@ describe("the plugin", () => { | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         newFolderAction = openmct.contextMenu._allActions.filter(action => { | ||||
|             return action.key === 'newFolder'; | ||||
|         })[0]; | ||||
|         newFolderAction = openmct.actions._allActions.newFolder; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|   | ||||
							
								
								
									
										40
									
								
								src/plugins/notebook/actions/CopyToNotebookAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/plugins/notebook/actions/CopyToNotebookAction.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry } from '../utils/notebook-entries'; | ||||
|  | ||||
| export default class CopyToNotebookAction { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.cssClass = 'icon-duplicate'; | ||||
|         this.description = 'Copy value to notebook as an entry'; | ||||
|         this.group = "action"; | ||||
|         this.key = 'copyToNotebook'; | ||||
|         this.name = 'Copy to Notebook'; | ||||
|         this.priority = 1; | ||||
|     } | ||||
|  | ||||
|     copyToNotebook(entryText) { | ||||
|         const notebookStorage = getDefaultNotebook(); | ||||
|         this.openmct.objects.get(notebookStorage.notebookMeta.identifier) | ||||
|             .then(domainObject => { | ||||
|                 addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText); | ||||
|  | ||||
|                 const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`; | ||||
|                 const msg = `Saved to Notebook ${defaultPath}`; | ||||
|                 this.openmct.notifications.info(msg); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         this.copyToNotebook(viewContext.formattedValueForCopy()); | ||||
|     } | ||||
|  | ||||
|     appliesTo(objectPath, view = {}) { | ||||
|         let viewContext = view.getViewContext && view.getViewContext(); | ||||
|  | ||||
|         return viewContext && viewContext.formattedValueForCopy | ||||
|             && typeof viewContext.formattedValueForCopy === 'function'; | ||||
|     } | ||||
| } | ||||
| @@ -9,10 +9,11 @@ | ||||
|     </div> | ||||
|     <SearchResults v-if="search.length" | ||||
|                    ref="searchResults" | ||||
|                    :results="getSearchResults()" | ||||
|                    :domain-object="internalDomainObject" | ||||
|                    :results="searchedEntries" | ||||
|                    @changeSectionPage="changeSelectedSection" | ||||
|                    @updateEntries="updateEntries" | ||||
|     /> | ||||
| 
 | ||||
|     <div v-if="!search.length" | ||||
|          class="c-notebook__body" | ||||
|     > | ||||
| @@ -23,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"> | ||||
| @@ -105,15 +106,15 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import NotebookEntry from './notebook-entry.vue'; | ||||
| import NotebookEntry from './NotebookEntry.vue'; | ||||
| import Search from '@/ui/components/search.vue'; | ||||
| import SearchResults from './search-results.vue'; | ||||
| import Sidebar from './sidebar.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 { throttle } from 'lodash'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import objectUtils from 'objectUtils'; | ||||
| 
 | ||||
| const DEFAULT_CLASS = 'is-notebook-default'; | ||||
| import { throttle } from 'lodash'; | ||||
| 
 | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'snapshotContainer'], | ||||
| @@ -153,6 +154,9 @@ export default { | ||||
|         pages() { | ||||
|             return this.getPages() || []; | ||||
|         }, | ||||
|         searchedEntries() { | ||||
|             return this.getSearchResults(); | ||||
|         }, | ||||
|         sections() { | ||||
|             return this.internalDomainObject.configuration.sections || []; | ||||
|         }, | ||||
| @@ -172,8 +176,6 @@ export default { | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         this.throttledSearchItem = throttle(this.searchItem, 500); | ||||
|     }, | ||||
| @@ -195,15 +197,6 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         addDefaultClass() { | ||||
|             const classList = this.internalDomainObject.classList || []; | ||||
|             if (classList.includes(DEFAULT_CLASS)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             classList.push(DEFAULT_CLASS); | ||||
|             this.mutateObject('classList', classList); | ||||
|         }, | ||||
|         changeSelectedSection({ sectionId, pageId }) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
| @@ -227,7 +220,7 @@ export default { | ||||
|                 return s; | ||||
|             }); | ||||
| 
 | ||||
|             this.updateSection({ sections }); | ||||
|             this.sectionsChanged({ sections }); | ||||
|             this.throttledSearchItem(''); | ||||
|         }, | ||||
|         createNotebookStorageObject() { | ||||
| @@ -259,7 +252,7 @@ export default { | ||||
|             event.preventDefault(); | ||||
|             event.stopImmediatePropagation(); | ||||
| 
 | ||||
|             const snapshotId = event.dataTransfer.getData('snapshot/id'); | ||||
|             const snapshotId = event.dataTransfer.getData('openmct/snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|                 this.newEntry(snapshot); | ||||
| @@ -316,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); | ||||
| @@ -386,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) { | ||||
| @@ -405,7 +395,7 @@ export default { | ||||
|                 return s; | ||||
|             }); | ||||
| 
 | ||||
|             this.updateSection({ sections }); | ||||
|             this.sectionsChanged({ sections }); | ||||
|         }, | ||||
|         newEntry(embed = null) { | ||||
|             this.search = ''; | ||||
| @@ -418,19 +408,30 @@ 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; | ||||
|             } | ||||
| 
 | ||||
|             const classList = domainObject.classList || []; | ||||
|             const index = classList.indexOf(DEFAULT_CLASS); | ||||
|             if (!classList.length || index < 0) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             classList.splice(index, 1); | ||||
|             this.openmct.objects.mutate(domainObject, 'classList', classList); | ||||
|             this.openmct.status.delete(domainObject.identifier); | ||||
|         }, | ||||
|         searchItem(input) { | ||||
|             this.search = input; | ||||
| @@ -440,11 +441,22 @@ export default { | ||||
|         }, | ||||
|         async updateDefaultNotebook(notebookStorage) { | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             this.removeDefaultClass(defaultNotebookObject); | ||||
|             setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             this.addDefaultClass(); | ||||
|             this.defaultSectionId = notebookStorage.section.id; | ||||
|             this.defaultPageId = notebookStorage.page.id; | ||||
|             if (!defaultNotebookObject) { | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) { | ||||
|                 this.removeDefaultClass(defaultNotebookObject); | ||||
|                 setDefaultNotebook(this.openmct, notebookStorage); | ||||
|             } | ||||
| 
 | ||||
|             if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) { | ||||
|                 this.defaultSectionId = notebookStorage.section.id; | ||||
|                 setDefaultNotebookSection(notebookStorage.section); | ||||
|             } | ||||
| 
 | ||||
|             if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) { | ||||
|                 this.defaultPageId = notebookStorage.page.id; | ||||
|                 setDefaultNotebookPage(notebookStorage.page); | ||||
|             } | ||||
|         }, | ||||
|         updateDefaultNotebookPage(pages, id) { | ||||
|             if (!id) { | ||||
| @@ -507,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) { | ||||
| @@ -553,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); | ||||
| @@ -17,7 +17,7 @@ | ||||
|         <div v-if="embed.snapshot" | ||||
|              class="c-ne__embed__time" | ||||
|         > | ||||
|             {{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }} | ||||
|             {{ createdOn }} | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -25,10 +25,10 @@ | ||||
| 
 | ||||
| <script> | ||||
| import Moment from 'moment'; | ||||
| import PopupMenu from './popup-menu.vue'; | ||||
| import PopupMenu from './PopupMenu.vue'; | ||||
| import PreviewAction from '../../../ui/preview/PreviewAction'; | ||||
| import Painterro from 'painterro'; | ||||
| import RemoveDialog from '../utils/removeDialog'; | ||||
| import PainterroInstance from '../utils/painterroInstance'; | ||||
| import SnapshotTemplate from './snapshot-template.html'; | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| @@ -56,7 +56,10 @@ export default { | ||||
|             popupMenuItems: [] | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|     computed: { | ||||
|         createdOn() { | ||||
|             return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss'); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.addPopupMenuItems(); | ||||
| @@ -78,95 +81,44 @@ export default { | ||||
|             this.popupMenuItems = [removeEmbed, preview]; | ||||
|         }, | ||||
|         annotateSnapshot() { | ||||
|             const self = this; | ||||
| 
 | ||||
|             let save = false; | ||||
|             let painterroInstance = {}; | ||||
|             const annotateVue = new Vue({ | ||||
|                 template: '<div id="snap-annotation"></div>' | ||||
|             }); | ||||
|             }).$mount(); | ||||
| 
 | ||||
|             let annotateOverlay = self.openmct.overlays.overlay({ | ||||
|                 element: annotateVue.$mount().$el, | ||||
|             const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot); | ||||
|             const annotateOverlay = this.openmct.overlays.overlay({ | ||||
|                 element: annotateVue.$el, | ||||
|                 size: 'large', | ||||
|                 dismissable: false, | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Cancel', | ||||
|                         callback: function () { | ||||
|                             save = false; | ||||
|                             painterroInstance.save(); | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             painterroInstance.dismiss(); | ||||
|                             annotateOverlay.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         label: 'Save', | ||||
|                         callback: function () { | ||||
| 
 | ||||
|                             save = true; | ||||
|                         callback: () => { | ||||
|                             painterroInstance.save(); | ||||
|                             annotateOverlay.dismiss(); | ||||
|                             this.snapshotOverlay.dismiss(); | ||||
|                             this.openSnapshot(); | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 onDestroy: function () { | ||||
|                 onDestroy: () => { | ||||
|                     annotateVue.$destroy(true); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             painterroInstance = Painterro({ | ||||
|                 id: 'snap-annotation', | ||||
|                 activeColor: '#ff0000', | ||||
|                 activeColorAlpha: 1.0, | ||||
|                 activeFillColor: '#fff', | ||||
|                 activeFillColorAlpha: 0.0, | ||||
|                 backgroundFillColor: '#000', | ||||
|                 backgroundFillColorAlpha: 0.0, | ||||
|                 defaultFontSize: 16, | ||||
|                 defaultLineWidth: 2, | ||||
|                 defaultTool: 'ellipse', | ||||
|                 hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'], | ||||
|                 translation: { | ||||
|                     name: 'en', | ||||
|                     strings: { | ||||
|                         lineColor: 'Line', | ||||
|                         fillColor: 'Fill', | ||||
|                         lineWidth: 'Size', | ||||
|                         textColor: 'Color', | ||||
|                         fontSize: 'Size', | ||||
|                         fontStyle: 'Style' | ||||
|                     } | ||||
|                 }, | ||||
|                 saveHandler: function (image, done) { | ||||
|                     if (save) { | ||||
|                         const url = image.asBlob(); | ||||
|                         const reader = new window.FileReader(); | ||||
|                         reader.readAsDataURL(url); | ||||
|                         reader.onloadend = function () { | ||||
|                             const snapshot = reader.result; | ||||
|                             const snapshotObject = { | ||||
|                                 src: snapshot, | ||||
|                                 type: url.type, | ||||
|                                 size: url.size, | ||||
|                                 modified: Date.now() | ||||
|                             }; | ||||
| 
 | ||||
|                             self.embed.snapshot = snapshotObject; | ||||
|                             self.updateEmbed(self.embed); | ||||
|                         }; | ||||
|                     } else { | ||||
|                         console.log('You cancelled the annotation!!!'); | ||||
|                     } | ||||
| 
 | ||||
|                     done(true); | ||||
|                 } | ||||
|             }).show(this.embed.snapshot.src); | ||||
|             painterroInstance.intialize(); | ||||
|             painterroInstance.show(this.embed.snapshot.src); | ||||
|         }, | ||||
|         changeLocation() { | ||||
|             const link = this.embed.historicLink; | ||||
|             if (!link) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const isTimeBoundChanged = this.embed.bounds.start !== bounds.start | ||||
| @@ -191,7 +143,8 @@ export default { | ||||
|                 this.openmct.notifications.alert(message); | ||||
|             } | ||||
| 
 | ||||
|             window.location.href = link; | ||||
|             const url = new URL(link); | ||||
|             window.location.href = url.hash; | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
| @@ -209,7 +162,8 @@ export default { | ||||
|             this.snapshot = new Vue({ | ||||
|                 data: () => { | ||||
|                     return { | ||||
|                         embed: self.embed | ||||
|                         createdOn: this.createdOn, | ||||
|                         embed: this.embed | ||||
|                     }; | ||||
|                 }, | ||||
|                 methods: { | ||||
| @@ -218,13 +172,11 @@ export default { | ||||
|                     exportImage: self.exportImage | ||||
|                 }, | ||||
|                 template: SnapshotTemplate | ||||
|             }); | ||||
|             }).$mount(); | ||||
| 
 | ||||
|             const snapshotOverlay = this.openmct.overlays.overlay({ | ||||
|                 element: this.snapshot.$mount().$el, | ||||
|                 onDestroy: () => { | ||||
|                     this.snapshot.$destroy(true); | ||||
|                 }, | ||||
|             this.snapshotOverlay = this.openmct.overlays.overlay({ | ||||
|                 element: this.snapshot.$el, | ||||
|                 onDestroy: () => this.snapshot.$destroy(true), | ||||
|                 size: 'large', | ||||
|                 dismissable: true, | ||||
|                 buttons: [ | ||||
| @@ -232,7 +184,7 @@ export default { | ||||
|                         label: 'Done', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             snapshotOverlay.dismiss(); | ||||
|                             this.snapshotOverlay.dismiss(); | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
| @@ -262,6 +214,10 @@ export default { | ||||
|         }, | ||||
|         updateEmbed(embed) { | ||||
|             this.$emit('updateEmbed', embed); | ||||
|         }, | ||||
|         updateSnapshot(snapshotObject) { | ||||
|             this.embed.snapshot = snapshotObject; | ||||
|             this.updateEmbed(this.embed); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -1,23 +1,22 @@ | ||||
| <template> | ||||
| <div class="c-notebook__entry c-ne has-local-controls" | ||||
|      @dragover="dragover" | ||||
|      @drop.capture="dropCapture" | ||||
|      @drop.prevent="dropOnEntry(entry.id, $event)" | ||||
|      @dragover="changeCursor" | ||||
|      @drop.capture="cancelEditMode" | ||||
|      @drop.prevent="dropOnEntry" | ||||
| > | ||||
|     <div class="c-ne__time-and-content"> | ||||
|         <div class="c-ne__time"> | ||||
|             <span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span> | ||||
|             <span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span> | ||||
|             <span>{{ createdOnDate }}</span> | ||||
|             <span>{{ createdOnTime }}</span> | ||||
|         </div> | ||||
|         <div class="c-ne__content"> | ||||
|             <div :id="entry.id" | ||||
|                  class="c-ne__text" | ||||
|                  :class="{'c-input-inline' : !readOnly }" | ||||
|                  :class="{'c-ne__input' : !readOnly }" | ||||
|                  :contenteditable="!readOnly" | ||||
|                  :style="!entry.text.length ? defaultEntryStyle : ''" | ||||
|                  @blur="textBlur($event, entry.id)" | ||||
|                  @focus="textFocus($event, entry.id)" | ||||
|             >{{ entry.text.length ? entry.text : defaultText }}</div> | ||||
|                  @blur="updateEntryValue($event, entry.id)" | ||||
|                  @focus="updateCurrentEntryValue($event, entry.id)" | ||||
|             >{{ entry.text }}</div> | ||||
|             <div class="c-snapshots c-ne__embeds"> | ||||
|                 <NotebookEmbed v-for="embed in entry.embeds" | ||||
|                                :key="embed.id" | ||||
| @@ -57,7 +56,7 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import NotebookEmbed from './notebook-embed.vue'; | ||||
| import NotebookEmbed from './NotebookEmbed.vue'; | ||||
| import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries'; | ||||
| import Moment from 'moment'; | ||||
| 
 | ||||
| @@ -106,37 +105,35 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentEntryValue: '', | ||||
|             defaultEntryStyle: { | ||||
|                 fontStyle: 'italic', | ||||
|                 color: '#6e6e6e' | ||||
|             }, | ||||
|             defaultText: 'add description' | ||||
|             currentEntryValue: '' | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         entry() { | ||||
|     computed: { | ||||
|         createdOnDate() { | ||||
|             return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD'); | ||||
|         }, | ||||
|         readOnly(readOnly) { | ||||
|         }, | ||||
|         selectedSection(selectedSection) { | ||||
|         }, | ||||
|         selectedPage(selectedSection) { | ||||
|         createdOnTime() { | ||||
|             return this.formatTime(this.entry.createdOn, 'HH:mm:ss'); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.updateEntries = this.updateEntries.bind(this); | ||||
|     }, | ||||
|     beforeDestory() { | ||||
|         this.dropOnEntry = this.dropOnEntry.bind(this); | ||||
|     }, | ||||
|     methods: { | ||||
|         cancelEditMode(event) { | ||||
|             const isEditing = this.openmct.editor.isEditing(); | ||||
|             if (isEditing) { | ||||
|                 this.openmct.editor.cancel(); | ||||
|             } | ||||
|         }, | ||||
|         changeCursor() { | ||||
|             event.preventDefault(); | ||||
|             event.dataTransfer.dropEffect = "copy"; | ||||
|         }, | ||||
|         deleteEntry() { | ||||
|             const self = this; | ||||
|             if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const entryPosById = this.entryPosById(this.entry.id); | ||||
|             const entryPosById = self.entryPosById(self.entry.id); | ||||
|             if (entryPosById === -1) { | ||||
|                 return; | ||||
|             } | ||||
| @@ -151,7 +148,7 @@ export default { | ||||
|                         callback: () => { | ||||
|                             const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage); | ||||
|                             entries.splice(entryPosById, 1); | ||||
|                             this.updateEntries(entries); | ||||
|                             self.updateEntries(entries); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
| @@ -164,24 +161,10 @@ export default { | ||||
|                 ] | ||||
|             }); | ||||
|         }, | ||||
|         dragover() { | ||||
|             event.preventDefault(); | ||||
|             event.dataTransfer.dropEffect = "copy"; | ||||
|         }, | ||||
|         dropCapture(event) { | ||||
|             const isEditing = this.openmct.editor.isEditing(); | ||||
|             if (isEditing) { | ||||
|                 this.openmct.editor.cancel(); | ||||
|             } | ||||
|         }, | ||||
|         dropOnEntry(entryId, $event) { | ||||
|         dropOnEntry($event) { | ||||
|             event.stopImmediatePropagation(); | ||||
| 
 | ||||
|             if (!this.domainObject || !this.selectedSection || !this.selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const snapshotId = $event.dataTransfer.getData('snapshot/id'); | ||||
|             const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 this.moveSnapshot(snapshotId); | ||||
| 
 | ||||
| @@ -190,7 +173,7 @@ export default { | ||||
| 
 | ||||
|             const data = $event.dataTransfer.getData('openmct/domain-object-path'); | ||||
|             const objectPath = JSON.parse(data); | ||||
|             const entryPos = this.entryPosById(entryId); | ||||
|             const entryPos = this.entryPosById(this.entry.id); | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const snapshotMeta = { | ||||
|                 bounds, | ||||
| @@ -246,14 +229,40 @@ export default { | ||||
|             this.entry.embeds.splice(embedPosition, 1); | ||||
|             this.updateEntry(this.entry); | ||||
|         }, | ||||
|         selectTextInsideElement(element) { | ||||
|             const range = document.createRange(); | ||||
|             range.selectNodeContents(element); | ||||
|             let selection = window.getSelection(); | ||||
|             selection.removeAllRanges(); | ||||
|             selection.addRange(range); | ||||
|         updateCurrentEntryValue($event) { | ||||
|             if (this.readOnly) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const target = $event.target; | ||||
|             this.currentEntryValue = target ? target.textContent : ''; | ||||
|         }, | ||||
|         textBlur($event, entryId) { | ||||
|         updateEmbed(newEmbed) { | ||||
|             this.entry.embeds.some(e => { | ||||
|                 const found = (e.id === newEmbed.id); | ||||
|                 if (found) { | ||||
|                     e = newEmbed; | ||||
|                 } | ||||
| 
 | ||||
|                 return found; | ||||
|             }); | ||||
| 
 | ||||
|             this.updateEntry(this.entry); | ||||
|         }, | ||||
|         updateEntry(newEntry) { | ||||
|             const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|             entries.some(entry => { | ||||
|                 const found = (entry.id === newEntry.id); | ||||
|                 if (found) { | ||||
|                     entry = newEntry; | ||||
|                 } | ||||
| 
 | ||||
|                 return found; | ||||
|             }); | ||||
| 
 | ||||
|             this.updateEntries(entries); | ||||
|         }, | ||||
|         updateEntryValue($event, entryId) { | ||||
|             if (!this.domainObject || !this.selectedSection || !this.selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
| @@ -266,48 +275,14 @@ export default { | ||||
|             const entryPos = this.entryPosById(entryId); | ||||
|             const value = target.textContent.trim(); | ||||
|             if (this.currentEntryValue !== value) { | ||||
|                 target.textContent = value; | ||||
| 
 | ||||
|                 const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|                 entries[entryPos].text = value; | ||||
| 
 | ||||
|                 this.updateEntries(entries); | ||||
|             } | ||||
|         }, | ||||
|         textFocus($event) { | ||||
|             if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const target = $event.target; | ||||
|             this.currentEntryValue = target ? target.innerText : ''; | ||||
| 
 | ||||
|             if (!this.entry.text.length) { | ||||
|                 this.selectTextInsideElement(target); | ||||
|             } | ||||
|         }, | ||||
|         updateEmbed(newEmbed) { | ||||
|             let embed = this.entry.embeds.find(e => e.id === newEmbed.id); | ||||
| 
 | ||||
|             if (!embed) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             embed = newEmbed; | ||||
|             this.updateEntry(this.entry); | ||||
|         }, | ||||
|         updateEntry(newEntry) { | ||||
|             if (!this.domainObject || !this.selectedSection || !this.selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage); | ||||
|             entries.forEach(entry => { | ||||
|                 if (entry.id === newEntry.id) { | ||||
|                     entry = newEntry; | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             this.updateEntries(entries); | ||||
|         }, | ||||
|         updateEntries(entries) { | ||||
|             this.$emit('updateEntries', entries); | ||||
|         } | ||||
| @@ -1,29 +1,17 @@ | ||||
| <template> | ||||
| <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> | ||||
|     <button | ||||
|         class="c-button--menu icon-notebook" | ||||
|         class="c-icon-button c-button--menu icon-camera" | ||||
|         title="Take a Notebook Snapshot" | ||||
|         @click="setNotebookTypes" | ||||
|         @click.stop="toggleMenu" | ||||
|         @click.stop.prevent="showMenu" | ||||
|     > | ||||
|         <span class="c-button__label"></span> | ||||
|         <span | ||||
|             title="Take Notebook Snapshot" | ||||
|             class="c-icon-button__label" | ||||
|         > | ||||
|             Snapshot | ||||
|         </span> | ||||
|     </button> | ||||
|     <div | ||||
|         v-show="showMenu" | ||||
|         class="c-menu" | ||||
|     > | ||||
|         <ul> | ||||
|             <li | ||||
|                 v-for="(type, index) in notebookTypes" | ||||
|                 :key="index" | ||||
|                 :class="type.cssClass" | ||||
|                 :title="type.name" | ||||
|                 @click="snapshot(type)" | ||||
|             > | ||||
|                 {{ type.name }} | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| @@ -57,22 +45,20 @@ export default { | ||||
|     data() { | ||||
|         return { | ||||
|             notebookSnapshot: null, | ||||
|             notebookTypes: [], | ||||
|             showMenu: false | ||||
|             notebookTypes: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.notebookSnapshot = new Snapshot(this.openmct); | ||||
| 
 | ||||
|         document.addEventListener('click', this.hideMenu); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         document.removeEventListener('click', this.hideMenu); | ||||
|         this.setDefaultNotebookStatus(); | ||||
|     }, | ||||
|     methods: { | ||||
|         setNotebookTypes() { | ||||
|         showMenu(event) { | ||||
|             const notebookTypes = []; | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const elementBoundingClientRect = this.$el.getBoundingClientRect(); | ||||
|             const x = elementBoundingClientRect.x; | ||||
|             const y = elementBoundingClientRect.y + elementBoundingClientRect.height; | ||||
| 
 | ||||
|             if (defaultNotebook) { | ||||
|                 const domainObject = defaultNotebook.domainObject; | ||||
| @@ -83,35 +69,31 @@ export default { | ||||
|                     notebookTypes.push({ | ||||
|                         cssClass: 'icon-notebook', | ||||
|                         name: `Save to Notebook ${defaultPath}`, | ||||
|                         type: NOTEBOOK_DEFAULT | ||||
|                         callBack: () => { | ||||
|                             return this.snapshot(NOTEBOOK_DEFAULT); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             notebookTypes.push({ | ||||
|                 cssClass: 'icon-notebook', | ||||
|                 cssClass: 'icon-camera', | ||||
|                 name: 'Save to Notebook Snapshots', | ||||
|                 type: NOTEBOOK_SNAPSHOT | ||||
|                 callBack: () => { | ||||
|                     return this.snapshot(NOTEBOOK_SNAPSHOT); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             this.notebookTypes = notebookTypes; | ||||
|             this.openmct.menus.showMenu(x, y, notebookTypes); | ||||
|         }, | ||||
|         toggleMenu() { | ||||
|             this.showMenu = !this.showMenu; | ||||
|         }, | ||||
|         hideMenu() { | ||||
|             this.showMenu = false; | ||||
|         }, | ||||
|         snapshot(notebook) { | ||||
|             this.hideMenu(); | ||||
| 
 | ||||
|         snapshot(notebookType) { | ||||
|             this.$nextTick(() => { | ||||
|                 const element = document.querySelector('.c-overlay__contents') | ||||
|                     || document.getElementsByClassName('l-shell__main-container')[0]; | ||||
| 
 | ||||
|                 const bounds = this.openmct.time.bounds(); | ||||
|                 const link = !this.ignoreLink | ||||
|                     ? window.location.href | ||||
|                     ? window.location.hash | ||||
|                     : null; | ||||
| 
 | ||||
|                 const objectPath = this.objectPath || this.openmct.router.path; | ||||
| @@ -122,8 +104,17 @@ export default { | ||||
|                     openmct: this.openmct | ||||
|                 }; | ||||
| 
 | ||||
|                 this.notebookSnapshot.capture(snapshotMeta, notebook.type, element); | ||||
|                 this.notebookSnapshot.capture(snapshotMeta, notebookType, element); | ||||
|             }); | ||||
|         }, | ||||
|         setDefaultNotebookStatus() { | ||||
|             let defaultNotebookObject = getDefaultNotebook(); | ||||
| 
 | ||||
|             if (defaultNotebookObject && defaultNotebookObject.notebookMeta) { | ||||
|                 let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier; | ||||
| 
 | ||||
|                 this.openmct.status.set(notebookIdentifier, 'notebook-default'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user