Compare commits
	
		
			72 Commits
		
	
	
		
			v1.4.0-rc2
			...
			reusable-t
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					92737b43af | ||
| 
						 | 
					8b0f6885ee | ||
| 
						 | 
					9af2d15cef | ||
| 
						 | 
					e60d8d08a4 | ||
| 
						 | 
					3e9b567fce | ||
| 
						 | 
					6f51de85db | ||
| 
						 | 
					f202ae19cb | ||
| 
						 | 
					668bd75025 | ||
| 
						 | 
					e6e07cf959 | ||
| 
						 | 
					2f8431905f | ||
| 
						 | 
					23aba14dfe | ||
| 
						 | 
					b0fa955914 | ||
| 
						 | 
					98207a3e0d | ||
| 
						 | 
					26b81345f2 | ||
| 
						 | 
					ac2034b243 | ||
| 
						 | 
					351848ad56 | ||
| 
						 | 
					cbac495f93 | ||
| 
						 | 
					15ef5b7623 | ||
| 
						 | 
					46c7ac944f | ||
| 
						 | 
					aa4bfab462 | ||
| 
						 | 
					f5cbb37e5a | ||
| 
						 | 
					8d9079984a | ||
| 
						 | 
					41783d8939 | ||
| 
						 | 
					441ad58fe7 | ||
| 
						 | 
					06a6a3f773 | ||
| 
						 | 
					52fab78625 | ||
| 
						 | 
					5eb6c15959 | ||
| 
						 | 
					ce8c31cfa4 | ||
| 
						 | 
					d80c0eef8e | ||
| 
						 | 
					55829dcf05 | ||
| 
						 | 
					d78956327c | ||
| 
						 | 
					4a0728a55b | ||
| 
						 | 
					2e1d57aa8c | ||
| 
						 | 
					1c2b0678be | ||
| 
						 | 
					6f810add43 | ||
| 
						 | 
					12727adb16 | ||
| 
						 | 
					9da750c3bb | ||
| 
						 | 
					176226ddef | ||
| 
						 | 
					acea18fa70 | ||
| 
						 | 
					d1656f8561 | ||
| 
						 | 
					87751e882c | ||
| 
						 | 
					dff393a714 | ||
| 
						 | 
					fd9c9aee03 | ||
| 
						 | 
					59bf981fb0 | ||
| 
						 | 
					4bbdac759f | ||
| 
						 | 
					13fe7509de | ||
| 
						 | 
					6fd8f6cd43 | ||
| 
						 | 
					6375ecda34 | ||
| 
						 | 
					d232dacc65 | ||
| 
						 | 
					59946e89ef | ||
| 
						 | 
					d75c4b4049 | ||
| 
						 | 
					30ca4b707d | ||
| 
						 | 
					27704c9a48 | ||
| 
						 | 
					b0203f2272 | ||
| 
						 | 
					77b720d00d | ||
| 
						 | 
					ba982671b2 | ||
| 
						 | 
					5df7d92d64 | ||
| 
						 | 
					a8228406de | ||
| 
						 | 
					2401473012 | ||
| 
						 | 
					e502fb88fa | ||
| 
						 | 
					37a52cb011 | ||
| 
						 | 
					04fb4e8a82 | ||
| 
						 | 
					5646a252f7 | ||
| 
						 | 
					0e6ce7f58b | ||
| 
						 | 
					8cd6a4c6a3 | ||
| 
						 | 
					02fc162197 | ||
| 
						 | 
					84d21a3695 | ||
| 
						 | 
					1a6369c2b9 | ||
| 
						 | 
					463c44679d | ||
| 
						 | 
					c1f3ea4e61 | ||
| 
						 | 
					142b767470 | ||
| 
						 | 
					184b716b53 | 
@@ -182,7 +182,7 @@ The following guidelines are provided for anyone contributing source code to the
 | 
			
		||||
1. Avoid the use of "magic" values.
 | 
			
		||||
   eg.
 | 
			
		||||
   ```JavaScript
 | 
			
		||||
   Const UNAUTHORIZED = 401
 | 
			
		||||
   const UNAUTHORIZED = 401;
 | 
			
		||||
   if (responseCode === UNAUTHORIZED)
 | 
			
		||||
   ```
 | 
			
		||||
   is preferable to
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								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'
 | 
			
		||||
@@ -73,30 +111,30 @@
 | 
			
		||||
                        {
 | 
			
		||||
                            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()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    ],
 | 
			
		||||
                    // maximum recent bounds to retain in conductor history
 | 
			
		||||
                    records: 10,
 | 
			
		||||
                    records: 10
 | 
			
		||||
                    // maximum duration between start and end bounds
 | 
			
		||||
                    // for utc-based time systems this is in milliseconds
 | 
			
		||||
                    limit: 1000 * 60 * 60 * 24
 | 
			
		||||
                    // limit: ONE_DAY
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: "Realtime",
 | 
			
		||||
@@ -105,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: 65
 | 
			
		||||
                    lines: 66
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "openmct",
 | 
			
		||||
  "version": "1.3.3-SNAPSHOT",
 | 
			
		||||
  "version": "1.6.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"
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,9 @@ define(
 | 
			
		||||
            // is also invoked during the create process which should be allowed,
 | 
			
		||||
            // because it may be saved elsewhere
 | 
			
		||||
            if ((key === 'edit' && category === 'view-control') || key === 'properties') {
 | 
			
		||||
                let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId());
 | 
			
		||||
                let identifier = this.openmct.objects.parseKeyString(domainObject.getId());
 | 
			
		||||
 | 
			
		||||
                return this.openmct.objects.isPersistable(newStyleObject);
 | 
			
		||||
                return this.openmct.objects.isPersistable(identifier);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,8 @@ define(
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                mockObjectAPI = jasmine.createSpyObj('objectAPI', [
 | 
			
		||||
                    'isPersistable'
 | 
			
		||||
                    'isPersistable',
 | 
			
		||||
                    'parseKeyString'
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                mockAPI = {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,12 +20,12 @@
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<div class="c-object-label"
 | 
			
		||||
     ng-class="{ 'is-missing': model.status === 'missing' }"
 | 
			
		||||
     ng-class="{ 'is-status--missing': model.status === 'missing' }"
 | 
			
		||||
>
 | 
			
		||||
    <div class="c-object-label__type-icon {{type.getCssClass()}}"
 | 
			
		||||
         ng-class="{ 'l-icon-link':location.isLink() }"
 | 
			
		||||
    >
 | 
			
		||||
        <span class="is-missing__indicator" title="This item is missing"></span>
 | 
			
		||||
        <span class="is-status__indicator" title="This item is missing or suspect"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class='c-object-label__name'>{{model.name}}</div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -48,9 +48,9 @@ define(
 | 
			
		||||
            // prevents editing of objects that cannot be persisted, so we can assume that this
 | 
			
		||||
            // is a new object.
 | 
			
		||||
            if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
 | 
			
		||||
                let newStyleObject = objectUtils.toNewFormat(parent, parent.getId());
 | 
			
		||||
                let identifier = this.openmct.objects.parseKeyString(parent.getId());
 | 
			
		||||
 | 
			
		||||
                return this.openmct.objects.isPersistable(newStyleObject);
 | 
			
		||||
                return this.openmct.objects.isPersistable(identifier);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ define(
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                objectAPI = jasmine.createSpyObj('objectsAPI', [
 | 
			
		||||
                    'isPersistable'
 | 
			
		||||
                    'isPersistable',
 | 
			
		||||
                    'parseKeyString'
 | 
			
		||||
                ]);
 | 
			
		||||
 | 
			
		||||
                mockOpenMCT = {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,32 +21,24 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    "./src/actions/MoveAction",
 | 
			
		||||
    "./src/actions/CopyAction",
 | 
			
		||||
    "./src/actions/LinkAction",
 | 
			
		||||
    "./src/actions/SetPrimaryLocationAction",
 | 
			
		||||
    "./src/services/LocatingCreationDecorator",
 | 
			
		||||
    "./src/services/LocatingObjectDecorator",
 | 
			
		||||
    "./src/policies/CopyPolicy",
 | 
			
		||||
    "./src/policies/CrossSpacePolicy",
 | 
			
		||||
    "./src/policies/MovePolicy",
 | 
			
		||||
    "./src/capabilities/LocationCapability",
 | 
			
		||||
    "./src/services/MoveService",
 | 
			
		||||
    "./src/services/LinkService",
 | 
			
		||||
    "./src/services/CopyService",
 | 
			
		||||
    "./src/services/LocationService"
 | 
			
		||||
], function (
 | 
			
		||||
    MoveAction,
 | 
			
		||||
    CopyAction,
 | 
			
		||||
    LinkAction,
 | 
			
		||||
    SetPrimaryLocationAction,
 | 
			
		||||
    LocatingCreationDecorator,
 | 
			
		||||
    LocatingObjectDecorator,
 | 
			
		||||
    CopyPolicy,
 | 
			
		||||
    CrossSpacePolicy,
 | 
			
		||||
    MovePolicy,
 | 
			
		||||
    LocationCapability,
 | 
			
		||||
    MoveService,
 | 
			
		||||
    LinkService,
 | 
			
		||||
    CopyService,
 | 
			
		||||
    LocationService
 | 
			
		||||
@@ -60,41 +52,14 @@ define([
 | 
			
		||||
            "configuration": {},
 | 
			
		||||
            "extensions": {
 | 
			
		||||
                "actions": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "move",
 | 
			
		||||
                        "name": "Move",
 | 
			
		||||
                        "description": "Move object to another location.",
 | 
			
		||||
                        "cssClass": "icon-move",
 | 
			
		||||
                        "category": "contextual",
 | 
			
		||||
                        "implementation": MoveAction,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "policyService",
 | 
			
		||||
                            "locationService",
 | 
			
		||||
                            "moveService"
 | 
			
		||||
                        ]
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "copy",
 | 
			
		||||
                        "name": "Duplicate",
 | 
			
		||||
                        "description": "Duplicate object to another location.",
 | 
			
		||||
                        "cssClass": "icon-duplicate",
 | 
			
		||||
                        "category": "contextual",
 | 
			
		||||
                        "implementation": CopyAction,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "$log",
 | 
			
		||||
                            "policyService",
 | 
			
		||||
                            "locationService",
 | 
			
		||||
                            "copyService",
 | 
			
		||||
                            "dialogService",
 | 
			
		||||
                            "notificationService"
 | 
			
		||||
                        ]
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "link",
 | 
			
		||||
                        "name": "Create Link",
 | 
			
		||||
                        "description": "Create Link to object in another location.",
 | 
			
		||||
                        "cssClass": "icon-link",
 | 
			
		||||
                        "category": "contextual",
 | 
			
		||||
                        "group": "action",
 | 
			
		||||
                        "priority": 7,
 | 
			
		||||
                        "implementation": LinkAction,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "policyService",
 | 
			
		||||
@@ -135,10 +100,6 @@ define([
 | 
			
		||||
                    {
 | 
			
		||||
                        "category": "action",
 | 
			
		||||
                        "implementation": CopyPolicy
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "category": "action",
 | 
			
		||||
                        "implementation": MovePolicy
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "capabilities": [
 | 
			
		||||
@@ -154,17 +115,6 @@ define([
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "services": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "moveService",
 | 
			
		||||
                        "name": "Move Service",
 | 
			
		||||
                        "description": "Provides a service for moving objects",
 | 
			
		||||
                        "implementation": MoveService,
 | 
			
		||||
                        "depends": [
 | 
			
		||||
                            "openmct",
 | 
			
		||||
                            "linkService",
 | 
			
		||||
                            "$q"
 | 
			
		||||
                        ]
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "key": "linkService",
 | 
			
		||||
                        "name": "Link Service",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,168 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    ['./AbstractComposeAction', './CancelError'],
 | 
			
		||||
    function (AbstractComposeAction, CancelError) {
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The CopyAction is available from context menus and allows a user to
 | 
			
		||||
         * deep copy an object to another location of their choosing.
 | 
			
		||||
         *
 | 
			
		||||
         * @implements {Action}
 | 
			
		||||
         * @constructor
 | 
			
		||||
         * @memberof platform/entanglement
 | 
			
		||||
         */
 | 
			
		||||
        function CopyAction(
 | 
			
		||||
            $log,
 | 
			
		||||
            policyService,
 | 
			
		||||
            locationService,
 | 
			
		||||
            copyService,
 | 
			
		||||
            dialogService,
 | 
			
		||||
            notificationService,
 | 
			
		||||
            context
 | 
			
		||||
        ) {
 | 
			
		||||
            this.dialog = undefined;
 | 
			
		||||
            this.notification = undefined;
 | 
			
		||||
            this.dialogService = dialogService;
 | 
			
		||||
            this.notificationService = notificationService;
 | 
			
		||||
            this.$log = $log;
 | 
			
		||||
            //Extend the behaviour of the Abstract Compose Action
 | 
			
		||||
            AbstractComposeAction.call(
 | 
			
		||||
                this,
 | 
			
		||||
                policyService,
 | 
			
		||||
                locationService,
 | 
			
		||||
                copyService,
 | 
			
		||||
                context,
 | 
			
		||||
                "Duplicate",
 | 
			
		||||
                "To a Location"
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Updates user about progress of copy. Should not be invoked by
 | 
			
		||||
         * client code under any circumstances.
 | 
			
		||||
         *
 | 
			
		||||
         * @private
 | 
			
		||||
         * @param phase
 | 
			
		||||
         * @param totalObjects
 | 
			
		||||
         * @param processed
 | 
			
		||||
         */
 | 
			
		||||
        CopyAction.prototype.progress = function (phase, totalObjects, processed) {
 | 
			
		||||
            /*
 | 
			
		||||
             Copy has two distinct phases. In the first phase a copy plan is
 | 
			
		||||
             made in memory. During this phase of execution, the user is
 | 
			
		||||
             shown a blocking 'modal' dialog.
 | 
			
		||||
 | 
			
		||||
             In the second phase, the copying is taking place, and the user
 | 
			
		||||
             is shown non-invasive banner notifications at the bottom of the screen.
 | 
			
		||||
             */
 | 
			
		||||
            if (phase.toLowerCase() === 'preparing' && !this.dialog) {
 | 
			
		||||
                this.dialog = this.dialogService.showBlockingMessage({
 | 
			
		||||
                    title: "Preparing to copy objects",
 | 
			
		||||
                    hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
 | 
			
		||||
                    unknownProgress: true,
 | 
			
		||||
                    severity: "info"
 | 
			
		||||
                });
 | 
			
		||||
            } else if (phase.toLowerCase() === "copying") {
 | 
			
		||||
                if (this.dialog) {
 | 
			
		||||
                    this.dialog.dismiss();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!this.notification) {
 | 
			
		||||
                    this.notification = this.notificationService
 | 
			
		||||
                        .notify({
 | 
			
		||||
                            title: "Copying objects",
 | 
			
		||||
                            unknownProgress: false,
 | 
			
		||||
                            severity: "info"
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.notification.model.progress = (processed / totalObjects) * 100;
 | 
			
		||||
                this.notification.model.title = ["Copied ", processed, "of ",
 | 
			
		||||
                    totalObjects, "objects"].join(" ");
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Executes the CopyAction. The CopyAction uses the default behaviour of
 | 
			
		||||
         * the AbstractComposeAction, but extends it to support notification
 | 
			
		||||
         * updates of progress on copy.
 | 
			
		||||
         */
 | 
			
		||||
        CopyAction.prototype.perform = function () {
 | 
			
		||||
            var self = this;
 | 
			
		||||
 | 
			
		||||
            function success(domainObject) {
 | 
			
		||||
                var domainObjectName = domainObject.model.name;
 | 
			
		||||
 | 
			
		||||
                self.notification.dismiss();
 | 
			
		||||
                self.notificationService.info(domainObjectName + " copied successfully.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function error(errorDetails) {
 | 
			
		||||
                // No need to notify user of their own cancellation
 | 
			
		||||
                if (errorDetails instanceof CancelError) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var errorDialog,
 | 
			
		||||
                    errorMessage = {
 | 
			
		||||
                        title: "Error copying objects.",
 | 
			
		||||
                        severity: "error",
 | 
			
		||||
                        hint: errorDetails.message,
 | 
			
		||||
                        minimized: true, // want the notification to be minimized initially (don't show banner)
 | 
			
		||||
                        options: [{
 | 
			
		||||
                            label: "OK",
 | 
			
		||||
                            callback: function () {
 | 
			
		||||
                                errorDialog.dismiss();
 | 
			
		||||
                            }
 | 
			
		||||
                        }]
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                self.dialog.dismiss();
 | 
			
		||||
                if (self.notification) {
 | 
			
		||||
                    self.notification.dismiss(); // Clear the progress notification
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.$log.error("Error copying objects. ", errorDetails);
 | 
			
		||||
                //Show a minimized notification of error for posterity
 | 
			
		||||
                self.notificationService.notify(errorMessage);
 | 
			
		||||
                //Display a blocking message
 | 
			
		||||
                errorDialog = self.dialogService.showBlockingMessage(errorMessage);
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function notification(details) {
 | 
			
		||||
                self.progress(details.phase, details.totalObjects, details.processed);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return AbstractComposeAction.prototype.perform.call(this)
 | 
			
		||||
                .then(success, error, notification);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        CopyAction.appliesTo = AbstractComposeAction.appliesTo;
 | 
			
		||||
 | 
			
		||||
        return CopyAction;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    function () {
 | 
			
		||||
        /**
 | 
			
		||||
         * MoveService provides an interface for moving objects from one
 | 
			
		||||
         * location to another.  It also provides a method for determining if
 | 
			
		||||
         * an object can be copied to a specific location.
 | 
			
		||||
         * @constructor
 | 
			
		||||
         * @memberof platform/entanglement
 | 
			
		||||
         * @implements {platform/entanglement.AbstractComposeService}
 | 
			
		||||
         */
 | 
			
		||||
        function MoveService(openmct, linkService) {
 | 
			
		||||
            this.openmct = openmct;
 | 
			
		||||
            this.linkService = linkService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MoveService.prototype.validate = function (object, parentCandidate) {
 | 
			
		||||
            var currentParent = object
 | 
			
		||||
                .getCapability('context')
 | 
			
		||||
                .getParent();
 | 
			
		||||
 | 
			
		||||
            if (!parentCandidate || !parentCandidate.getId) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (parentCandidate.getId() === currentParent.getId()) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (parentCandidate.getId() === object.getId()) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.openmct.composition.checkPolicy(
 | 
			
		||||
                parentCandidate.useCapability('adapter'),
 | 
			
		||||
                object.useCapability('adapter')
 | 
			
		||||
            );
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        MoveService.prototype.perform = function (object, parentObject) {
 | 
			
		||||
            function relocate(objectInNewContext) {
 | 
			
		||||
                var newLocationCapability = objectInNewContext
 | 
			
		||||
                        .getCapability('location'),
 | 
			
		||||
                    oldLocationCapability = object
 | 
			
		||||
                        .getCapability('location');
 | 
			
		||||
 | 
			
		||||
                if (!newLocationCapability
 | 
			
		||||
                        || !oldLocationCapability) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (oldLocationCapability.isOriginal()) {
 | 
			
		||||
                    return newLocationCapability.setPrimaryLocation(
 | 
			
		||||
                        newLocationCapability
 | 
			
		||||
                            .getContextualLocation()
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!this.validate(object, parentObject)) {
 | 
			
		||||
                throw new Error(
 | 
			
		||||
                    "Tried to move objects without validating first."
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.linkService
 | 
			
		||||
                .perform(object, parentObject)
 | 
			
		||||
                .then(relocate)
 | 
			
		||||
                .then(function () {
 | 
			
		||||
                    return object
 | 
			
		||||
                        .getCapability('action')
 | 
			
		||||
                        .perform('remove', true);
 | 
			
		||||
                });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return MoveService;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@@ -1,243 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    [
 | 
			
		||||
        '../../src/actions/CopyAction',
 | 
			
		||||
        '../services/MockCopyService',
 | 
			
		||||
        '../DomainObjectFactory'
 | 
			
		||||
    ],
 | 
			
		||||
    function (CopyAction, MockCopyService, domainObjectFactory) {
 | 
			
		||||
 | 
			
		||||
        describe("Copy Action", function () {
 | 
			
		||||
 | 
			
		||||
            var copyAction,
 | 
			
		||||
                policyService,
 | 
			
		||||
                locationService,
 | 
			
		||||
                locationServicePromise,
 | 
			
		||||
                copyService,
 | 
			
		||||
                context,
 | 
			
		||||
                selectedObject,
 | 
			
		||||
                selectedObjectContextCapability,
 | 
			
		||||
                currentParent,
 | 
			
		||||
                newParent,
 | 
			
		||||
                notificationService,
 | 
			
		||||
                notification,
 | 
			
		||||
                dialogService,
 | 
			
		||||
                mockDialog,
 | 
			
		||||
                mockLog,
 | 
			
		||||
                abstractComposePromise,
 | 
			
		||||
                domainObject = {model: {name: "mockObject"}},
 | 
			
		||||
                progress = {
 | 
			
		||||
                    phase: "copying",
 | 
			
		||||
                    totalObjects: 10,
 | 
			
		||||
                    processed: 1
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                policyService = jasmine.createSpyObj(
 | 
			
		||||
                    'policyService',
 | 
			
		||||
                    ['allow']
 | 
			
		||||
                );
 | 
			
		||||
                policyService.allow.and.returnValue(true);
 | 
			
		||||
 | 
			
		||||
                selectedObjectContextCapability = jasmine.createSpyObj(
 | 
			
		||||
                    'selectedObjectContextCapability',
 | 
			
		||||
                    [
 | 
			
		||||
                        'getParent'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                selectedObject = domainObjectFactory({
 | 
			
		||||
                    name: 'selectedObject',
 | 
			
		||||
                    model: {
 | 
			
		||||
                        name: 'selectedObject'
 | 
			
		||||
                    },
 | 
			
		||||
                    capabilities: {
 | 
			
		||||
                        context: selectedObjectContextCapability
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                currentParent = domainObjectFactory({
 | 
			
		||||
                    name: 'currentParent'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                selectedObjectContextCapability
 | 
			
		||||
                    .getParent
 | 
			
		||||
                    .and.returnValue(currentParent);
 | 
			
		||||
 | 
			
		||||
                newParent = domainObjectFactory({
 | 
			
		||||
                    name: 'newParent'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                locationService = jasmine.createSpyObj(
 | 
			
		||||
                    'locationService',
 | 
			
		||||
                    [
 | 
			
		||||
                        'getLocationFromUser'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                locationServicePromise = jasmine.createSpyObj(
 | 
			
		||||
                    'locationServicePromise',
 | 
			
		||||
                    [
 | 
			
		||||
                        'then'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                abstractComposePromise = jasmine.createSpyObj(
 | 
			
		||||
                    'abstractComposePromise',
 | 
			
		||||
                    [
 | 
			
		||||
                        'then'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                abstractComposePromise.then.and.callFake(function (success, error, notify) {
 | 
			
		||||
                    notify(progress);
 | 
			
		||||
                    success(domainObject);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                locationServicePromise.then.and.callFake(function (callback) {
 | 
			
		||||
                    callback(newParent);
 | 
			
		||||
 | 
			
		||||
                    return abstractComposePromise;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                locationService
 | 
			
		||||
                    .getLocationFromUser
 | 
			
		||||
                    .and.returnValue(locationServicePromise);
 | 
			
		||||
 | 
			
		||||
                dialogService = jasmine.createSpyObj('dialogService',
 | 
			
		||||
                    ['showBlockingMessage']
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
 | 
			
		||||
                dialogService.showBlockingMessage.and.returnValue(mockDialog);
 | 
			
		||||
 | 
			
		||||
                notification = jasmine.createSpyObj('notification',
 | 
			
		||||
                    ['dismiss', 'model']
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                notificationService = jasmine.createSpyObj('notificationService',
 | 
			
		||||
                    ['notify', 'info']
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                notificationService.notify.and.returnValue(notification);
 | 
			
		||||
 | 
			
		||||
                mockLog = jasmine.createSpyObj('log', ['error']);
 | 
			
		||||
 | 
			
		||||
                copyService = new MockCopyService();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("with context from context-action", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    context = {
 | 
			
		||||
                        domainObject: selectedObject
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    copyAction = new CopyAction(
 | 
			
		||||
                        mockLog,
 | 
			
		||||
                        policyService,
 | 
			
		||||
                        locationService,
 | 
			
		||||
                        copyService,
 | 
			
		||||
                        dialogService,
 | 
			
		||||
                        notificationService,
 | 
			
		||||
                        context
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("initializes happily", function () {
 | 
			
		||||
                    expect(copyAction).toBeDefined();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                describe("when performed it", function () {
 | 
			
		||||
                    beforeEach(function () {
 | 
			
		||||
                        spyOn(copyAction, 'progress').and.callThrough();
 | 
			
		||||
                        copyAction.perform();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("prompts for location", function () {
 | 
			
		||||
                        expect(locationService.getLocationFromUser)
 | 
			
		||||
                            .toHaveBeenCalledWith(
 | 
			
		||||
                                "Duplicate selectedObject To a Location",
 | 
			
		||||
                                "Duplicate To",
 | 
			
		||||
                                jasmine.any(Function),
 | 
			
		||||
                                currentParent
 | 
			
		||||
                            );
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("waits for location and handles cancellation by user", function () {
 | 
			
		||||
                        expect(locationServicePromise.then)
 | 
			
		||||
                            .toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("copies object to selected location", function () {
 | 
			
		||||
                        locationServicePromise
 | 
			
		||||
                            .then
 | 
			
		||||
                            .calls.mostRecent()
 | 
			
		||||
                            .args[0](newParent);
 | 
			
		||||
 | 
			
		||||
                        expect(copyService.perform)
 | 
			
		||||
                            .toHaveBeenCalledWith(selectedObject, newParent);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("notifies the user of progress", function () {
 | 
			
		||||
                        expect(notificationService.info).toHaveBeenCalled();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("notifies the user with name of object copied", function () {
 | 
			
		||||
                        expect(notificationService.info)
 | 
			
		||||
                            .toHaveBeenCalledWith("mockObject copied successfully.");
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("with context from drag-drop", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    context = {
 | 
			
		||||
                        selectedObject: selectedObject,
 | 
			
		||||
                        domainObject: newParent
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    copyAction = new CopyAction(
 | 
			
		||||
                        mockLog,
 | 
			
		||||
                        policyService,
 | 
			
		||||
                        locationService,
 | 
			
		||||
                        copyService,
 | 
			
		||||
                        dialogService,
 | 
			
		||||
                        notificationService,
 | 
			
		||||
                        context
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("initializes happily", function () {
 | 
			
		||||
                    expect(copyAction).toBeDefined();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("performs copy immediately", function () {
 | 
			
		||||
                    copyAction.perform();
 | 
			
		||||
                    expect(copyService.perform)
 | 
			
		||||
                        .toHaveBeenCalledWith(selectedObject, newParent);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,178 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    [
 | 
			
		||||
        '../../src/actions/MoveAction',
 | 
			
		||||
        '../services/MockMoveService',
 | 
			
		||||
        '../DomainObjectFactory'
 | 
			
		||||
    ],
 | 
			
		||||
    function (MoveAction, MockMoveService, domainObjectFactory) {
 | 
			
		||||
 | 
			
		||||
        describe("Move Action", function () {
 | 
			
		||||
 | 
			
		||||
            var moveAction,
 | 
			
		||||
                policyService,
 | 
			
		||||
                locationService,
 | 
			
		||||
                locationServicePromise,
 | 
			
		||||
                moveService,
 | 
			
		||||
                context,
 | 
			
		||||
                selectedObject,
 | 
			
		||||
                selectedObjectContextCapability,
 | 
			
		||||
                currentParent,
 | 
			
		||||
                newParent;
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                policyService = jasmine.createSpyObj(
 | 
			
		||||
                    'policyService',
 | 
			
		||||
                    ['allow']
 | 
			
		||||
                );
 | 
			
		||||
                policyService.allow.and.returnValue(true);
 | 
			
		||||
 | 
			
		||||
                selectedObjectContextCapability = jasmine.createSpyObj(
 | 
			
		||||
                    'selectedObjectContextCapability',
 | 
			
		||||
                    [
 | 
			
		||||
                        'getParent'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                selectedObject = domainObjectFactory({
 | 
			
		||||
                    name: 'selectedObject',
 | 
			
		||||
                    model: {
 | 
			
		||||
                        name: 'selectedObject'
 | 
			
		||||
                    },
 | 
			
		||||
                    capabilities: {
 | 
			
		||||
                        context: selectedObjectContextCapability
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                currentParent = domainObjectFactory({
 | 
			
		||||
                    name: 'currentParent'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                selectedObjectContextCapability
 | 
			
		||||
                    .getParent
 | 
			
		||||
                    .and.returnValue(currentParent);
 | 
			
		||||
 | 
			
		||||
                newParent = domainObjectFactory({
 | 
			
		||||
                    name: 'newParent'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                locationService = jasmine.createSpyObj(
 | 
			
		||||
                    'locationService',
 | 
			
		||||
                    [
 | 
			
		||||
                        'getLocationFromUser'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                locationServicePromise = jasmine.createSpyObj(
 | 
			
		||||
                    'locationServicePromise',
 | 
			
		||||
                    [
 | 
			
		||||
                        'then'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                locationService
 | 
			
		||||
                    .getLocationFromUser
 | 
			
		||||
                    .and.returnValue(locationServicePromise);
 | 
			
		||||
 | 
			
		||||
                moveService = new MockMoveService();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("with context from context-action", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    context = {
 | 
			
		||||
                        domainObject: selectedObject
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    moveAction = new MoveAction(
 | 
			
		||||
                        policyService,
 | 
			
		||||
                        locationService,
 | 
			
		||||
                        moveService,
 | 
			
		||||
                        context
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("initializes happily", function () {
 | 
			
		||||
                    expect(moveAction).toBeDefined();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                describe("when performed it", function () {
 | 
			
		||||
                    beforeEach(function () {
 | 
			
		||||
                        moveAction.perform();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("prompts for location", function () {
 | 
			
		||||
                        expect(locationService.getLocationFromUser)
 | 
			
		||||
                            .toHaveBeenCalledWith(
 | 
			
		||||
                                "Move selectedObject To a New Location",
 | 
			
		||||
                                "Move To",
 | 
			
		||||
                                jasmine.any(Function),
 | 
			
		||||
                                currentParent
 | 
			
		||||
                            );
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("waits for location and handles cancellation by user", function () {
 | 
			
		||||
                        expect(locationServicePromise.then)
 | 
			
		||||
                            .toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("moves object to selected location", function () {
 | 
			
		||||
                        locationServicePromise
 | 
			
		||||
                            .then
 | 
			
		||||
                            .calls.mostRecent()
 | 
			
		||||
                            .args[0](newParent);
 | 
			
		||||
 | 
			
		||||
                        expect(moveService.perform)
 | 
			
		||||
                            .toHaveBeenCalledWith(selectedObject, newParent);
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("with context from drag-drop", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    context = {
 | 
			
		||||
                        selectedObject: selectedObject,
 | 
			
		||||
                        domainObject: newParent
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    moveAction = new MoveAction(
 | 
			
		||||
                        policyService,
 | 
			
		||||
                        locationService,
 | 
			
		||||
                        moveService,
 | 
			
		||||
                        context
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("initializes happily", function () {
 | 
			
		||||
                    expect(moveAction).toBeDefined();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("performs move immediately", function () {
 | 
			
		||||
                    moveAction.perform();
 | 
			
		||||
                    expect(moveService.perform)
 | 
			
		||||
                        .toHaveBeenCalledWith(selectedObject, newParent);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,124 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    '../../src/policies/MovePolicy',
 | 
			
		||||
    '../DomainObjectFactory'
 | 
			
		||||
], function (MovePolicy, domainObjectFactory) {
 | 
			
		||||
 | 
			
		||||
    describe("MovePolicy", function () {
 | 
			
		||||
        var testMetadata,
 | 
			
		||||
            testContext,
 | 
			
		||||
            mockDomainObject,
 | 
			
		||||
            mockParent,
 | 
			
		||||
            mockParentType,
 | 
			
		||||
            mockType,
 | 
			
		||||
            mockAction,
 | 
			
		||||
            policy;
 | 
			
		||||
 | 
			
		||||
        beforeEach(function () {
 | 
			
		||||
            var mockContextCapability =
 | 
			
		||||
                    jasmine.createSpyObj('context', ['getParent']);
 | 
			
		||||
 | 
			
		||||
            mockType =
 | 
			
		||||
                jasmine.createSpyObj('type', ['hasFeature']);
 | 
			
		||||
            mockParentType =
 | 
			
		||||
                jasmine.createSpyObj('parent-type', ['hasFeature']);
 | 
			
		||||
 | 
			
		||||
            testMetadata = {};
 | 
			
		||||
 | 
			
		||||
            mockDomainObject = domainObjectFactory({
 | 
			
		||||
                capabilities: {
 | 
			
		||||
                    context: mockContextCapability,
 | 
			
		||||
                    type: mockType
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            mockParent = domainObjectFactory({
 | 
			
		||||
                capabilities: {
 | 
			
		||||
                    type: mockParentType
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            mockContextCapability.getParent.and.returnValue(mockParent);
 | 
			
		||||
 | 
			
		||||
            mockType.hasFeature.and.callFake(function (feature) {
 | 
			
		||||
                return feature === 'creation';
 | 
			
		||||
            });
 | 
			
		||||
            mockParentType.hasFeature.and.callFake(function (feature) {
 | 
			
		||||
                return feature === 'creation';
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            mockAction = jasmine.createSpyObj('action', ['getMetadata']);
 | 
			
		||||
            mockAction.getMetadata.and.returnValue(testMetadata);
 | 
			
		||||
 | 
			
		||||
            testContext = { domainObject: mockDomainObject };
 | 
			
		||||
 | 
			
		||||
            policy = new MovePolicy();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe("for move actions", function () {
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                testMetadata.key = 'move';
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("when an object is non-modifiable", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    mockType.hasFeature.and.returnValue(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("disallows the action", function () {
 | 
			
		||||
                    expect(policy.allow(mockAction, testContext)).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("when a parent is non-modifiable", function () {
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    mockParentType.hasFeature.and.returnValue(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("disallows the action", function () {
 | 
			
		||||
                    expect(policy.allow(mockAction, testContext)).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("when an object and its parent are modifiable", function () {
 | 
			
		||||
                it("allows the action", function () {
 | 
			
		||||
                    expect(policy.allow(mockAction, testContext)).toBe(true);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe("for other actions", function () {
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                testMetadata.key = 'foo';
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("simply allows the action", function () {
 | 
			
		||||
                expect(policy.allow(mockAction, testContext)).toBe(true);
 | 
			
		||||
                mockType.hasFeature.and.returnValue(false);
 | 
			
		||||
                expect(policy.allow(mockAction, testContext)).toBe(true);
 | 
			
		||||
                mockParentType.hasFeature.and.returnValue(false);
 | 
			
		||||
                expect(policy.allow(mockAction, testContext)).toBe(true);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    function () {
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * MockMoveService provides the same interface as the moveService,
 | 
			
		||||
         * returning promises where it would normally do so.  At it's core,
 | 
			
		||||
         * it is a jasmine spy object, but it also tracks the promises it
 | 
			
		||||
         * returns and provides shortcut methods for resolving those promises
 | 
			
		||||
         * synchronously.
 | 
			
		||||
         *
 | 
			
		||||
         * Usage:
 | 
			
		||||
         *
 | 
			
		||||
         * ```javascript
 | 
			
		||||
         * var moveService = new MockMoveService();
 | 
			
		||||
         *
 | 
			
		||||
         * // validate is a standard jasmine spy.
 | 
			
		||||
         * moveService.validate.and.returnValue(true);
 | 
			
		||||
         * var isValid = moveService.validate(object, parentCandidate);
 | 
			
		||||
         * expect(isValid).toBe(true);
 | 
			
		||||
         *
 | 
			
		||||
         * // perform returns promises and tracks them.
 | 
			
		||||
         * var whenCopied = jasmine.createSpy('whenCopied');
 | 
			
		||||
         * moveService.perform(object, parentObject).then(whenCopied);
 | 
			
		||||
         * expect(whenCopied).not.toHaveBeenCalled();
 | 
			
		||||
         * moveService.perform.calls.mostRecent().resolve('someArg');
 | 
			
		||||
         * expect(whenCopied).toHaveBeenCalledWith('someArg');
 | 
			
		||||
         * ```
 | 
			
		||||
         */
 | 
			
		||||
        function MockMoveService() {
 | 
			
		||||
            // track most recent call of a function,
 | 
			
		||||
            // perform automatically returns
 | 
			
		||||
            var mockMoveService = jasmine.createSpyObj(
 | 
			
		||||
                'MockMoveService',
 | 
			
		||||
                [
 | 
			
		||||
                    'validate',
 | 
			
		||||
                    'perform'
 | 
			
		||||
                ]
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            mockMoveService.perform.and.callFake(() => {
 | 
			
		||||
                var performPromise,
 | 
			
		||||
                    callExtensions,
 | 
			
		||||
                    spy;
 | 
			
		||||
 | 
			
		||||
                performPromise = jasmine.createSpyObj(
 | 
			
		||||
                    'performPromise',
 | 
			
		||||
                    ['then']
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                callExtensions = {
 | 
			
		||||
                    promise: performPromise,
 | 
			
		||||
                    resolve: function (resolveWith) {
 | 
			
		||||
                        performPromise.then.calls.all().forEach(function (call) {
 | 
			
		||||
                            call.args[0](resolveWith);
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                spy = mockMoveService.perform;
 | 
			
		||||
 | 
			
		||||
                Object.keys(callExtensions).forEach(function (key) {
 | 
			
		||||
                    spy.calls.mostRecent()[key] = callExtensions[key];
 | 
			
		||||
                    spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key];
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return performPromise;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return mockMoveService;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MockMoveService;
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -1,260 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define(
 | 
			
		||||
    [
 | 
			
		||||
        '../../src/services/MoveService',
 | 
			
		||||
        '../services/MockLinkService',
 | 
			
		||||
        '../DomainObjectFactory',
 | 
			
		||||
        '../ControlledPromise'
 | 
			
		||||
    ],
 | 
			
		||||
    function (
 | 
			
		||||
        MoveService,
 | 
			
		||||
        MockLinkService,
 | 
			
		||||
        domainObjectFactory,
 | 
			
		||||
        ControlledPromise
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        xdescribe("MoveService", function () {
 | 
			
		||||
 | 
			
		||||
            var moveService,
 | 
			
		||||
                policyService,
 | 
			
		||||
                object,
 | 
			
		||||
                objectContextCapability,
 | 
			
		||||
                currentParent,
 | 
			
		||||
                parentCandidate,
 | 
			
		||||
                linkService;
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                objectContextCapability = jasmine.createSpyObj(
 | 
			
		||||
                    'objectContextCapability',
 | 
			
		||||
                    [
 | 
			
		||||
                        'getParent'
 | 
			
		||||
                    ]
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                object = domainObjectFactory({
 | 
			
		||||
                    name: 'object',
 | 
			
		||||
                    id: 'a',
 | 
			
		||||
                    capabilities: {
 | 
			
		||||
                        context: objectContextCapability,
 | 
			
		||||
                        type: { type: 'object' }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                currentParent = domainObjectFactory({
 | 
			
		||||
                    name: 'currentParent',
 | 
			
		||||
                    id: 'b'
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                objectContextCapability.getParent.and.returnValue(currentParent);
 | 
			
		||||
 | 
			
		||||
                parentCandidate = domainObjectFactory({
 | 
			
		||||
                    name: 'parentCandidate',
 | 
			
		||||
                    model: { composition: [] },
 | 
			
		||||
                    id: 'c',
 | 
			
		||||
                    capabilities: {
 | 
			
		||||
                        type: { type: 'parentCandidate' }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                policyService = jasmine.createSpyObj(
 | 
			
		||||
                    'policyService',
 | 
			
		||||
                    ['allow']
 | 
			
		||||
                );
 | 
			
		||||
                linkService = new MockLinkService();
 | 
			
		||||
                policyService.allow.and.returnValue(true);
 | 
			
		||||
                moveService = new MoveService(policyService, linkService);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("validate", function () {
 | 
			
		||||
                var validate;
 | 
			
		||||
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    validate = function () {
 | 
			
		||||
                        return moveService.validate(object, parentCandidate);
 | 
			
		||||
                    };
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("does not allow an invalid parent", function () {
 | 
			
		||||
                    parentCandidate = undefined;
 | 
			
		||||
                    expect(validate()).toBe(false);
 | 
			
		||||
                    parentCandidate = {};
 | 
			
		||||
                    expect(validate()).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("does not allow moving to current parent", function () {
 | 
			
		||||
                    parentCandidate.id = currentParent.id = 'xyz';
 | 
			
		||||
                    expect(validate()).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("does not allow moving to self", function () {
 | 
			
		||||
                    object.id = parentCandidate.id = 'xyz';
 | 
			
		||||
                    expect(validate()).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("does not allow moving to the same location", function () {
 | 
			
		||||
                    object.id = 'abc';
 | 
			
		||||
                    parentCandidate.model.composition = ['abc'];
 | 
			
		||||
                    expect(validate()).toBe(false);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                describe("defers to policyService", function () {
 | 
			
		||||
 | 
			
		||||
                    it("calls policy service with correct args", function () {
 | 
			
		||||
                        validate();
 | 
			
		||||
                        expect(policyService.allow).toHaveBeenCalledWith(
 | 
			
		||||
                            "composition",
 | 
			
		||||
                            parentCandidate,
 | 
			
		||||
                            object
 | 
			
		||||
                        );
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("and returns false", function () {
 | 
			
		||||
                        policyService.allow.and.returnValue(false);
 | 
			
		||||
                        expect(validate()).toBe(false);
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("and returns true", function () {
 | 
			
		||||
                        policyService.allow.and.returnValue(true);
 | 
			
		||||
                        expect(validate()).toBe(true);
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            describe("perform", function () {
 | 
			
		||||
 | 
			
		||||
                var actionCapability,
 | 
			
		||||
                    locationCapability,
 | 
			
		||||
                    locationPromise,
 | 
			
		||||
                    newParent,
 | 
			
		||||
                    moveResult;
 | 
			
		||||
 | 
			
		||||
                beforeEach(function () {
 | 
			
		||||
                    newParent = parentCandidate;
 | 
			
		||||
 | 
			
		||||
                    actionCapability = jasmine.createSpyObj(
 | 
			
		||||
                        'actionCapability',
 | 
			
		||||
                        ['perform']
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    locationCapability = jasmine.createSpyObj(
 | 
			
		||||
                        'locationCapability',
 | 
			
		||||
                        [
 | 
			
		||||
                            'isOriginal',
 | 
			
		||||
                            'setPrimaryLocation',
 | 
			
		||||
                            'getContextualLocation'
 | 
			
		||||
                        ]
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    locationPromise = new ControlledPromise();
 | 
			
		||||
                    locationCapability.setPrimaryLocation
 | 
			
		||||
                        .and.returnValue(locationPromise);
 | 
			
		||||
 | 
			
		||||
                    object = domainObjectFactory({
 | 
			
		||||
                        name: 'object',
 | 
			
		||||
                        capabilities: {
 | 
			
		||||
                            action: actionCapability,
 | 
			
		||||
                            location: locationCapability,
 | 
			
		||||
                            context: objectContextCapability,
 | 
			
		||||
                            type: { type: 'object' }
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                    moveResult = moveService.perform(object, newParent);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("links object to newParent", function () {
 | 
			
		||||
                    expect(linkService.perform).toHaveBeenCalledWith(
 | 
			
		||||
                        object,
 | 
			
		||||
                        newParent
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("returns a promise", function () {
 | 
			
		||||
                    expect(moveResult.then).toEqual(jasmine.any(Function));
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("waits for result of link", function () {
 | 
			
		||||
                    expect(linkService.perform.calls.mostRecent().promise.then)
 | 
			
		||||
                        .toHaveBeenCalledWith(jasmine.any(Function));
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                it("throws an error when performed on invalid inputs", function () {
 | 
			
		||||
                    function perform() {
 | 
			
		||||
                        moveService.perform(object, newParent);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    spyOn(moveService, "validate");
 | 
			
		||||
                    moveService.validate.and.returnValue(true);
 | 
			
		||||
                    expect(perform).not.toThrow();
 | 
			
		||||
                    moveService.validate.and.returnValue(false);
 | 
			
		||||
                    expect(perform).toThrow();
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                describe("when moving an original", function () {
 | 
			
		||||
                    beforeEach(function () {
 | 
			
		||||
                        locationCapability.getContextualLocation
 | 
			
		||||
                            .and.returnValue('new-location');
 | 
			
		||||
                        locationCapability.isOriginal.and.returnValue(true);
 | 
			
		||||
                        linkService.perform.calls.mostRecent().promise.resolve();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("updates location", function () {
 | 
			
		||||
                        expect(locationCapability.setPrimaryLocation)
 | 
			
		||||
                            .toHaveBeenCalledWith('new-location');
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    describe("after location update", function () {
 | 
			
		||||
                        beforeEach(function () {
 | 
			
		||||
                            locationPromise.resolve();
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        it("removes object from parent without user warning dialog", function () {
 | 
			
		||||
                            expect(actionCapability.perform)
 | 
			
		||||
                                .toHaveBeenCalledWith('remove', true);
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                describe("when moving a link", function () {
 | 
			
		||||
                    beforeEach(function () {
 | 
			
		||||
                        locationCapability.isOriginal.and.returnValue(false);
 | 
			
		||||
                        linkService.perform.calls.mostRecent().promise.resolve();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("does not update location", function () {
 | 
			
		||||
                        expect(locationCapability.setPrimaryLocation)
 | 
			
		||||
                            .not
 | 
			
		||||
                            .toHaveBeenCalled();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    it("removes object from parent without user warning dialog", function () {
 | 
			
		||||
                        expect(actionCapability.perform)
 | 
			
		||||
                            .toHaveBeenCalledWith('remove', true);
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
@@ -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'"
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,8 @@ define([
 | 
			
		||||
                            "implementation": ExportAsJSONAction,
 | 
			
		||||
                            "category": "contextual",
 | 
			
		||||
                            "cssClass": "icon-export",
 | 
			
		||||
                            "group": "json",
 | 
			
		||||
                            "priority": 2,
 | 
			
		||||
                            "depends": [
 | 
			
		||||
                                "openmct",
 | 
			
		||||
                                "exportService",
 | 
			
		||||
@@ -61,6 +63,8 @@ define([
 | 
			
		||||
                            "implementation": ImportAsJSONAction,
 | 
			
		||||
                            "category": "contextual",
 | 
			
		||||
                            "cssClass": "icon-import",
 | 
			
		||||
                            "group": "json",
 | 
			
		||||
                            "priority": 2,
 | 
			
		||||
                            "depends": [
 | 
			
		||||
                                "exportService",
 | 
			
		||||
                                "identifierService",
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,8 @@
 | 
			
		||||
    function indexItem(id, model) {
 | 
			
		||||
        indexedItems.push({
 | 
			
		||||
            id: id,
 | 
			
		||||
            name: model.name.toLowerCase()
 | 
			
		||||
            name: model.name.toLowerCase(),
 | 
			
		||||
            type: model.type
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -125,13 +125,12 @@ define([
 | 
			
		||||
     * @param topic the topicService.
 | 
			
		||||
     */
 | 
			
		||||
    GenericSearchProvider.prototype.indexOnMutation = function (topic) {
 | 
			
		||||
        var mutationTopic = topic('mutation'),
 | 
			
		||||
            provider = this;
 | 
			
		||||
        let mutationTopic = topic('mutation');
 | 
			
		||||
 | 
			
		||||
        mutationTopic.listen(function (mutatedObject) {
 | 
			
		||||
            var editor = mutatedObject.getCapability('editor');
 | 
			
		||||
        mutationTopic.listen(mutatedObject => {
 | 
			
		||||
            let editor = mutatedObject.getCapability('editor');
 | 
			
		||||
            if (!editor || !editor.inEditContext()) {
 | 
			
		||||
                provider.index(
 | 
			
		||||
                this.index(
 | 
			
		||||
                    mutatedObject.getId(),
 | 
			
		||||
                    mutatedObject.getModel()
 | 
			
		||||
                );
 | 
			
		||||
@@ -262,6 +261,7 @@ define([
 | 
			
		||||
                return {
 | 
			
		||||
                    id: hit.item.id,
 | 
			
		||||
                    model: hit.item.model,
 | 
			
		||||
                    type: hit.item.type,
 | 
			
		||||
                    score: hit.matchCount
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,8 @@
 | 
			
		||||
        indexedItems.push({
 | 
			
		||||
            id: id,
 | 
			
		||||
            vector: vector,
 | 
			
		||||
            model: model
 | 
			
		||||
            model: model,
 | 
			
		||||
            type: model.type
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/MCT.js
									
									
									
									
									
								
							@@ -46,6 +46,8 @@ define([
 | 
			
		||||
    './api/Branding',
 | 
			
		||||
    './plugins/licenses/plugin',
 | 
			
		||||
    './plugins/remove/plugin',
 | 
			
		||||
    './plugins/move/plugin',
 | 
			
		||||
    './plugins/duplicate/plugin',
 | 
			
		||||
    'vue'
 | 
			
		||||
], function (
 | 
			
		||||
    EventEmitter,
 | 
			
		||||
@@ -73,6 +75,8 @@ define([
 | 
			
		||||
    BrandingAPI,
 | 
			
		||||
    LicensesPlugin,
 | 
			
		||||
    RemoveActionPlugin,
 | 
			
		||||
    MoveActionPlugin,
 | 
			
		||||
    DuplicateActionPlugin,
 | 
			
		||||
    Vue
 | 
			
		||||
) {
 | 
			
		||||
    /**
 | 
			
		||||
@@ -215,7 +219,7 @@ define([
 | 
			
		||||
         * @memberof module:openmct.MCT#
 | 
			
		||||
         * @name objects
 | 
			
		||||
         */
 | 
			
		||||
        this.objects = new api.ObjectAPI();
 | 
			
		||||
        this.objects = new api.ObjectAPI.default(this.types);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * An interface for retrieving and interpreting telemetry data associated
 | 
			
		||||
@@ -242,7 +246,11 @@ define([
 | 
			
		||||
 | 
			
		||||
        this.overlays = new OverlayAPI.default();
 | 
			
		||||
 | 
			
		||||
        this.contextMenu = new api.ContextMenuRegistry();
 | 
			
		||||
        this.menus = new api.MenuAPI(this);
 | 
			
		||||
 | 
			
		||||
        this.actions = new api.ActionsAPI(this);
 | 
			
		||||
 | 
			
		||||
        this.status = new api.StatusAPI(this);
 | 
			
		||||
 | 
			
		||||
        this.router = new ApplicationRouter();
 | 
			
		||||
 | 
			
		||||
@@ -259,6 +267,8 @@ define([
 | 
			
		||||
        this.install(LegacyIndicatorsPlugin());
 | 
			
		||||
        this.install(LicensesPlugin.default());
 | 
			
		||||
        this.install(RemoveActionPlugin.default());
 | 
			
		||||
        this.install(MoveActionPlugin.default());
 | 
			
		||||
        this.install(DuplicateActionPlugin.default());
 | 
			
		||||
        this.install(this.plugins.FolderView());
 | 
			
		||||
        this.install(this.plugins.Tabs());
 | 
			
		||||
        this.install(ImageryPlugin.default());
 | 
			
		||||
@@ -271,6 +281,8 @@ define([
 | 
			
		||||
        this.install(this.plugins.URLTimeSettingsSynchronizer());
 | 
			
		||||
        this.install(this.plugins.NotificationIndicator());
 | 
			
		||||
        this.install(this.plugins.NewFolderAction());
 | 
			
		||||
        this.install(this.plugins.ViewDatumAction());
 | 
			
		||||
        this.install(this.plugins.ObjectInterceptors());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MCT.prototype = Object.create(EventEmitter.prototype);
 | 
			
		||||
@@ -359,7 +371,7 @@ define([
 | 
			
		||||
     *        MCT; if undefined, MCT will be run in the body of the document
 | 
			
		||||
     */
 | 
			
		||||
    MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
 | 
			
		||||
        if (!this.plugins.DisplayLayout._installed) {
 | 
			
		||||
        if (this.types.get('layout') === undefined) {
 | 
			
		||||
            this.install(this.plugins.DisplayLayout({
 | 
			
		||||
                showAsView: ['summary-widget']
 | 
			
		||||
            }));
 | 
			
		||||
 
 | 
			
		||||
@@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
 | 
			
		||||
 | 
			
		||||
    legacyActions.filter(contextualCategoryOnly)
 | 
			
		||||
        .map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
 | 
			
		||||
        .forEach(openmct.contextMenu.registerAction);
 | 
			
		||||
        .forEach(openmct.actions.register);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,8 @@ export default class LegacyContextMenuAction {
 | 
			
		||||
        this.description = LegacyAction.definition.description;
 | 
			
		||||
        this.cssClass = LegacyAction.definition.cssClass;
 | 
			
		||||
        this.LegacyAction = LegacyAction;
 | 
			
		||||
        this.group = LegacyAction.definition.group;
 | 
			
		||||
        this.priority = LegacyAction.definition.priority;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    invoke(objectPath) {
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,7 @@ define([
 | 
			
		||||
            const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
 | 
			
		||||
            const keystring = utils.makeKeyString(newStyleObject.identifier);
 | 
			
		||||
 | 
			
		||||
            this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
 | 
			
		||||
            this.eventEmitter.emit(keystring + ":*", newStyleObject);
 | 
			
		||||
            this.eventEmitter.emit('mutation', newStyleObject);
 | 
			
		||||
        }.bind(this);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/api/actions/ActionCollection.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
class ActionCollection extends EventEmitter {
 | 
			
		||||
    constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.applicableActions = applicableActions;
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.objectPath = objectPath;
 | 
			
		||||
        this.view = view;
 | 
			
		||||
        this.skipEnvironmentObservers = skipEnvironmentObservers;
 | 
			
		||||
        this.objectUnsubscribes = [];
 | 
			
		||||
 | 
			
		||||
        let debounceOptions = {
 | 
			
		||||
            leading: false,
 | 
			
		||||
            trailing: true
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
 | 
			
		||||
        this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
 | 
			
		||||
 | 
			
		||||
        if (!skipEnvironmentObservers) {
 | 
			
		||||
            this._observeObjectPath();
 | 
			
		||||
            this.openmct.editor.on('isEditing', this._updateActions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._initializeActions();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disable(actionKeys) {
 | 
			
		||||
        actionKeys.forEach(actionKey => {
 | 
			
		||||
            if (this.applicableActions[actionKey]) {
 | 
			
		||||
                this.applicableActions[actionKey].isDisabled = true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enable(actionKeys) {
 | 
			
		||||
        actionKeys.forEach(actionKey => {
 | 
			
		||||
            if (this.applicableActions[actionKey]) {
 | 
			
		||||
                this.applicableActions[actionKey].isDisabled = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hide(actionKeys) {
 | 
			
		||||
        actionKeys.forEach(actionKey => {
 | 
			
		||||
            if (this.applicableActions[actionKey]) {
 | 
			
		||||
                this.applicableActions[actionKey].isHidden = true;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    show(actionKeys) {
 | 
			
		||||
        actionKeys.forEach(actionKey => {
 | 
			
		||||
            if (this.applicableActions[actionKey]) {
 | 
			
		||||
                this.applicableActions[actionKey].isHidden = false;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this._update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        super.removeAllListeners();
 | 
			
		||||
 | 
			
		||||
        if (!this.skipEnvironmentObservers) {
 | 
			
		||||
            this.objectUnsubscribes.forEach(unsubscribe => {
 | 
			
		||||
                unsubscribe();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.openmct.editor.off('isEditing', this._updateActions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.emit('destroy', this.view);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getVisibleActions() {
 | 
			
		||||
        let actionsArray = Object.keys(this.applicableActions);
 | 
			
		||||
        let visibleActions = [];
 | 
			
		||||
 | 
			
		||||
        actionsArray.forEach(actionKey => {
 | 
			
		||||
            let action = this.applicableActions[actionKey];
 | 
			
		||||
 | 
			
		||||
            if (!action.isHidden) {
 | 
			
		||||
                visibleActions.push(action);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return visibleActions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getStatusBarActions() {
 | 
			
		||||
        let actionsArray = Object.keys(this.applicableActions);
 | 
			
		||||
        let statusBarActions = [];
 | 
			
		||||
 | 
			
		||||
        actionsArray.forEach(actionKey => {
 | 
			
		||||
            let action = this.applicableActions[actionKey];
 | 
			
		||||
 | 
			
		||||
            if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
 | 
			
		||||
                statusBarActions.push(action);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return statusBarActions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getActionsObject() {
 | 
			
		||||
        return this.applicableActions;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _update() {
 | 
			
		||||
        this.emit('update', this.applicableActions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _observeObjectPath() {
 | 
			
		||||
        let actionCollection = this;
 | 
			
		||||
 | 
			
		||||
        function updateObject(oldObject, newObject) {
 | 
			
		||||
            Object.assign(oldObject, newObject);
 | 
			
		||||
 | 
			
		||||
            actionCollection._updateActions();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.objectPath.forEach(object => {
 | 
			
		||||
            if (object) {
 | 
			
		||||
                let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object));
 | 
			
		||||
 | 
			
		||||
                this.objectUnsubscribes.push(unsubscribe);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _initializeActions() {
 | 
			
		||||
        Object.keys(this.applicableActions).forEach(key => {
 | 
			
		||||
            this.applicableActions[key].callBack = () => {
 | 
			
		||||
                return this.applicableActions[key].invoke(this.objectPath, this.view);
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _updateActions() {
 | 
			
		||||
        let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
 | 
			
		||||
 | 
			
		||||
        this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
 | 
			
		||||
        this._initializeActions();
 | 
			
		||||
        this._update();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _mergeOldAndNewActions(oldActions, newActions) {
 | 
			
		||||
        let mergedActions = {};
 | 
			
		||||
        Object.keys(newActions).forEach(key => {
 | 
			
		||||
            if (oldActions[key]) {
 | 
			
		||||
                mergedActions[key] = oldActions[key];
 | 
			
		||||
            } else {
 | 
			
		||||
                mergedActions[key] = newActions[key];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return mergedActions;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ActionCollection;
 | 
			
		||||
							
								
								
									
										229
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/api/actions/ActionCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import ActionCollection from './ActionCollection';
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
 | 
			
		||||
describe('The ActionCollection', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let actionCollection;
 | 
			
		||||
    let mockApplicableActions;
 | 
			
		||||
    let mockObjectPath;
 | 
			
		||||
    let mockView;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        mockObjectPath = [
 | 
			
		||||
            {
 | 
			
		||||
                name: 'mock folder',
 | 
			
		||||
                type: 'fake-folder',
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    key: 'mock-folder',
 | 
			
		||||
                    namespace: ''
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'mock parent folder',
 | 
			
		||||
                type: 'fake-folder',
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    key: 'mock-parent-folder',
 | 
			
		||||
                    namespace: ''
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
 | 
			
		||||
            'create',
 | 
			
		||||
            'update'
 | 
			
		||||
        ]));
 | 
			
		||||
        mockView = {
 | 
			
		||||
            getViewContext: () => {
 | 
			
		||||
                return {
 | 
			
		||||
                    onlyAppliesToTestCase: true
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        mockApplicableActions = {
 | 
			
		||||
            'test-action-object-path': {
 | 
			
		||||
                name: 'Test Action Object Path',
 | 
			
		||||
                key: 'test-action-object-path',
 | 
			
		||||
                cssClass: 'test-action-object-path',
 | 
			
		||||
                description: 'This is a test action for object path',
 | 
			
		||||
                group: 'action',
 | 
			
		||||
                priority: 9,
 | 
			
		||||
                appliesTo: (objectPath) => {
 | 
			
		||||
                    if (objectPath.length) {
 | 
			
		||||
                        return objectPath[0].type === 'fake-folder';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return false;
 | 
			
		||||
                },
 | 
			
		||||
                invoke: () => {
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            'test-action-view': {
 | 
			
		||||
                name: 'Test Action View',
 | 
			
		||||
                key: 'test-action-view',
 | 
			
		||||
                cssClass: 'test-action-view',
 | 
			
		||||
                description: 'This is a test action for view',
 | 
			
		||||
                group: 'action',
 | 
			
		||||
                priority: 9,
 | 
			
		||||
                showInStatusBar: true,
 | 
			
		||||
                appliesTo: (objectPath, view = {}) => {
 | 
			
		||||
                    if (view.getViewContext) {
 | 
			
		||||
                        let viewContext = view.getViewContext();
 | 
			
		||||
 | 
			
		||||
                        return viewContext.onlyAppliesToTestCase;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return false;
 | 
			
		||||
                },
 | 
			
		||||
                invoke: () => {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        actionCollection.destroy();
 | 
			
		||||
        resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("disable method invoked with action keys", () => {
 | 
			
		||||
        it("marks those actions as isDisabled", () => {
 | 
			
		||||
            let actionKey = 'test-action-object-path';
 | 
			
		||||
            let actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            let action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isDisabled).toBeFalsy();
 | 
			
		||||
 | 
			
		||||
            actionCollection.disable([actionKey]);
 | 
			
		||||
            actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isDisabled).toBeTrue();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("enable method invoked with action keys", () => {
 | 
			
		||||
        it("marks the isDisabled property as false", () => {
 | 
			
		||||
            let actionKey = 'test-action-object-path';
 | 
			
		||||
 | 
			
		||||
            actionCollection.disable([actionKey]);
 | 
			
		||||
 | 
			
		||||
            let actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            let action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isDisabled).toBeTrue();
 | 
			
		||||
 | 
			
		||||
            actionCollection.enable([actionKey]);
 | 
			
		||||
            actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isDisabled).toBeFalse();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("hide method invoked with action keys", () => {
 | 
			
		||||
        it("marks those actions as isHidden", () => {
 | 
			
		||||
            let actionKey = 'test-action-object-path';
 | 
			
		||||
            let actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            let action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isHidden).toBeFalsy();
 | 
			
		||||
 | 
			
		||||
            actionCollection.hide([actionKey]);
 | 
			
		||||
            actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isHidden).toBeTrue();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("show method invoked with action keys", () => {
 | 
			
		||||
        it("marks the isHidden property as false", () => {
 | 
			
		||||
            let actionKey = 'test-action-object-path';
 | 
			
		||||
 | 
			
		||||
            actionCollection.hide([actionKey]);
 | 
			
		||||
 | 
			
		||||
            let actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            let action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isHidden).toBeTrue();
 | 
			
		||||
 | 
			
		||||
            actionCollection.show([actionKey]);
 | 
			
		||||
            actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
            action = actionsObject[actionKey];
 | 
			
		||||
 | 
			
		||||
            expect(action.isHidden).toBeFalse();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("getVisibleActions method", () => {
 | 
			
		||||
        it("returns an array of non hidden actions", () => {
 | 
			
		||||
            let action1Key = 'test-action-object-path';
 | 
			
		||||
            let action2Key = 'test-action-view';
 | 
			
		||||
 | 
			
		||||
            actionCollection.hide([action1Key]);
 | 
			
		||||
 | 
			
		||||
            let visibleActions = actionCollection.getVisibleActions();
 | 
			
		||||
 | 
			
		||||
            expect(Array.isArray(visibleActions)).toBeTrue();
 | 
			
		||||
            expect(visibleActions.length).toEqual(1);
 | 
			
		||||
            expect(visibleActions[0].key).toEqual(action2Key);
 | 
			
		||||
 | 
			
		||||
            actionCollection.show([action1Key]);
 | 
			
		||||
            visibleActions = actionCollection.getVisibleActions();
 | 
			
		||||
 | 
			
		||||
            expect(visibleActions.length).toEqual(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("getStatusBarActions method", () => {
 | 
			
		||||
        it("returns an array of non disabled, non hidden statusBar actions", () => {
 | 
			
		||||
            let action2Key = 'test-action-view';
 | 
			
		||||
 | 
			
		||||
            let statusBarActions = actionCollection.getStatusBarActions();
 | 
			
		||||
 | 
			
		||||
            expect(Array.isArray(statusBarActions)).toBeTrue();
 | 
			
		||||
            expect(statusBarActions.length).toEqual(1);
 | 
			
		||||
            expect(statusBarActions[0].key).toEqual(action2Key);
 | 
			
		||||
 | 
			
		||||
            actionCollection.disable([action2Key]);
 | 
			
		||||
            statusBarActions = actionCollection.getStatusBarActions();
 | 
			
		||||
 | 
			
		||||
            expect(statusBarActions.length).toEqual(0);
 | 
			
		||||
 | 
			
		||||
            actionCollection.enable([action2Key]);
 | 
			
		||||
            statusBarActions = actionCollection.getStatusBarActions();
 | 
			
		||||
 | 
			
		||||
            expect(statusBarActions.length).toEqual(1);
 | 
			
		||||
            expect(statusBarActions[0].key).toEqual(action2Key);
 | 
			
		||||
 | 
			
		||||
            actionCollection.hide([action2Key]);
 | 
			
		||||
            statusBarActions = actionCollection.getStatusBarActions();
 | 
			
		||||
 | 
			
		||||
            expect(statusBarActions.length).toEqual(0);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/api/actions/ActionsAPI.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import ActionCollection from './ActionCollection';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
 | 
			
		||||
class ActionsAPI extends EventEmitter {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this._allActions = {};
 | 
			
		||||
        this._actionCollections = new WeakMap();
 | 
			
		||||
        this._openmct = openmct;
 | 
			
		||||
 | 
			
		||||
        this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
 | 
			
		||||
 | 
			
		||||
        this.register = this.register.bind(this);
 | 
			
		||||
        this.get = this.get.bind(this);
 | 
			
		||||
        this._applicableActions = this._applicableActions.bind(this);
 | 
			
		||||
        this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    register(actionDefinition) {
 | 
			
		||||
        this._allActions[actionDefinition.key] = actionDefinition;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get(objectPath, view) {
 | 
			
		||||
        if (view) {
 | 
			
		||||
 | 
			
		||||
            return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
            return this._newActionCollection(objectPath, view, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateGroupOrder(groupArray) {
 | 
			
		||||
        this._groupOrder = groupArray;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _get(objectPath, view) {
 | 
			
		||||
        let actionCollection = this._newActionCollection(objectPath, view);
 | 
			
		||||
 | 
			
		||||
        this._actionCollections.set(view, actionCollection);
 | 
			
		||||
        actionCollection.on('destroy', this._updateCachedActionCollections);
 | 
			
		||||
 | 
			
		||||
        return actionCollection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _getCachedActionCollection(objectPath, view) {
 | 
			
		||||
        let cachedActionCollection = this._actionCollections.get(view);
 | 
			
		||||
 | 
			
		||||
        return cachedActionCollection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _newActionCollection(objectPath, view, skipEnvironmentObservers) {
 | 
			
		||||
        let applicableActions = this._applicableActions(objectPath, view);
 | 
			
		||||
 | 
			
		||||
        return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _updateCachedActionCollections(key) {
 | 
			
		||||
        if (this._actionCollections.has(key)) {
 | 
			
		||||
            let actionCollection = this._actionCollections.get(key);
 | 
			
		||||
            actionCollection.off('destroy', this._updateCachedActionCollections);
 | 
			
		||||
 | 
			
		||||
            this._actionCollections.delete(key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _applicableActions(objectPath, view) {
 | 
			
		||||
        let actionsObject = {};
 | 
			
		||||
 | 
			
		||||
        let keys = Object.keys(this._allActions).filter(key => {
 | 
			
		||||
            let actionDefinition = this._allActions[key];
 | 
			
		||||
 | 
			
		||||
            if (actionDefinition.appliesTo === undefined) {
 | 
			
		||||
                return true;
 | 
			
		||||
            } else {
 | 
			
		||||
                return actionDefinition.appliesTo(objectPath, view);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        keys.forEach(key => {
 | 
			
		||||
            let action = _.clone(this._allActions[key]);
 | 
			
		||||
 | 
			
		||||
            actionsObject[key] = action;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return actionsObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _groupAndSortActions(actionsArray) {
 | 
			
		||||
        if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
 | 
			
		||||
            actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let actionsObject = {};
 | 
			
		||||
        let groupedSortedActionsArray = [];
 | 
			
		||||
 | 
			
		||||
        function sortDescending(a, b) {
 | 
			
		||||
            return b.priority - a.priority;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        actionsArray.forEach(action => {
 | 
			
		||||
            if (actionsObject[action.group] === undefined) {
 | 
			
		||||
                actionsObject[action.group] = [action];
 | 
			
		||||
            } else {
 | 
			
		||||
                actionsObject[action.group].push(action);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this._groupOrder.forEach(group => {
 | 
			
		||||
            let groupArray = actionsObject[group];
 | 
			
		||||
 | 
			
		||||
            if (groupArray) {
 | 
			
		||||
                groupedSortedActionsArray.push(groupArray.sort(sortDescending));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return groupedSortedActionsArray;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ActionsAPI;
 | 
			
		||||
							
								
								
									
										153
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/api/actions/ActionsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,153 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import ActionsAPI from './ActionsAPI';
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
import ActionCollection from './ActionCollection';
 | 
			
		||||
 | 
			
		||||
describe('The Actions API', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let actionsAPI;
 | 
			
		||||
    let mockAction;
 | 
			
		||||
    let mockObjectPath;
 | 
			
		||||
    let mockObjectPathAction;
 | 
			
		||||
    let mockViewContext1;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        actionsAPI = new ActionsAPI(openmct);
 | 
			
		||||
        mockObjectPathAction = {
 | 
			
		||||
            name: 'Test Action Object Path',
 | 
			
		||||
            key: 'test-action-object-path',
 | 
			
		||||
            cssClass: 'test-action-object-path',
 | 
			
		||||
            description: 'This is a test action for object path',
 | 
			
		||||
            group: 'action',
 | 
			
		||||
            priority: 9,
 | 
			
		||||
            appliesTo: (objectPath) => {
 | 
			
		||||
                if (objectPath.length) {
 | 
			
		||||
                    return objectPath[0].type === 'fake-folder';
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            invoke: () => {
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        mockAction = {
 | 
			
		||||
            name: 'Test Action View',
 | 
			
		||||
            key: 'test-action-view',
 | 
			
		||||
            cssClass: 'test-action-view',
 | 
			
		||||
            description: 'This is a test action for view',
 | 
			
		||||
            group: 'action',
 | 
			
		||||
            priority: 9,
 | 
			
		||||
            appliesTo: (objectPath, view = {}) => {
 | 
			
		||||
                if (view.getViewContext) {
 | 
			
		||||
                    let viewContext = view.getViewContext();
 | 
			
		||||
 | 
			
		||||
                    return viewContext.onlyAppliesToTestCase;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            invoke: () => {
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        mockObjectPath = [
 | 
			
		||||
            {
 | 
			
		||||
                name: 'mock folder',
 | 
			
		||||
                type: 'fake-folder',
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    key: 'mock-folder',
 | 
			
		||||
                    namespace: ''
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'mock parent folder',
 | 
			
		||||
                type: 'fake-folder',
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    key: 'mock-parent-folder',
 | 
			
		||||
                    namespace: ''
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        mockViewContext1 = {
 | 
			
		||||
            getViewContext: () => {
 | 
			
		||||
                return {
 | 
			
		||||
                    onlyAppliesToTestCase: true
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("register method", () => {
 | 
			
		||||
        it("adds action to ActionsAPI", () => {
 | 
			
		||||
            actionsAPI.register(mockAction);
 | 
			
		||||
 | 
			
		||||
            let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
 | 
			
		||||
            let action = actionCollection.getActionsObject()[mockAction.key];
 | 
			
		||||
 | 
			
		||||
            expect(action.key).toEqual(mockAction.key);
 | 
			
		||||
            expect(action.name).toEqual(mockAction.name);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("get method", () => {
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            actionsAPI.register(mockAction);
 | 
			
		||||
            actionsAPI.register(mockObjectPathAction);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("returns an ActionCollection when invoked with an objectPath only", () => {
 | 
			
		||||
            let actionCollection = actionsAPI.get(mockObjectPath);
 | 
			
		||||
            let instanceOfActionCollection = actionCollection instanceof ActionCollection;
 | 
			
		||||
 | 
			
		||||
            expect(instanceOfActionCollection).toBeTrue();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("returns an ActionCollection when invoked with an objectPath and view", () => {
 | 
			
		||||
            let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
 | 
			
		||||
            let instanceOfActionCollection = actionCollection instanceof ActionCollection;
 | 
			
		||||
 | 
			
		||||
            expect(instanceOfActionCollection).toBeTrue();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("returns relevant actions when invoked with objectPath only", () => {
 | 
			
		||||
            let actionCollection = actionsAPI.get(mockObjectPath);
 | 
			
		||||
            let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
 | 
			
		||||
 | 
			
		||||
            expect(action.key).toEqual(mockObjectPathAction.key);
 | 
			
		||||
            expect(action.name).toEqual(mockObjectPathAction.name);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("returns relevant actions when invoked with objectPath and view", () => {
 | 
			
		||||
            let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
 | 
			
		||||
            let action = actionCollection.getActionsObject()[mockAction.key];
 | 
			
		||||
 | 
			
		||||
            expect(action.key).toEqual(mockAction.key);
 | 
			
		||||
            expect(action.name).toEqual(mockAction.name);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -28,9 +28,10 @@ define([
 | 
			
		||||
    './telemetry/TelemetryAPI',
 | 
			
		||||
    './indicators/IndicatorAPI',
 | 
			
		||||
    './notifications/NotificationAPI',
 | 
			
		||||
    './contextMenu/ContextMenuAPI',
 | 
			
		||||
    './Editor'
 | 
			
		||||
 | 
			
		||||
    './Editor',
 | 
			
		||||
    './menu/MenuAPI',
 | 
			
		||||
    './actions/ActionsAPI',
 | 
			
		||||
    './status/StatusAPI'
 | 
			
		||||
], function (
 | 
			
		||||
    TimeAPI,
 | 
			
		||||
    ObjectAPI,
 | 
			
		||||
@@ -39,8 +40,10 @@ define([
 | 
			
		||||
    TelemetryAPI,
 | 
			
		||||
    IndicatorAPI,
 | 
			
		||||
    NotificationAPI,
 | 
			
		||||
    ContextMenuAPI,
 | 
			
		||||
    EditorAPI
 | 
			
		||||
    EditorAPI,
 | 
			
		||||
    MenuAPI,
 | 
			
		||||
    ActionsAPI,
 | 
			
		||||
    StatusAPI
 | 
			
		||||
) {
 | 
			
		||||
    return {
 | 
			
		||||
        TimeAPI: TimeAPI,
 | 
			
		||||
@@ -51,6 +54,8 @@ define([
 | 
			
		||||
        IndicatorAPI: IndicatorAPI,
 | 
			
		||||
        NotificationAPI: NotificationAPI.default,
 | 
			
		||||
        EditorAPI: EditorAPI,
 | 
			
		||||
        ContextMenuRegistry: ContextMenuAPI.default
 | 
			
		||||
        MenuAPI: MenuAPI.default,
 | 
			
		||||
        ActionsAPI: ActionsAPI.default,
 | 
			
		||||
        StatusAPI: StatusAPI.default
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,17 @@ define([
 | 
			
		||||
        };
 | 
			
		||||
        this.onProviderAdd = this.onProviderAdd.bind(this);
 | 
			
		||||
        this.onProviderRemove = this.onProviderRemove.bind(this);
 | 
			
		||||
        this.mutables = {};
 | 
			
		||||
 | 
			
		||||
        if (this.domainObject.isMutable) {
 | 
			
		||||
            this.returnMutables = true;
 | 
			
		||||
            let unobserve = this.domainObject.$on('$_destroy', () => {
 | 
			
		||||
                Object.values(this.mutables).forEach(mutable => {
 | 
			
		||||
                    this.publicAPI.objects.destroyMutable(mutable);
 | 
			
		||||
                });
 | 
			
		||||
                unobserve();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -75,10 +86,6 @@ define([
 | 
			
		||||
            throw new Error('Event not supported by composition: ' + event);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.mutationListener) {
 | 
			
		||||
            this._synchronize();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.provider.on && this.provider.off) {
 | 
			
		||||
            if (event === 'add') {
 | 
			
		||||
                this.provider.on(
 | 
			
		||||
@@ -189,6 +196,13 @@ define([
 | 
			
		||||
 | 
			
		||||
            this.provider.add(this.domainObject, child.identifier);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (this.returnMutables && this.publicAPI.objects.supportsMutation(child)) {
 | 
			
		||||
                let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
 | 
			
		||||
 | 
			
		||||
                child = this.publicAPI.objects._toMutable(child);
 | 
			
		||||
                this.mutables[keyString] = child;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.emit('add', child);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
@@ -202,6 +216,8 @@ define([
 | 
			
		||||
     * @name load
 | 
			
		||||
     */
 | 
			
		||||
    CompositionCollection.prototype.load = function () {
 | 
			
		||||
        this.cleanUpMutables();
 | 
			
		||||
 | 
			
		||||
        return this.provider.load(this.domainObject)
 | 
			
		||||
            .then(function (children) {
 | 
			
		||||
                return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
 | 
			
		||||
@@ -234,6 +250,14 @@ define([
 | 
			
		||||
        if (!skipMutate) {
 | 
			
		||||
            this.provider.remove(this.domainObject, child.identifier);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (this.returnMutables) {
 | 
			
		||||
                let keyString = this.publicAPI.objects.makeKeyString(child);
 | 
			
		||||
                if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
 | 
			
		||||
                    this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
 | 
			
		||||
                    delete this.mutables[keyString];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.emit('remove', child);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
@@ -281,12 +305,6 @@ define([
 | 
			
		||||
        this.remove(child, true);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    CompositionCollection.prototype._synchronize = function () {
 | 
			
		||||
        this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
 | 
			
		||||
            this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    CompositionCollection.prototype._destroy = function () {
 | 
			
		||||
        if (this.mutationListener) {
 | 
			
		||||
            this.mutationListener();
 | 
			
		||||
@@ -308,5 +326,11 @@ define([
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    CompositionCollection.prototype.cleanUpMutables = function () {
 | 
			
		||||
        Object.values(this.mutables).forEach(mutable => {
 | 
			
		||||
            this.publicAPI.objects.destroyMutable(mutable);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return CompositionCollection;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-menu">
 | 
			
		||||
    <ul>
 | 
			
		||||
        <li
 | 
			
		||||
            v-for="action in actions"
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            @click="action.invoke(objectPath)"
 | 
			
		||||
        >
 | 
			
		||||
            {{ action.name }}
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-if="actions.length === 0">
 | 
			
		||||
            No actions defined.
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['actions', 'objectPath']
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,159 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import ContextMenuComponent from './ContextMenu.vue';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
 | 
			
		||||
 * custom HTML elements.
 | 
			
		||||
 * @interface ContextMenuAPI
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
class ContextMenuAPI {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this._allActions = [];
 | 
			
		||||
        this._activeContextMenu = undefined;
 | 
			
		||||
 | 
			
		||||
        this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
 | 
			
		||||
        this.registerAction = this.registerAction.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
 | 
			
		||||
     * selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
 | 
			
		||||
     *
 | 
			
		||||
     * @interface ContextMenuAction
 | 
			
		||||
     * @memberof module:openmct
 | 
			
		||||
     * @property {string} name the human-readable name of this view
 | 
			
		||||
     * @property {string} description a longer-form description (typically
 | 
			
		||||
     *           a single sentence or short paragraph) of this kind of view
 | 
			
		||||
     * @property {string} cssClass the CSS class to apply to labels for this
 | 
			
		||||
     *           view (to add icons, for instance)
 | 
			
		||||
     * @property {string} key unique key to identify the context menu action
 | 
			
		||||
     *           (used in custom context menu eg table rows, to identify which actions to include)
 | 
			
		||||
     * @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
 | 
			
		||||
     */
 | 
			
		||||
    /**
 | 
			
		||||
     * @method appliesTo
 | 
			
		||||
     * @memberof module:openmct.ContextMenuAction#
 | 
			
		||||
     * @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
 | 
			
		||||
     * @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
 | 
			
		||||
     */
 | 
			
		||||
    /**
 | 
			
		||||
     * Code to be executed when the action is selected from a context menu
 | 
			
		||||
     * @method invoke
 | 
			
		||||
     * @memberof module:openmct.ContextMenuAction#
 | 
			
		||||
     * @param {DomainObject[]} objectPath the path of the object to invoke the action on.
 | 
			
		||||
     */
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {ContextMenuAction} actionDefinition
 | 
			
		||||
     */
 | 
			
		||||
    registerAction(actionDefinition) {
 | 
			
		||||
        this._allActions.push(actionDefinition);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
 | 
			
		||||
 | 
			
		||||
        let applicableActions = this._allActions.filter((action) => {
 | 
			
		||||
 | 
			
		||||
            if (actionsToBeIncluded) {
 | 
			
		||||
                if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
 | 
			
		||||
            } else {
 | 
			
		||||
                if (action.appliesTo === undefined) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (this._activeContextMenu) {
 | 
			
		||||
            this._hideActiveContextMenu();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
 | 
			
		||||
        this._activeContextMenu.$mount();
 | 
			
		||||
        document.body.appendChild(this._activeContextMenu.$el);
 | 
			
		||||
 | 
			
		||||
        let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
 | 
			
		||||
        this._activeContextMenu.$el.style.left = `${position.x}px`;
 | 
			
		||||
        this._activeContextMenu.$el.style.top = `${position.y}px`;
 | 
			
		||||
 | 
			
		||||
        document.addEventListener('click', this._hideActiveContextMenu);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _calculatePopupPosition(eventPosX, eventPosY, menuElement) {
 | 
			
		||||
        let menuDimensions = menuElement.getBoundingClientRect();
 | 
			
		||||
        let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
 | 
			
		||||
        let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
 | 
			
		||||
 | 
			
		||||
        if (overflowX > 0) {
 | 
			
		||||
            eventPosX = eventPosX - overflowX;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (overflowY > 0) {
 | 
			
		||||
            eventPosY = eventPosY - overflowY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            x: eventPosX,
 | 
			
		||||
            y: eventPosY
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _hideActiveContextMenu() {
 | 
			
		||||
        document.removeEventListener('click', this._hideActiveContextMenu);
 | 
			
		||||
        document.body.removeChild(this._activeContextMenu.$el);
 | 
			
		||||
        this._activeContextMenu.$destroy();
 | 
			
		||||
        this._activeContextMenu = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _createContextMenuForObject(objectPath, actions) {
 | 
			
		||||
        return new Vue({
 | 
			
		||||
            components: {
 | 
			
		||||
                ContextMenu: ContextMenuComponent
 | 
			
		||||
            },
 | 
			
		||||
            provide: {
 | 
			
		||||
                actions: actions,
 | 
			
		||||
                objectPath: objectPath
 | 
			
		||||
            },
 | 
			
		||||
            template: '<ContextMenu></ContextMenu>'
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
export default ContextMenuAPI;
 | 
			
		||||
							
								
								
									
										68
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/api/menu/MenuAPI.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import Menu from './menu.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
 | 
			
		||||
 * custom HTML elements.
 | 
			
		||||
 * @interface MenuAPI
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
class MenuAPI {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
 | 
			
		||||
        this.showMenu = this.showMenu.bind(this);
 | 
			
		||||
        this._clearMenuComponent = this._clearMenuComponent.bind(this);
 | 
			
		||||
        this._showObjectMenu = this._showObjectMenu.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showMenu(x, y, actions, onDestroy) {
 | 
			
		||||
        if (this.menuComponent) {
 | 
			
		||||
            this.menuComponent.dismiss();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let options = {
 | 
			
		||||
            x,
 | 
			
		||||
            y,
 | 
			
		||||
            actions,
 | 
			
		||||
            onDestroy
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.menuComponent = new Menu(options);
 | 
			
		||||
        this.menuComponent.once('destroy', this._clearMenuComponent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _clearMenuComponent() {
 | 
			
		||||
        this.menuComponent = undefined;
 | 
			
		||||
        delete this.menuComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
 | 
			
		||||
        let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
 | 
			
		||||
 | 
			
		||||
        this.showMenu(x, y, applicableActions);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
export default MenuAPI;
 | 
			
		||||
							
								
								
									
										134
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/api/menu/MenuAPISpec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import MenuAPI from './MenuAPI';
 | 
			
		||||
import Menu from './menu';
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
 | 
			
		||||
describe ('The Menu API', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let menuAPI;
 | 
			
		||||
    let actionsArray;
 | 
			
		||||
    let x;
 | 
			
		||||
    let y;
 | 
			
		||||
    let result;
 | 
			
		||||
    let onDestroy;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        menuAPI = new MenuAPI(openmct);
 | 
			
		||||
        actionsArray = [
 | 
			
		||||
            {
 | 
			
		||||
                name: 'Test Action 1',
 | 
			
		||||
                cssClass: 'test-css-class-1',
 | 
			
		||||
                description: 'This is a test action',
 | 
			
		||||
                callBack: () => {
 | 
			
		||||
                    result = 'Test Action 1 Invoked';
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                name: 'Test Action 2',
 | 
			
		||||
                cssClass: 'test-css-class-2',
 | 
			
		||||
                description: 'This is a test action',
 | 
			
		||||
                callBack: () => {
 | 
			
		||||
                    result = 'Test Action 2 Invoked';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        x = 8;
 | 
			
		||||
        y = 16;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("showMenu method", () => {
 | 
			
		||||
        it("creates an instance of Menu when invoked", () => {
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray);
 | 
			
		||||
 | 
			
		||||
            expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe("creates a menu component", () => {
 | 
			
		||||
            let menuComponent;
 | 
			
		||||
            let vueComponent;
 | 
			
		||||
 | 
			
		||||
            beforeEach(() => {
 | 
			
		||||
                onDestroy = jasmine.createSpy('onDestroy');
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, onDestroy);
 | 
			
		||||
                vueComponent = menuAPI.menuComponent.component;
 | 
			
		||||
                menuComponent = document.querySelector(".c-menu");
 | 
			
		||||
 | 
			
		||||
                spyOn(vueComponent, '$destroy');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("renders a menu component in the expected x and y coordinates", () => {
 | 
			
		||||
                let boundingClientRect = menuComponent.getBoundingClientRect();
 | 
			
		||||
                let left = boundingClientRect.left;
 | 
			
		||||
                let top = boundingClientRect.top;
 | 
			
		||||
 | 
			
		||||
                expect(left).toEqual(x);
 | 
			
		||||
                expect(top).toEqual(y);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("with all the actions passed in", () => {
 | 
			
		||||
                expect(menuComponent).toBeDefined();
 | 
			
		||||
 | 
			
		||||
                let listItems = menuComponent.children[0].children;
 | 
			
		||||
 | 
			
		||||
                expect(listItems.length).toEqual(actionsArray.length);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("with click-able menu items, that will invoke the correct callBacks", () => {
 | 
			
		||||
                let listItem1 = menuComponent.children[0].children[0];
 | 
			
		||||
 | 
			
		||||
                listItem1.click();
 | 
			
		||||
 | 
			
		||||
                expect(result).toEqual("Test Action 1 Invoked");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("dismisses the menu when action is clicked on", () => {
 | 
			
		||||
                let listItem1 = menuComponent.children[0].children[0];
 | 
			
		||||
 | 
			
		||||
                listItem1.click();
 | 
			
		||||
 | 
			
		||||
                let menu = document.querySelector('.c-menu');
 | 
			
		||||
 | 
			
		||||
                expect(menu).toBeNull();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("invokes the destroy method when menu is dismissed", () => {
 | 
			
		||||
                document.body.click();
 | 
			
		||||
 | 
			
		||||
                expect(vueComponent.$destroy).toHaveBeenCalled();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("invokes the onDestroy callback if passed in", () => {
 | 
			
		||||
                document.body.click();
 | 
			
		||||
 | 
			
		||||
                expect(onDestroy).toHaveBeenCalled();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/api/menu/components/Menu.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-menu">
 | 
			
		||||
    <ul v-if="actions.length && actions[0].length">
 | 
			
		||||
        <template
 | 
			
		||||
            v-for="(actionGroups, index) in actions"
 | 
			
		||||
        >
 | 
			
		||||
            <li
 | 
			
		||||
                v-for="action in actionGroups"
 | 
			
		||||
                :key="action.name"
 | 
			
		||||
                :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
                :title="action.description"
 | 
			
		||||
                @click="action.callBack"
 | 
			
		||||
            >
 | 
			
		||||
                {{ action.name }}
 | 
			
		||||
            </li>
 | 
			
		||||
            <div
 | 
			
		||||
                v-if="index !== actions.length - 1"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                class="c-menu__section-separator"
 | 
			
		||||
            >
 | 
			
		||||
            </div>
 | 
			
		||||
            <li
 | 
			
		||||
                v-if="actionGroups.length === 0"
 | 
			
		||||
                :key="index"
 | 
			
		||||
            >
 | 
			
		||||
                No actions defined.
 | 
			
		||||
            </li>
 | 
			
		||||
        </template>
 | 
			
		||||
    </ul>
 | 
			
		||||
 | 
			
		||||
    <ul v-else>
 | 
			
		||||
        <li
 | 
			
		||||
            v-for="action in actions"
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            @click="action.callBack"
 | 
			
		||||
        >
 | 
			
		||||
            {{ action.name }}
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-if="actions.length === 0">
 | 
			
		||||
            No actions defined.
 | 
			
		||||
        </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['actions']
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/api/menu/menu.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import MenuComponent from './components/Menu.vue';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
class Menu extends EventEmitter {
 | 
			
		||||
    constructor(options) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.options = options;
 | 
			
		||||
 | 
			
		||||
        this.component = new Vue({
 | 
			
		||||
            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;
 | 
			
		||||
							
								
								
									
										66
									
								
								src/api/objects/InterceptorRegistry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/api/objects/InterceptorRegistry.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
export default class InterceptorRegistry {
 | 
			
		||||
    /**
 | 
			
		||||
     * A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
 | 
			
		||||
     * @interface InterceptorRegistry
 | 
			
		||||
     * @memberof module:openmct
 | 
			
		||||
     */
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.interceptors = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @interface InterceptorDef
 | 
			
		||||
     * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
 | 
			
		||||
     * @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
 | 
			
		||||
     * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
 | 
			
		||||
     * @memberof module:openmct InterceptorRegistry#
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a new object interceptor.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
 | 
			
		||||
     * @method addInterceptor
 | 
			
		||||
     * @memberof module:openmct.InterceptorRegistry#
 | 
			
		||||
     */
 | 
			
		||||
    addInterceptor(interceptorDef) {
 | 
			
		||||
        //TODO: sort by priority
 | 
			
		||||
        this.interceptors.push(interceptorDef);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve all interceptors applicable to a domain object.
 | 
			
		||||
     * @method getInterceptors
 | 
			
		||||
     * @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
 | 
			
		||||
     * @memberof module:openmct.InterceptorRegistry#
 | 
			
		||||
     */
 | 
			
		||||
    getInterceptors(identifier, object) {
 | 
			
		||||
        return this.interceptors.filter(interceptor => {
 | 
			
		||||
            return typeof interceptor.appliesTo === 'function'
 | 
			
		||||
                && interceptor.appliesTo(identifier, object);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								src/api/objects/InterceptorRegistrySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/api/objects/InterceptorRegistrySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										137
									
								
								src/api/objects/MutableDomainObject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/api/objects/MutableDomainObject.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import utils from './object-utils.js';
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
 | 
			
		||||
const ANY_OBJECT_EVENT = 'mutation';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps a domain object to keep its model synchronized with other instances of the same object.
 | 
			
		||||
 *
 | 
			
		||||
 * Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers
 | 
			
		||||
 * should be careful to destroy MutableDomainObject in order to avoid memory leaks.
 | 
			
		||||
 *
 | 
			
		||||
 * All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except
 | 
			
		||||
 * `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`
 | 
			
		||||
 * will result in the creation of a new MutableDomainObject and you will be responsible for destroying it
 | 
			
		||||
 * (via openmct.objects.destroy) when you're done with it.
 | 
			
		||||
 *
 | 
			
		||||
 * @typedef MutableDomainObject
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
class MutableDomainObject {
 | 
			
		||||
    constructor(eventEmitter) {
 | 
			
		||||
        Object.defineProperties(this, {
 | 
			
		||||
            _globalEventEmitter: {
 | 
			
		||||
                value: eventEmitter,
 | 
			
		||||
                // Property should not be serialized
 | 
			
		||||
                enumerable: false
 | 
			
		||||
            },
 | 
			
		||||
            _instanceEventEmitter: {
 | 
			
		||||
                value: new EventEmitter(),
 | 
			
		||||
                // Property should not be serialized
 | 
			
		||||
                enumerable: false
 | 
			
		||||
            },
 | 
			
		||||
            _observers: {
 | 
			
		||||
                value: [],
 | 
			
		||||
                // Property should not be serialized
 | 
			
		||||
                enumerable: false
 | 
			
		||||
            },
 | 
			
		||||
            isMutable: {
 | 
			
		||||
                value: true,
 | 
			
		||||
                // Property should not be serialized
 | 
			
		||||
                enumerable: false
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    $observe(path, callback) {
 | 
			
		||||
        let fullPath = qualifiedEventName(this, path);
 | 
			
		||||
        let eventOff =
 | 
			
		||||
            this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
 | 
			
		||||
 | 
			
		||||
        this._globalEventEmitter.on(fullPath, callback);
 | 
			
		||||
        this._observers.push(eventOff);
 | 
			
		||||
 | 
			
		||||
        return eventOff;
 | 
			
		||||
    }
 | 
			
		||||
    $set(path, value) {
 | 
			
		||||
        _.set(this, path, value);
 | 
			
		||||
        _.set(this, 'modified', Date.now());
 | 
			
		||||
 | 
			
		||||
        //Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
 | 
			
		||||
        this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
 | 
			
		||||
 | 
			
		||||
        //Emit a general "any object" event
 | 
			
		||||
        this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
 | 
			
		||||
        //Emit wildcard event, with path so that callback knows what changed
 | 
			
		||||
        this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
 | 
			
		||||
 | 
			
		||||
        //Emit events specific to properties affected
 | 
			
		||||
        let parentPropertiesList = path.split('.');
 | 
			
		||||
        for (let index = parentPropertiesList.length; index > 0; index--) {
 | 
			
		||||
            let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
 | 
			
		||||
            this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //TODO: Emit events for listeners of child properties when parent changes.
 | 
			
		||||
        // Do it at observer time - also register observers for parent attribute path.
 | 
			
		||||
    }
 | 
			
		||||
    $on(event, callback) {
 | 
			
		||||
        this._instanceEventEmitter.on(event, callback);
 | 
			
		||||
 | 
			
		||||
        return () => this._instanceEventEmitter.off(event, callback);
 | 
			
		||||
    }
 | 
			
		||||
    $destroy() {
 | 
			
		||||
        this._observers.forEach(observer => observer());
 | 
			
		||||
        delete this._globalEventEmitter;
 | 
			
		||||
        delete this._observers;
 | 
			
		||||
        this._instanceEventEmitter.emit('$_destroy');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static createMutable(object, mutationTopic) {
 | 
			
		||||
        let mutable = Object.create(new MutableDomainObject(mutationTopic));
 | 
			
		||||
        Object.assign(mutable, object);
 | 
			
		||||
 | 
			
		||||
        mutable.$observe('$_synchronize_model', (updatedObject) => {
 | 
			
		||||
            let clone = JSON.parse(JSON.stringify(updatedObject));
 | 
			
		||||
            let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
 | 
			
		||||
            deleted.forEach((propertyName) => delete mutable[propertyName]);
 | 
			
		||||
            Object.assign(mutable, clone);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return mutable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static mutateObject(object, path, value) {
 | 
			
		||||
        _.set(object, path, value);
 | 
			
		||||
        _.set(object, 'modified', Date.now());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function qualifiedEventName(object, eventName) {
 | 
			
		||||
    let keystring = utils.makeKeyString(object.identifier);
 | 
			
		||||
 | 
			
		||||
    return [keystring, eventName].join(':');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MutableDomainObject;
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    'objectUtils',
 | 
			
		||||
    'lodash'
 | 
			
		||||
], function (
 | 
			
		||||
    utils,
 | 
			
		||||
    _
 | 
			
		||||
) {
 | 
			
		||||
    const ANY_OBJECT_EVENT = "mutation";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The MutableObject wraps a DomainObject and provides getters and
 | 
			
		||||
     * setters for
 | 
			
		||||
     * @param eventEmitter
 | 
			
		||||
     * @param object
 | 
			
		||||
     * @interface MutableObject
 | 
			
		||||
     */
 | 
			
		||||
    function MutableObject(eventEmitter, object) {
 | 
			
		||||
        this.eventEmitter = eventEmitter;
 | 
			
		||||
        this.object = object;
 | 
			
		||||
        this.unlisteners = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function qualifiedEventName(object, eventName) {
 | 
			
		||||
        const keystring = utils.makeKeyString(object.identifier);
 | 
			
		||||
 | 
			
		||||
        return [keystring, eventName].join(':');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MutableObject.prototype.stopListening = function () {
 | 
			
		||||
        this.unlisteners.forEach(function (unlisten) {
 | 
			
		||||
            unlisten();
 | 
			
		||||
        });
 | 
			
		||||
        this.unlisteners = [];
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Observe changes to this domain object.
 | 
			
		||||
     * @param {string} path the property to observe
 | 
			
		||||
     * @param {Function} callback a callback to invoke when new values for
 | 
			
		||||
     *        this property are observed
 | 
			
		||||
     * @method on
 | 
			
		||||
     * @memberof module:openmct.MutableObject#
 | 
			
		||||
     */
 | 
			
		||||
    MutableObject.prototype.on = function (path, callback) {
 | 
			
		||||
        const fullPath = qualifiedEventName(this.object, path);
 | 
			
		||||
        const eventOff =
 | 
			
		||||
            this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback);
 | 
			
		||||
 | 
			
		||||
        this.eventEmitter.on(fullPath, callback);
 | 
			
		||||
        this.unlisteners.push(eventOff);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modify this domain object.
 | 
			
		||||
     * @param {string} path the property to modify
 | 
			
		||||
     * @param {*} value the new value for this property
 | 
			
		||||
     * @method set
 | 
			
		||||
     * @memberof module:openmct.MutableObject#
 | 
			
		||||
     */
 | 
			
		||||
    MutableObject.prototype.set = function (path, value) {
 | 
			
		||||
        _.set(this.object, path, value);
 | 
			
		||||
        _.set(this.object, 'modified', Date.now());
 | 
			
		||||
 | 
			
		||||
        const handleRecursiveMutation = function (newObject) {
 | 
			
		||||
            this.object = newObject;
 | 
			
		||||
        }.bind(this);
 | 
			
		||||
 | 
			
		||||
        //Emit wildcard event
 | 
			
		||||
        this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
 | 
			
		||||
        //Emit a general "any object" event
 | 
			
		||||
        this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object);
 | 
			
		||||
 | 
			
		||||
        this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
 | 
			
		||||
        //Emit event specific to property
 | 
			
		||||
        this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
 | 
			
		||||
        this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return MutableObject;
 | 
			
		||||
});
 | 
			
		||||
@@ -20,337 +20,425 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    'lodash',
 | 
			
		||||
    'objectUtils',
 | 
			
		||||
    './MutableObject',
 | 
			
		||||
    './RootRegistry',
 | 
			
		||||
    './RootObjectProvider',
 | 
			
		||||
    'EventEmitter'
 | 
			
		||||
], function (
 | 
			
		||||
    _,
 | 
			
		||||
    utils,
 | 
			
		||||
    MutableObject,
 | 
			
		||||
    RootRegistry,
 | 
			
		||||
    RootObjectProvider,
 | 
			
		||||
    EventEmitter
 | 
			
		||||
) {
 | 
			
		||||
import utils from 'objectUtils';
 | 
			
		||||
import MutableDomainObject from './MutableDomainObject';
 | 
			
		||||
import RootRegistry from './RootRegistry';
 | 
			
		||||
import RootObjectProvider from './RootObjectProvider';
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import InterceptorRegistry from './InterceptorRegistry';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Utilities for loading, saving, and manipulating domain objects.
 | 
			
		||||
     * @interface ObjectAPI
 | 
			
		||||
     * @memberof module:openmct
 | 
			
		||||
     */
 | 
			
		||||
/**
 | 
			
		||||
 * Utilities for loading, saving, and manipulating domain objects.
 | 
			
		||||
 * @interface ObjectAPI
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    function ObjectAPI() {
 | 
			
		||||
        this.eventEmitter = new EventEmitter();
 | 
			
		||||
        this.providers = {};
 | 
			
		||||
        this.rootRegistry = new RootRegistry();
 | 
			
		||||
        this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
 | 
			
		||||
        this.cache = {};
 | 
			
		||||
function ObjectAPI(typeRegistry) {
 | 
			
		||||
    this.typeRegistry = typeRegistry;
 | 
			
		||||
    this.eventEmitter = new EventEmitter();
 | 
			
		||||
    this.providers = {};
 | 
			
		||||
    this.rootRegistry = new RootRegistry();
 | 
			
		||||
    this.rootProvider = new RootObjectProvider(this.rootRegistry);
 | 
			
		||||
    this.cache = {};
 | 
			
		||||
    this.interceptorRegistry = new InterceptorRegistry();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set fallback provider, this is an internal API for legacy reasons.
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
 | 
			
		||||
    this.fallbackProvider = p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Retrieve the provider for a given identifier.
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.getProvider = function (identifier) {
 | 
			
		||||
    if (identifier.key === 'ROOT') {
 | 
			
		||||
        return this.rootProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set fallback provider, this is an internal API for legacy reasons.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
 | 
			
		||||
        this.fallbackProvider = p;
 | 
			
		||||
    };
 | 
			
		||||
    return this.providers[identifier.namespace] || this.fallbackProvider;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve the provider for a given identifier.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.getProvider = function (identifier) {
 | 
			
		||||
        if (identifier.key === 'ROOT') {
 | 
			
		||||
            return this.rootProvider;
 | 
			
		||||
        }
 | 
			
		||||
/**
 | 
			
		||||
 * Get the root-level object.
 | 
			
		||||
 * @returns {Promise.<DomainObject>} a promise for the root object
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.getRoot = function () {
 | 
			
		||||
    return this.rootProvider.get();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
        return this.providers[identifier.namespace] || this.fallbackProvider;
 | 
			
		||||
    };
 | 
			
		||||
/**
 | 
			
		||||
 * Register a new object provider for a particular namespace.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {string} namespace the namespace for which to provide objects
 | 
			
		||||
 * @param {module:openmct.ObjectProvider} provider the provider which
 | 
			
		||||
 *        will handle loading domain objects from this namespace
 | 
			
		||||
 * @memberof {module:openmct.ObjectAPI#}
 | 
			
		||||
 * @name addProvider
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
 | 
			
		||||
    this.providers[namespace] = provider;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the root-level object.
 | 
			
		||||
     * @returns {Promise.<DomainObject>} a promise for the root object
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.getRoot = function () {
 | 
			
		||||
        return this.rootProvider.get();
 | 
			
		||||
    };
 | 
			
		||||
/**
 | 
			
		||||
 * Provides the ability to read, write, and delete domain objects.
 | 
			
		||||
 *
 | 
			
		||||
 * When registering a new object provider, all methods on this interface
 | 
			
		||||
 * are optional.
 | 
			
		||||
 *
 | 
			
		||||
 * @interface ObjectProvider
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a new object provider for a particular namespace.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} namespace the namespace for which to provide objects
 | 
			
		||||
     * @param {module:openmct.ObjectProvider} provider the provider which
 | 
			
		||||
     *        will handle loading domain objects from this namespace
 | 
			
		||||
     * @memberof {module:openmct.ObjectAPI#}
 | 
			
		||||
     * @name addProvider
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.addProvider = function (namespace, provider) {
 | 
			
		||||
        this.providers[namespace] = provider;
 | 
			
		||||
    };
 | 
			
		||||
/**
 | 
			
		||||
 * Create the given domain object in the corresponding persistence store
 | 
			
		||||
 *
 | 
			
		||||
 * @method create
 | 
			
		||||
 * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
 * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
 *        create
 | 
			
		||||
 * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
 *          has been created, or be rejected if it cannot be saved
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Provides the ability to read, write, and delete domain objects.
 | 
			
		||||
     *
 | 
			
		||||
     * When registering a new object provider, all methods on this interface
 | 
			
		||||
     * are optional.
 | 
			
		||||
     *
 | 
			
		||||
     * @interface ObjectProvider
 | 
			
		||||
     * @memberof module:openmct
 | 
			
		||||
     */
 | 
			
		||||
/**
 | 
			
		||||
 * Update this domain object in its persistence store
 | 
			
		||||
 *
 | 
			
		||||
 * @method update
 | 
			
		||||
 * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
 * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
 *        update
 | 
			
		||||
 * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
 *          has been updated, or be rejected if it cannot be saved
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the given domain object in the corresponding persistence store
 | 
			
		||||
     *
 | 
			
		||||
     * @method create
 | 
			
		||||
     * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
     * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
     *        create
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been created, or be rejected if it cannot be saved
 | 
			
		||||
     */
 | 
			
		||||
/**
 | 
			
		||||
 * Delete this domain object.
 | 
			
		||||
 *
 | 
			
		||||
 * @method delete
 | 
			
		||||
 * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
 * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
 *        delete
 | 
			
		||||
 * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
 *          has been deleted, or be rejected if it cannot be deleted
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update this domain object in its persistence store
 | 
			
		||||
     *
 | 
			
		||||
     * @method update
 | 
			
		||||
     * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
     * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
     *        update
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been updated, or be rejected if it cannot be saved
 | 
			
		||||
     */
 | 
			
		||||
/**
 | 
			
		||||
 * Get a domain object.
 | 
			
		||||
 *
 | 
			
		||||
 * @method get
 | 
			
		||||
 * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
 * @param {string} key the key for the domain object to load
 | 
			
		||||
 * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
 *          has been saved, or be rejected if it cannot be saved
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete this domain object.
 | 
			
		||||
     *
 | 
			
		||||
     * @method delete
 | 
			
		||||
     * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
     * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
     *        delete
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been deleted, or be rejected if it cannot be deleted
 | 
			
		||||
     */
 | 
			
		||||
ObjectAPI.prototype.get = function (identifier) {
 | 
			
		||||
    let keystring = this.makeKeyString(identifier);
 | 
			
		||||
    if (this.cache[keystring] !== undefined) {
 | 
			
		||||
        return this.cache[keystring];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a domain object.
 | 
			
		||||
     *
 | 
			
		||||
     * @method get
 | 
			
		||||
     * @memberof module:openmct.ObjectProvider#
 | 
			
		||||
     * @param {string} key the key for the domain object to load
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been saved, or be rejected if it cannot be saved
 | 
			
		||||
     */
 | 
			
		||||
    identifier = utils.parseKeyString(identifier);
 | 
			
		||||
    const provider = this.getProvider(identifier);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a domain object.
 | 
			
		||||
     *
 | 
			
		||||
     * @method get
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     * @param {module:openmct.ObjectAPI~Identifier} identifier
 | 
			
		||||
     *        the identifier for the domain object to load
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been saved, or be rejected if it cannot be saved
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.get = function (identifier) {
 | 
			
		||||
        let keystring = this.makeKeyString(identifier);
 | 
			
		||||
        if (this.cache[keystring] !== undefined) {
 | 
			
		||||
            return this.cache[keystring];
 | 
			
		||||
        }
 | 
			
		||||
    if (!provider) {
 | 
			
		||||
        throw new Error('No Provider Matched');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        identifier = utils.parseKeyString(identifier);
 | 
			
		||||
        const provider = this.getProvider(identifier);
 | 
			
		||||
    if (!provider.get) {
 | 
			
		||||
        throw new Error('Provider does not support get!');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        if (!provider) {
 | 
			
		||||
            throw new Error('No Provider Matched');
 | 
			
		||||
        }
 | 
			
		||||
    let objectPromise = provider.get(identifier);
 | 
			
		||||
    this.cache[keystring] = objectPromise;
 | 
			
		||||
 | 
			
		||||
        if (!provider.get) {
 | 
			
		||||
            throw new Error('Provider does not support get!');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let objectPromise = provider.get(identifier);
 | 
			
		||||
 | 
			
		||||
        this.cache[keystring] = objectPromise;
 | 
			
		||||
 | 
			
		||||
        return objectPromise.then(result => {
 | 
			
		||||
            delete this.cache[keystring];
 | 
			
		||||
 | 
			
		||||
            return result;
 | 
			
		||||
    return objectPromise.then(result => {
 | 
			
		||||
        delete this.cache[keystring];
 | 
			
		||||
        const interceptors = this.listGetInterceptors(identifier, result);
 | 
			
		||||
        interceptors.forEach(interceptor => {
 | 
			
		||||
            result = interceptor.invoke(identifier, result);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectAPI.prototype.delete = function () {
 | 
			
		||||
        throw new Error('Delete not implemented');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectAPI.prototype.isPersistable = function (domainObject) {
 | 
			
		||||
        let provider = this.getProvider(domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
        return provider !== undefined
 | 
			
		||||
            && provider.create !== undefined
 | 
			
		||||
            && provider.update !== undefined;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save this domain object in its current state. EXPERIMENTAL
 | 
			
		||||
     *
 | 
			
		||||
     * @private
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
     *        save
 | 
			
		||||
     * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
     *          has been saved, or be rejected if it cannot be saved
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.save = function (domainObject) {
 | 
			
		||||
        let provider = this.getProvider(domainObject.identifier);
 | 
			
		||||
        let savedResolve;
 | 
			
		||||
        let result;
 | 
			
		||||
 | 
			
		||||
        if (!this.isPersistable(domainObject)) {
 | 
			
		||||
            result = Promise.reject('Object provider does not support saving');
 | 
			
		||||
        } else if (hasAlreadyBeenPersisted(domainObject)) {
 | 
			
		||||
            result = Promise.resolve(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            const persistedTime = Date.now();
 | 
			
		||||
            if (domainObject.persisted === undefined) {
 | 
			
		||||
                result = new Promise((resolve) => {
 | 
			
		||||
                    savedResolve = resolve;
 | 
			
		||||
                });
 | 
			
		||||
                domainObject.persisted = persistedTime;
 | 
			
		||||
                provider.create(domainObject).then((response) => {
 | 
			
		||||
                    this.mutate(domainObject, 'persisted', persistedTime);
 | 
			
		||||
                    savedResolve(response);
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                domainObject.persisted = persistedTime;
 | 
			
		||||
                this.mutate(domainObject, 'persisted', persistedTime);
 | 
			
		||||
                result = provider.update(domainObject);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a root-level object.
 | 
			
		||||
     * @param {module:openmct.ObjectAPI~Identifier|function} an array of
 | 
			
		||||
     *        identifiers for root level objects, or a function that returns a
 | 
			
		||||
     *        promise for an identifier or an array of root level objects.
 | 
			
		||||
     * @method addRoot
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.addRoot = function (key) {
 | 
			
		||||
        this.rootRegistry.addRoot(key);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Modify a domain object.
 | 
			
		||||
     * @param {module:openmct.DomainObject} object the object to mutate
 | 
			
		||||
     * @param {string} path the property to modify
 | 
			
		||||
     * @param {*} value the new value for this property
 | 
			
		||||
     * @method mutate
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.mutate = function (domainObject, path, value) {
 | 
			
		||||
        const mutableObject =
 | 
			
		||||
            new MutableObject(this.eventEmitter, domainObject);
 | 
			
		||||
 | 
			
		||||
        return mutableObject.set(path, value);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Observe changes to a domain object.
 | 
			
		||||
     * @param {module:openmct.DomainObject} object the object to observe
 | 
			
		||||
     * @param {string} path the property to observe
 | 
			
		||||
     * @param {Function} callback a callback to invoke when new values for
 | 
			
		||||
     *        this property are observed
 | 
			
		||||
     * @method observe
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.observe = function (domainObject, path, callback) {
 | 
			
		||||
        const mutableObject =
 | 
			
		||||
            new MutableObject(this.eventEmitter, domainObject);
 | 
			
		||||
        mutableObject.on(path, callback);
 | 
			
		||||
 | 
			
		||||
        return mutableObject.stopListening.bind(mutableObject);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param {module:openmct.ObjectAPI~Identifier} identifier
 | 
			
		||||
     * @returns {string} A string representation of the given identifier, including namespace and key
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.makeKeyString = function (identifier) {
 | 
			
		||||
        return utils.makeKeyString(identifier);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given any number of identifiers, will return true if they are all equal, otherwise false.
 | 
			
		||||
     * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
 | 
			
		||||
     */
 | 
			
		||||
    ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
 | 
			
		||||
        return identifiers.map(utils.parseKeyString)
 | 
			
		||||
            .every(identifier => {
 | 
			
		||||
                return identifier === identifiers[0]
 | 
			
		||||
                    || (identifier.namespace === identifiers[0].namespace
 | 
			
		||||
                        && identifier.key === identifiers[0].key);
 | 
			
		||||
            });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
 | 
			
		||||
        return this.get(identifier).then((domainObject) => {
 | 
			
		||||
            path.push(domainObject);
 | 
			
		||||
            let location = domainObject.location;
 | 
			
		||||
 | 
			
		||||
            if (location) {
 | 
			
		||||
                return this.getOriginalPath(utils.parseKeyString(location), path);
 | 
			
		||||
            } else {
 | 
			
		||||
                return path;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Uniquely identifies a domain object.
 | 
			
		||||
     *
 | 
			
		||||
     * @typedef Identifier
 | 
			
		||||
     * @memberof module:openmct.ObjectAPI~
 | 
			
		||||
     * @property {string} namespace the namespace to/from which this domain
 | 
			
		||||
     *           object should be loaded/stored.
 | 
			
		||||
     * @property {string} key a unique identifier for the domain object
 | 
			
		||||
     *           within that namespace
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A domain object is an entity of relevance to a user's workflow, that
 | 
			
		||||
     * should appear as a distinct and meaningful object within the user
 | 
			
		||||
     * interface. Examples of domain objects are folders, telemetry sensors,
 | 
			
		||||
     * and so forth.
 | 
			
		||||
     *
 | 
			
		||||
     * A few common properties are defined for domain objects. Beyond these,
 | 
			
		||||
     * individual types of domain objects may add more as they see fit.
 | 
			
		||||
     *
 | 
			
		||||
     * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
 | 
			
		||||
     *           uniquely identifies this domain object
 | 
			
		||||
     * @property {string} type the type of domain object
 | 
			
		||||
     * @property {string} name the human-readable name for this domain object
 | 
			
		||||
     * @property {string} [creator] the user name of the creator of this domain
 | 
			
		||||
     *           object
 | 
			
		||||
     * @property {number} [modified] the time, in milliseconds since the UNIX
 | 
			
		||||
     *           epoch, at which this domain object was last modified
 | 
			
		||||
     * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
 | 
			
		||||
     *           present, this will be used by the default composition provider
 | 
			
		||||
     *           to load domain objects
 | 
			
		||||
     * @typedef DomainObject
 | 
			
		||||
     * @memberof module:openmct
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    function hasAlreadyBeenPersisted(domainObject) {
 | 
			
		||||
        return domainObject.persisted !== undefined
 | 
			
		||||
            && domainObject.persisted === domainObject.modified;
 | 
			
		||||
/**
 | 
			
		||||
 * Will fetch object for the given identifier, returning a version of the object that will automatically keep
 | 
			
		||||
 * itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
 | 
			
		||||
 * The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
 | 
			
		||||
 * platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
 | 
			
		||||
 * committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
 | 
			
		||||
 *
 | 
			
		||||
 * @memberof {module:openmct.ObjectAPI#}
 | 
			
		||||
 * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
 | 
			
		||||
 * the object can be mutated.
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.getMutable = function (identifier) {
 | 
			
		||||
    if (!this.supportsMutation(identifier)) {
 | 
			
		||||
        throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ObjectAPI;
 | 
			
		||||
});
 | 
			
		||||
    return this.get(identifier).then((object) => {
 | 
			
		||||
        return this._toMutable(object);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This function is for cleaning up a mutable domain object when you're done with it.
 | 
			
		||||
 * You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
 | 
			
		||||
 * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
 | 
			
		||||
 * @param {MutableDomainObject} domainObject
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.destroyMutable = function (domainObject) {
 | 
			
		||||
    if (domainObject.isMutable) {
 | 
			
		||||
        return domainObject.$destroy();
 | 
			
		||||
    } else {
 | 
			
		||||
        throw new Error("Attempted to destroy non-mutable domain object");
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ObjectAPI.prototype.delete = function () {
 | 
			
		||||
    throw new Error('Delete not implemented');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
 | 
			
		||||
    let identifier = utils.parseKeyString(idOrKeyString);
 | 
			
		||||
    let provider = this.getProvider(identifier);
 | 
			
		||||
 | 
			
		||||
    return provider !== undefined
 | 
			
		||||
        && provider.create !== undefined
 | 
			
		||||
        && provider.update !== undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Save this domain object in its current state. EXPERIMENTAL
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
 * @param {module:openmct.DomainObject} domainObject the domain object to
 | 
			
		||||
 *        save
 | 
			
		||||
 * @returns {Promise} a promise which will resolve when the domain object
 | 
			
		||||
 *          has been saved, or be rejected if it cannot be saved
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.save = function (domainObject) {
 | 
			
		||||
    let provider = this.getProvider(domainObject.identifier);
 | 
			
		||||
    let savedResolve;
 | 
			
		||||
    let result;
 | 
			
		||||
 | 
			
		||||
    if (!this.isPersistable(domainObject.identifier)) {
 | 
			
		||||
        result = Promise.reject('Object provider does not support saving');
 | 
			
		||||
    } else if (hasAlreadyBeenPersisted(domainObject)) {
 | 
			
		||||
        result = Promise.resolve(true);
 | 
			
		||||
    } else {
 | 
			
		||||
        const persistedTime = Date.now();
 | 
			
		||||
        if (domainObject.persisted === undefined) {
 | 
			
		||||
            result = new Promise((resolve) => {
 | 
			
		||||
                savedResolve = resolve;
 | 
			
		||||
            });
 | 
			
		||||
            domainObject.persisted = persistedTime;
 | 
			
		||||
            provider.create(domainObject).then((response) => {
 | 
			
		||||
                this.mutate(domainObject, 'persisted', persistedTime);
 | 
			
		||||
                savedResolve(response);
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            domainObject.persisted = persistedTime;
 | 
			
		||||
            this.mutate(domainObject, 'persisted', persistedTime);
 | 
			
		||||
            result = provider.update(domainObject);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Add a root-level object.
 | 
			
		||||
 * @param {module:openmct.ObjectAPI~Identifier|function} an array of
 | 
			
		||||
 *        identifiers for root level objects, or a function that returns a
 | 
			
		||||
 *        promise for an identifier or an array of root level objects.
 | 
			
		||||
 * @method addRoot
 | 
			
		||||
 * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.addRoot = function (key) {
 | 
			
		||||
    this.rootRegistry.addRoot(key);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
 | 
			
		||||
 * The domain object will be transformed after it is retrieved from the persistence store
 | 
			
		||||
 * The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
 | 
			
		||||
 *
 | 
			
		||||
 * @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
 | 
			
		||||
 * @method addGetInterceptor
 | 
			
		||||
 * @memberof module:openmct.InterceptorRegistry#
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
 | 
			
		||||
    this.interceptorRegistry.addInterceptor(interceptorDef);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Retrieve the interceptors for a given domain object.
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
 | 
			
		||||
    return this.interceptorRegistry.getInterceptors(identifier, object);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Modify a domain object.
 | 
			
		||||
 * @param {module:openmct.DomainObject} object the object to mutate
 | 
			
		||||
 * @param {string} path the property to modify
 | 
			
		||||
 * @param {*} value the new value for this property
 | 
			
		||||
 * @method mutate
 | 
			
		||||
 * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
 | 
			
		||||
    if (!this.supportsMutation(domainObject.identifier)) {
 | 
			
		||||
        throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (domainObject.isMutable) {
 | 
			
		||||
        domainObject.$set(path, value);
 | 
			
		||||
    } else {
 | 
			
		||||
        //Creating a temporary mutable domain object allows other mutable instances of the
 | 
			
		||||
        //object to be kept in sync.
 | 
			
		||||
        let mutableDomainObject = this._toMutable(domainObject);
 | 
			
		||||
 | 
			
		||||
        //Mutate original object
 | 
			
		||||
        MutableDomainObject.mutateObject(domainObject, path, value);
 | 
			
		||||
 | 
			
		||||
        //Mutate temporary mutable object, in the process informing any other mutable instances
 | 
			
		||||
        mutableDomainObject.$set(path, value);
 | 
			
		||||
 | 
			
		||||
        //Destroy temporary mutable object
 | 
			
		||||
        this.destroyMutable(mutableDomainObject);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype._toMutable = function (object) {
 | 
			
		||||
    if (object.isMutable) {
 | 
			
		||||
        return object;
 | 
			
		||||
    } else {
 | 
			
		||||
        return MutableDomainObject.createMutable(object, this.eventEmitter);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
 | 
			
		||||
 * @returns {boolean} true if the object can be mutated, otherwise returns false
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.supportsMutation = function (identifier) {
 | 
			
		||||
    return this.isPersistable(identifier);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Observe changes to a domain object.
 | 
			
		||||
 * @param {module:openmct.DomainObject} object the object to observe
 | 
			
		||||
 * @param {string} path the property to observe
 | 
			
		||||
 * @param {Function} callback a callback to invoke when new values for
 | 
			
		||||
 *        this property are observed
 | 
			
		||||
 * @method observe
 | 
			
		||||
 * @memberof module:openmct.ObjectAPI#
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
 | 
			
		||||
    if (domainObject.isMutable) {
 | 
			
		||||
        return domainObject.$observe(path, callback);
 | 
			
		||||
    } else {
 | 
			
		||||
        let mutable = this._toMutable(domainObject);
 | 
			
		||||
        mutable.$observe(path, callback);
 | 
			
		||||
 | 
			
		||||
        return () => mutable.$destroy();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {module:openmct.ObjectAPI~Identifier} identifier
 | 
			
		||||
 * @returns {string} A string representation of the given identifier, including namespace and key
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.makeKeyString = function (identifier) {
 | 
			
		||||
    return utils.makeKeyString(identifier);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
 | 
			
		||||
 * @returns {module:openmct.ObjectAPI~Identifier} An identifier object
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.parseKeyString = function (keyString) {
 | 
			
		||||
    return utils.parseKeyString(keyString);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Given any number of identifiers, will return true if they are all equal, otherwise false.
 | 
			
		||||
 * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
 | 
			
		||||
 */
 | 
			
		||||
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
 | 
			
		||||
    return identifiers.map(utils.parseKeyString)
 | 
			
		||||
        .every(identifier => {
 | 
			
		||||
            return identifier === identifiers[0]
 | 
			
		||||
                || (identifier.namespace === identifiers[0].namespace
 | 
			
		||||
                    && identifier.key === identifiers[0].key);
 | 
			
		||||
        });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
 | 
			
		||||
    return this.get(identifier).then((domainObject) => {
 | 
			
		||||
        path.push(domainObject);
 | 
			
		||||
        let location = domainObject.location;
 | 
			
		||||
 | 
			
		||||
        if (location) {
 | 
			
		||||
            return this.getOriginalPath(utils.parseKeyString(location), path);
 | 
			
		||||
        } else {
 | 
			
		||||
            return path;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Uniquely identifies a domain object.
 | 
			
		||||
 *
 | 
			
		||||
 * @typedef Identifier
 | 
			
		||||
 * @memberof module:openmct.ObjectAPI~
 | 
			
		||||
 * @property {string} namespace the namespace to/from which this domain
 | 
			
		||||
 *           object should be loaded/stored.
 | 
			
		||||
 * @property {string} key a unique identifier for the domain object
 | 
			
		||||
 *           within that namespace
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A domain object is an entity of relevance to a user's workflow, that
 | 
			
		||||
 * should appear as a distinct and meaningful object within the user
 | 
			
		||||
 * interface. Examples of domain objects are folders, telemetry sensors,
 | 
			
		||||
 * and so forth.
 | 
			
		||||
 *
 | 
			
		||||
 * A few common properties are defined for domain objects. Beyond these,
 | 
			
		||||
 * individual types of domain objects may add more as they see fit.
 | 
			
		||||
 *
 | 
			
		||||
 * @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
 | 
			
		||||
 *           uniquely identifies this domain object
 | 
			
		||||
 * @property {string} type the type of domain object
 | 
			
		||||
 * @property {string} name the human-readable name for this domain object
 | 
			
		||||
 * @property {string} [creator] the user name of the creator of this domain
 | 
			
		||||
 *           object
 | 
			
		||||
 * @property {number} [modified] the time, in milliseconds since the UNIX
 | 
			
		||||
 *           epoch, at which this domain object was last modified
 | 
			
		||||
 * @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
 | 
			
		||||
 *           present, this will be used by the default composition provider
 | 
			
		||||
 *           to load domain objects
 | 
			
		||||
 * @typedef DomainObject
 | 
			
		||||
 * @memberof module:openmct
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function hasAlreadyBeenPersisted(domainObject) {
 | 
			
		||||
    return domainObject.persisted !== undefined
 | 
			
		||||
        && domainObject.persisted === domainObject.modified;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ObjectAPI;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,16 @@ import ObjectAPI from './ObjectAPI.js';
 | 
			
		||||
 | 
			
		||||
describe("The Object API", () => {
 | 
			
		||||
    let objectAPI;
 | 
			
		||||
    let typeRegistry;
 | 
			
		||||
    let mockDomainObject;
 | 
			
		||||
    const TEST_NAMESPACE = "test-namespace";
 | 
			
		||||
    const FIFTEEN_MINUTES = 15 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        objectAPI = new ObjectAPI();
 | 
			
		||||
        typeRegistry = jasmine.createSpyObj('typeRegistry', [
 | 
			
		||||
            'get'
 | 
			
		||||
        ]);
 | 
			
		||||
        objectAPI = new ObjectAPI(typeRegistry);
 | 
			
		||||
        mockDomainObject = {
 | 
			
		||||
            identifier: {
 | 
			
		||||
                namespace: TEST_NAMESPACE,
 | 
			
		||||
@@ -33,6 +37,7 @@ describe("The Object API", () => {
 | 
			
		||||
                    "update"
 | 
			
		||||
                ]);
 | 
			
		||||
                mockProvider.create.and.returnValue(Promise.resolve(true));
 | 
			
		||||
                mockProvider.update.and.returnValue(Promise.resolve(true));
 | 
			
		||||
                objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
 | 
			
		||||
            });
 | 
			
		||||
            it("Calls 'create' on provider if object is new", () => {
 | 
			
		||||
@@ -63,12 +68,51 @@ describe("The Object API", () => {
 | 
			
		||||
    describe("The get function", () => {
 | 
			
		||||
        describe("when a provider is available", () => {
 | 
			
		||||
            let mockProvider;
 | 
			
		||||
            let mockInterceptor;
 | 
			
		||||
            let anotherMockInterceptor;
 | 
			
		||||
            let notApplicableMockInterceptor;
 | 
			
		||||
            beforeEach(() => {
 | 
			
		||||
                mockProvider = jasmine.createSpyObj("mock provider", [
 | 
			
		||||
                    "get"
 | 
			
		||||
                ]);
 | 
			
		||||
                mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
 | 
			
		||||
 | 
			
		||||
                mockInterceptor = jasmine.createSpyObj("mock interceptor", [
 | 
			
		||||
                    "appliesTo",
 | 
			
		||||
                    "invoke"
 | 
			
		||||
                ]);
 | 
			
		||||
                mockInterceptor.appliesTo.and.returnValue(true);
 | 
			
		||||
                mockInterceptor.invoke.and.callFake((identifier, object) => {
 | 
			
		||||
                    return Object.assign({
 | 
			
		||||
                        changed: true
 | 
			
		||||
                    }, object);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [
 | 
			
		||||
                    "appliesTo",
 | 
			
		||||
                    "invoke"
 | 
			
		||||
                ]);
 | 
			
		||||
                anotherMockInterceptor.appliesTo.and.returnValue(true);
 | 
			
		||||
                anotherMockInterceptor.invoke.and.callFake((identifier, object) => {
 | 
			
		||||
                    return Object.assign({
 | 
			
		||||
                        alsoChanged: true
 | 
			
		||||
                    }, object);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [
 | 
			
		||||
                    "appliesTo",
 | 
			
		||||
                    "invoke"
 | 
			
		||||
                ]);
 | 
			
		||||
                notApplicableMockInterceptor.appliesTo.and.returnValue(false);
 | 
			
		||||
                notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {
 | 
			
		||||
                    return Object.assign({
 | 
			
		||||
                        shouldNotBeChanged: true
 | 
			
		||||
                    }, object);
 | 
			
		||||
                });
 | 
			
		||||
                objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
 | 
			
		||||
                objectAPI.addGetInterceptor(mockInterceptor);
 | 
			
		||||
                objectAPI.addGetInterceptor(anotherMockInterceptor);
 | 
			
		||||
                objectAPI.addGetInterceptor(notApplicableMockInterceptor);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("Caches multiple requests for the same object", () => {
 | 
			
		||||
@@ -78,6 +122,142 @@ describe("The Object API", () => {
 | 
			
		||||
                objectAPI.get(mockDomainObject.identifier);
 | 
			
		||||
                expect(mockProvider.get.calls.count()).toBe(1);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("applies any applicable interceptors", () => {
 | 
			
		||||
                expect(mockDomainObject.changed).toBeUndefined();
 | 
			
		||||
                objectAPI.get(mockDomainObject.identifier).then((object) => {
 | 
			
		||||
                    expect(object.changed).toBeTrue();
 | 
			
		||||
                    expect(object.alsoChanged).toBeTrue();
 | 
			
		||||
                    expect(object.shouldNotBeChanged).toBeUndefined();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("the mutation API", () => {
 | 
			
		||||
        let testObject;
 | 
			
		||||
        let mutable;
 | 
			
		||||
        let mockProvider;
 | 
			
		||||
 | 
			
		||||
        beforeEach(function () {
 | 
			
		||||
            objectAPI = new ObjectAPI(typeRegistry);
 | 
			
		||||
            testObject = {
 | 
			
		||||
                identifier: {
 | 
			
		||||
                    namespace: TEST_NAMESPACE,
 | 
			
		||||
                    key: 'test-key'
 | 
			
		||||
                },
 | 
			
		||||
                name: 'test object',
 | 
			
		||||
                otherAttribute: 'other-attribute-value',
 | 
			
		||||
                objectAttribute: {
 | 
			
		||||
                    embeddedObject: {
 | 
			
		||||
                        embeddedKey: 'embedded-value'
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            mockProvider = jasmine.createSpyObj("mock provider", [
 | 
			
		||||
                "get",
 | 
			
		||||
                "create",
 | 
			
		||||
                "update"
 | 
			
		||||
            ]);
 | 
			
		||||
            mockProvider.get.and.returnValue(Promise.resolve(testObject));
 | 
			
		||||
            objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
 | 
			
		||||
 | 
			
		||||
            return objectAPI.getMutable(testObject.identifier)
 | 
			
		||||
                .then(object => {
 | 
			
		||||
                    mutable = object;
 | 
			
		||||
 | 
			
		||||
                    return mutable;
 | 
			
		||||
                });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(() => {
 | 
			
		||||
            mutable.$destroy();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('mutates the original object', () => {
 | 
			
		||||
            const MUTATED_NAME = 'mutated name';
 | 
			
		||||
            objectAPI.mutate(testObject, 'name', MUTATED_NAME);
 | 
			
		||||
            expect(testObject.name).toBe(MUTATED_NAME);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe ('uses a MutableDomainObject', () => {
 | 
			
		||||
            it('and retains properties of original object ', function () {
 | 
			
		||||
                expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
 | 
			
		||||
                expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);
 | 
			
		||||
                expect(mutable.identifier).toEqual(testObject.identifier);
 | 
			
		||||
                expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('that is identical to original object when serialized', function () {
 | 
			
		||||
                expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('uses events', function () {
 | 
			
		||||
            let testObjectDuplicate;
 | 
			
		||||
            let mutableSecondInstance;
 | 
			
		||||
 | 
			
		||||
            beforeEach(function () {
 | 
			
		||||
                // Duplicate object to guarantee we are not sharing object instance, which would invalidate test
 | 
			
		||||
                testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
 | 
			
		||||
                mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            afterEach(() => {
 | 
			
		||||
                mutableSecondInstance.$destroy();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('to stay synchronized when mutated', function () {
 | 
			
		||||
                objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
 | 
			
		||||
                expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('to indicate when a property changes', function () {
 | 
			
		||||
                let mutationCallback = jasmine.createSpy('mutation-callback');
 | 
			
		||||
                let unlisten;
 | 
			
		||||
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                    mutationCallback.and.callFake(resolve);
 | 
			
		||||
                    unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
 | 
			
		||||
                    objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
 | 
			
		||||
                }).then(function () {
 | 
			
		||||
                    expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
 | 
			
		||||
                    unlisten();
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('to indicate when a child property has changed', function () {
 | 
			
		||||
                let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
 | 
			
		||||
                let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
 | 
			
		||||
                let objectAttributeCallback = jasmine.createSpy('objectAttribute');
 | 
			
		||||
                let listeners = [];
 | 
			
		||||
 | 
			
		||||
                return new Promise(function (resolve) {
 | 
			
		||||
                    objectAttributeCallback.and.callFake(resolve);
 | 
			
		||||
 | 
			
		||||
                    listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
 | 
			
		||||
                    listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
 | 
			
		||||
                    listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
 | 
			
		||||
 | 
			
		||||
                    objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
 | 
			
		||||
                }).then(function () {
 | 
			
		||||
                    expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
 | 
			
		||||
                    expect(embeddedObjectCallback).toHaveBeenCalledWith({
 | 
			
		||||
                        embeddedKey: 'updated-embedded-value'
 | 
			
		||||
                    });
 | 
			
		||||
                    expect(objectAttributeCallback).toHaveBeenCalledWith({
 | 
			
		||||
                        embeddedObject: {
 | 
			
		||||
                            embeddedKey: 'updated-embedded-value'
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    listeners.forEach(listener => listener());
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function hasOwnProperty(object, property) {
 | 
			
		||||
    return Object.prototype.hasOwnProperty.call(object, property);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -43,12 +43,16 @@ export default function LADTableViewProvider(openmct) {
 | 
			
		||||
                        components: {
 | 
			
		||||
                            LadTableComponent: LadTable
 | 
			
		||||
                        },
 | 
			
		||||
                        provide: {
 | 
			
		||||
                            openmct,
 | 
			
		||||
                            domainObject,
 | 
			
		||||
                            objectPath
 | 
			
		||||
                        data: () => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                domainObject,
 | 
			
		||||
                                objectPath
 | 
			
		||||
                            };
 | 
			
		||||
                        },
 | 
			
		||||
                        template: '<lad-table-component></lad-table-component>'
 | 
			
		||||
                        provide: {
 | 
			
		||||
                            openmct
 | 
			
		||||
                        },
 | 
			
		||||
                        template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
 | 
			
		||||
                    });
 | 
			
		||||
                },
 | 
			
		||||
                destroy: function (element) {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
    class="js-lad-table__body__row"
 | 
			
		||||
    @contextmenu.prevent="showContextMenu"
 | 
			
		||||
>
 | 
			
		||||
    <td class="js-first-data">{{ name }}</td>
 | 
			
		||||
    <td class="js-first-data">{{ domainObject.name }}</td>
 | 
			
		||||
    <td class="js-second-data">{{ formattedTimestamp }}</td>
 | 
			
		||||
    <td
 | 
			
		||||
        class="js-third-data"
 | 
			
		||||
@@ -44,17 +44,22 @@
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
const CONTEXT_MENU_ACTIONS = [
 | 
			
		||||
    'viewDatumAction',
 | 
			
		||||
    'viewHistoricalData',
 | 
			
		||||
    'remove'
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'objectPath'],
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    props: {
 | 
			
		||||
        domainObject: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        objectPath: {
 | 
			
		||||
            type: Array,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        hasUnits: {
 | 
			
		||||
            type: Boolean,
 | 
			
		||||
            requred: true
 | 
			
		||||
@@ -65,7 +70,6 @@ export default {
 | 
			
		||||
        currentObjectPath.unshift(this.domainObject);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            name: this.domainObject.name,
 | 
			
		||||
            timestamp: undefined,
 | 
			
		||||
            value: '---',
 | 
			
		||||
            valueClass: '',
 | 
			
		||||
@@ -88,14 +92,6 @@ export default {
 | 
			
		||||
            .telemetry
 | 
			
		||||
            .limitEvaluator(this.domainObject);
 | 
			
		||||
 | 
			
		||||
        this.stopWatchingMutation = this.openmct
 | 
			
		||||
            .objects
 | 
			
		||||
            .observe(
 | 
			
		||||
                this.domainObject,
 | 
			
		||||
                '*',
 | 
			
		||||
                this.updateName
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
        this.openmct.time.on('timeSystem', this.updateTimeSystem);
 | 
			
		||||
        this.openmct.time.on('bounds', this.updateBounds);
 | 
			
		||||
 | 
			
		||||
@@ -118,7 +114,6 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
        this.stopWatchingMutation();
 | 
			
		||||
        this.unsubscribe();
 | 
			
		||||
        this.openmct.time.off('timeSystem', this.updateTimeSystem);
 | 
			
		||||
        this.openmct.time.off('bounds', this.updateBounds);
 | 
			
		||||
@@ -129,6 +124,7 @@ export default {
 | 
			
		||||
            let limit;
 | 
			
		||||
 | 
			
		||||
            if (this.shouldUpdate(newTimestamp)) {
 | 
			
		||||
                this.datum = datum;
 | 
			
		||||
                this.timestamp = newTimestamp;
 | 
			
		||||
                this.value = this.formats[this.valueKey].format(datum);
 | 
			
		||||
                limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
 | 
			
		||||
@@ -158,9 +154,6 @@ export default {
 | 
			
		||||
                })
 | 
			
		||||
                .then((array) => this.updateValues(array[array.length - 1]));
 | 
			
		||||
        },
 | 
			
		||||
        updateName(name) {
 | 
			
		||||
            this.name = name;
 | 
			
		||||
        },
 | 
			
		||||
        updateBounds(bounds, isTick) {
 | 
			
		||||
            this.bounds = bounds;
 | 
			
		||||
            if (!isTick) {
 | 
			
		||||
@@ -175,8 +168,25 @@ export default {
 | 
			
		||||
            this.resetValues();
 | 
			
		||||
            this.timestampKey = timeSystem.key;
 | 
			
		||||
        },
 | 
			
		||||
        getView() {
 | 
			
		||||
            return {
 | 
			
		||||
                getViewContext: () => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        viewHistoricalData: true,
 | 
			
		||||
                        viewDatumAction: true,
 | 
			
		||||
                        getDatum: () => {
 | 
			
		||||
                            return this.datum;
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        showContextMenu(event) {
 | 
			
		||||
            this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
 | 
			
		||||
            let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
 | 
			
		||||
            let allActions = actionCollection.getActionsObject();
 | 
			
		||||
            let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
 | 
			
		||||
 | 
			
		||||
            this.openmct.menus.showMenu(event.x, event.y, applicableActions);
 | 
			
		||||
        },
 | 
			
		||||
        resetValues() {
 | 
			
		||||
            this.value = '---';
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -36,6 +36,7 @@
 | 
			
		||||
                v-for="item in items"
 | 
			
		||||
                :key="item.key"
 | 
			
		||||
                :domain-object="item.domainObject"
 | 
			
		||||
                :object-path="objectPath"
 | 
			
		||||
                :has-units="hasUnits"
 | 
			
		||||
            />
 | 
			
		||||
        </tbody>
 | 
			
		||||
@@ -47,10 +48,20 @@
 | 
			
		||||
import LadRow from './LADRow.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    inject: ['openmct', 'domainObject', 'objectPath'],
 | 
			
		||||
    inject: ['openmct'],
 | 
			
		||||
    components: {
 | 
			
		||||
        LadRow
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        domainObject: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        },
 | 
			
		||||
        objectPath: {
 | 
			
		||||
            type: Array,
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            items: []
 | 
			
		||||
 
 | 
			
		||||
@@ -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 () {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,8 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isTelemetryUsed(datum.id)) {
 | 
			
		||||
        // if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
 | 
			
		||||
        if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
 | 
			
		||||
 | 
			
		||||
            this.criteria.forEach(criterion => {
 | 
			
		||||
                if (this.isAnyOrAllTelemetry(criterion)) {
 | 
			
		||||
@@ -93,6 +94,12 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
        return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasNoTelemetry() {
 | 
			
		||||
        return this.criteria.every((criterion) => {
 | 
			
		||||
            return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isTelemetryUsed(id) {
 | 
			
		||||
        return this.criteria.some(criterion => {
 | 
			
		||||
            return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
 | 
			
		||||
@@ -250,10 +257,17 @@ export default class Condition extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTriggerDescription() {
 | 
			
		||||
        return {
 | 
			
		||||
            conjunction: TRIGGER_CONJUNCTION[this.trigger],
 | 
			
		||||
            prefix: `${TRIGGER_LABEL[this.trigger]}: `
 | 
			
		||||
        };
 | 
			
		||||
        if (this.trigger) {
 | 
			
		||||
            return {
 | 
			
		||||
                conjunction: TRIGGER_CONJUNCTION[this.trigger],
 | 
			
		||||
                prefix: `${TRIGGER_LABEL[this.trigger]}: `
 | 
			
		||||
            };
 | 
			
		||||
        } else {
 | 
			
		||||
            return {
 | 
			
		||||
                conjunction: '',
 | 
			
		||||
                prefix: ''
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    requestLADConditionResult() {
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,17 @@ export default class ConditionManager extends EventEmitter {
 | 
			
		||||
        delete this.subscriptions[id];
 | 
			
		||||
        delete this.telemetryObjects[id];
 | 
			
		||||
        this.removeConditionTelemetryObjects();
 | 
			
		||||
 | 
			
		||||
        //force re-computation of condition set result as we might be in a state where
 | 
			
		||||
        // there is no telemetry datum coming in for a while or at all.
 | 
			
		||||
        let latestTimestamp = getLatestTimestamp(
 | 
			
		||||
            {},
 | 
			
		||||
            {},
 | 
			
		||||
            this.timeSystems,
 | 
			
		||||
            this.openmct.time.timeSystem()
 | 
			
		||||
        );
 | 
			
		||||
        this.updateConditionResults({id: id});
 | 
			
		||||
        this.updateCurrentCondition(latestTimestamp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initialize() {
 | 
			
		||||
@@ -336,14 +347,17 @@ export default class ConditionManager extends EventEmitter {
 | 
			
		||||
        let timestamp = {};
 | 
			
		||||
        timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
 | 
			
		||||
 | 
			
		||||
        this.updateConditionResults(normalizedDatum);
 | 
			
		||||
        this.updateCurrentCondition(timestamp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateConditionResults(normalizedDatum) {
 | 
			
		||||
        //We want to stop when the first condition evaluates to true.
 | 
			
		||||
        this.conditions.some((condition) => {
 | 
			
		||||
            condition.updateResult(normalizedDatum);
 | 
			
		||||
 | 
			
		||||
            return condition.result === true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.updateCurrentCondition(timestamp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateCurrentCondition(timestamp) {
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,7 @@ export default class StyleRuleManager extends EventEmitter {
 | 
			
		||||
    updateObjectStyleConfig(styleConfiguration) {
 | 
			
		||||
        if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
 | 
			
		||||
            this.initialize(styleConfiguration || {});
 | 
			
		||||
            this.applyStaticStyle();
 | 
			
		||||
            this.destroy();
 | 
			
		||||
        } else {
 | 
			
		||||
            let isNewConditionSet = !this.conditionSetIdentifier
 | 
			
		||||
@@ -158,7 +159,6 @@ export default class StyleRuleManager extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        this.applyStaticStyle();
 | 
			
		||||
        if (this.stopProvidingTelemetry) {
 | 
			
		||||
            this.stopProvidingTelemetry();
 | 
			
		||||
            delete this.stopProvidingTelemetry;
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,7 @@
 | 
			
		||||
.c-cs {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,6 @@
 | 
			
		||||
            v-model="expanded"
 | 
			
		||||
            class="c-tree__item__view-control"
 | 
			
		||||
            :enabled="hasChildren"
 | 
			
		||||
            :propagate="false"
 | 
			
		||||
        />
 | 
			
		||||
        <div class="c-tree__item__label c-object-label">
 | 
			
		||||
            <div
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@
 | 
			
		||||
 | 
			
		||||
import { createOpenMct, resetApplicationState } from "utils/testing";
 | 
			
		||||
import ConditionPlugin from "./plugin";
 | 
			
		||||
import stylesManager from '@/ui/inspector/styles/StylesManager';
 | 
			
		||||
import StylesView from "./components/inspector/StylesView.vue";
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
 | 
			
		||||
@@ -146,6 +147,8 @@ describe('the plugin', function () {
 | 
			
		||||
        let displayLayoutItem;
 | 
			
		||||
        let lineLayoutItem;
 | 
			
		||||
        let boxLayoutItem;
 | 
			
		||||
        let notCreatableObjectItem;
 | 
			
		||||
        let notCreatableObject;
 | 
			
		||||
        let selection;
 | 
			
		||||
        let component;
 | 
			
		||||
        let styleViewComponentObject;
 | 
			
		||||
@@ -264,6 +267,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 +313,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 +378,19 @@ describe('the plugin', function () {
 | 
			
		||||
                        "index": 0
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    context: {
 | 
			
		||||
                        item: displayLayoutItem,
 | 
			
		||||
                        "supportsMultiSelect": true
 | 
			
		||||
                    }
 | 
			
		||||
                }],
 | 
			
		||||
                [{
 | 
			
		||||
                    context: {
 | 
			
		||||
                        "item": notCreatableObject,
 | 
			
		||||
                        "layoutItem": notCreatableObjectItem,
 | 
			
		||||
                        "index": 2
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    context: {
 | 
			
		||||
                        item: displayLayoutItem,
 | 
			
		||||
@@ -328,7 +403,8 @@ describe('the plugin', function () {
 | 
			
		||||
            component = new Vue({
 | 
			
		||||
                provide: {
 | 
			
		||||
                    openmct: openmct,
 | 
			
		||||
                    selection: selection
 | 
			
		||||
                    selection: selection,
 | 
			
		||||
                    stylesManager
 | 
			
		||||
                },
 | 
			
		||||
                el: viewContainer,
 | 
			
		||||
                components: {
 | 
			
		||||
@@ -344,7 +420,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 +439,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 +461,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,
 | 
			
		||||
@@ -151,26 +152,41 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        let domainObject = JSON.parse(JSON.stringify(this.domainObject));
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            internalDomainObject: domainObject,
 | 
			
		||||
            initSelectIndex: undefined,
 | 
			
		||||
            selection: []
 | 
			
		||||
            selection: [],
 | 
			
		||||
            showGrid: true
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        gridSize() {
 | 
			
		||||
            return this.internalDomainObject.configuration.layoutGrid;
 | 
			
		||||
            return this.domainObject.configuration.layoutGrid;
 | 
			
		||||
        },
 | 
			
		||||
        layoutItems() {
 | 
			
		||||
            return this.internalDomainObject.configuration.items;
 | 
			
		||||
            return this.domainObject.configuration.items;
 | 
			
		||||
        },
 | 
			
		||||
        selectedLayoutItems() {
 | 
			
		||||
            return this.layoutItems.filter(item => {
 | 
			
		||||
                return this.itemIsInCurrentSelection(item);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        layoutDimensions() {
 | 
			
		||||
            return this.domainObject.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,14 +195,17 @@ 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));
 | 
			
		||||
        }.bind(this));
 | 
			
		||||
        this.openmct.selection.on('change', this.setSelection);
 | 
			
		||||
        this.initializeItems();
 | 
			
		||||
        this.composition = this.openmct.composition.get(this.internalDomainObject);
 | 
			
		||||
        this.composition = this.openmct.composition.get(this.domainObject);
 | 
			
		||||
        this.composition.on('add', this.addChild);
 | 
			
		||||
        this.composition.on('remove', this.removeChild);
 | 
			
		||||
        this.composition.load();
 | 
			
		||||
@@ -195,7 +214,6 @@ export default {
 | 
			
		||||
        this.openmct.selection.off('change', this.setSelection);
 | 
			
		||||
        this.composition.off('add', this.addChild);
 | 
			
		||||
        this.composition.off('remove', this.removeChild);
 | 
			
		||||
        this.unlisten();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        addElement(itemType, element) {
 | 
			
		||||
@@ -322,7 +340,7 @@ export default {
 | 
			
		||||
            this.startingMinY2 = undefined;
 | 
			
		||||
        },
 | 
			
		||||
        mutate(path, value) {
 | 
			
		||||
            this.openmct.objects.mutate(this.internalDomainObject, path, value);
 | 
			
		||||
            this.openmct.objects.mutate(this.domainObject, path, value);
 | 
			
		||||
        },
 | 
			
		||||
        handleDrop($event) {
 | 
			
		||||
            if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {
 | 
			
		||||
@@ -362,11 +380,11 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        containsObject(identifier) {
 | 
			
		||||
            return _.get(this.internalDomainObject, 'composition')
 | 
			
		||||
            return _.get(this.domainObject, 'composition')
 | 
			
		||||
                .some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
 | 
			
		||||
        },
 | 
			
		||||
        handleDragOver($event) {
 | 
			
		||||
            if (this.internalDomainObject.locked) {
 | 
			
		||||
            if (this.domainObject.locked) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -395,7 +413,7 @@ export default {
 | 
			
		||||
            item.id = uuid();
 | 
			
		||||
            this.trackItem(item);
 | 
			
		||||
            this.layoutItems.push(item);
 | 
			
		||||
            this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
 | 
			
		||||
            this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
 | 
			
		||||
            this.initSelectIndex = this.layoutItems.length - 1;
 | 
			
		||||
        },
 | 
			
		||||
        trackItem(item) {
 | 
			
		||||
@@ -452,7 +470,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        removeFromComposition(keyString) {
 | 
			
		||||
            let composition = _.get(this.internalDomainObject, 'composition');
 | 
			
		||||
            let composition = _.get(this.domainObject, 'composition');
 | 
			
		||||
            composition = composition.filter(identifier => {
 | 
			
		||||
                return this.openmct.objects.makeKeyString(identifier) !== keyString;
 | 
			
		||||
            });
 | 
			
		||||
@@ -604,10 +622,10 @@ export default {
 | 
			
		||||
        createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
 | 
			
		||||
            let identifier = {
 | 
			
		||||
                key: uuid(),
 | 
			
		||||
                namespace: this.internalDomainObject.identifier.namespace
 | 
			
		||||
                namespace: this.domainObject.identifier.namespace
 | 
			
		||||
            };
 | 
			
		||||
            let type = this.openmct.types.get(viewType);
 | 
			
		||||
            let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
 | 
			
		||||
            let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
            let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;
 | 
			
		||||
            let object = {};
 | 
			
		||||
 | 
			
		||||
@@ -664,7 +682,7 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        duplicateItem(selectedItems) {
 | 
			
		||||
            let objectStyles = this.internalDomainObject.configuration.objectStyles || {};
 | 
			
		||||
            let objectStyles = this.domainObject.configuration.objectStyles || {};
 | 
			
		||||
            let selectItemsArray = [];
 | 
			
		||||
            let newDomainObjectsArray = [];
 | 
			
		||||
 | 
			
		||||
@@ -703,8 +721,8 @@ export default {
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.$nextTick(() => {
 | 
			
		||||
                this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
 | 
			
		||||
                this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles);
 | 
			
		||||
                this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
 | 
			
		||||
                this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
 | 
			
		||||
                this.$el.click(); //clear selection;
 | 
			
		||||
 | 
			
		||||
                newDomainObjectsArray.forEach(domainObject => {
 | 
			
		||||
@@ -743,13 +761,13 @@ export default {
 | 
			
		||||
            };
 | 
			
		||||
            this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => {
 | 
			
		||||
                let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier);
 | 
			
		||||
                let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
 | 
			
		||||
                let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
                this.composition.add(newDomainObject);
 | 
			
		||||
                this.addItem('subobject-view', newDomainObject, position);
 | 
			
		||||
 | 
			
		||||
                overlayPlots.forEach(overlayPlot => {
 | 
			
		||||
                    if (overlayPlot.location === internalDomainObjectKeyString) {
 | 
			
		||||
                    if (overlayPlot.location === domainObjectKeyString) {
 | 
			
		||||
                        this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
@@ -798,6 +816,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
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
@@ -125,27 +129,40 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.openmct.objects.get(this.item.identifier)
 | 
			
		||||
            .then(this.setObject);
 | 
			
		||||
        if (this.openmct.objects.supportsMutation(this.item.identifier)) {
 | 
			
		||||
            this.openmct.objects.getMutable(this.item.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.openmct.objects.get(this.item.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        if (this.removeSelectable) {
 | 
			
		||||
            this.removeSelectable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.domainObject.isMutable) {
 | 
			
		||||
            this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        setObject(domainObject) {
 | 
			
		||||
            this.domainObject = domainObject;
 | 
			
		||||
            this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
 | 
			
		||||
            this.$nextTick(() => {
 | 
			
		||||
                let childContext = this.$refs.objectFrame.getSelectionContext();
 | 
			
		||||
                childContext.item = domainObject;
 | 
			
		||||
                childContext.layoutItem = this.item;
 | 
			
		||||
                childContext.index = this.index;
 | 
			
		||||
                this.context = childContext;
 | 
			
		||||
                this.removeSelectable = this.openmct.selection.selectable(
 | 
			
		||||
                    this.$el, this.context, this.immediatelySelect || this.initSelect);
 | 
			
		||||
                delete this.immediatelySelect;
 | 
			
		||||
                let reference = this.$refs.objectFrame;
 | 
			
		||||
 | 
			
		||||
                if (reference) {
 | 
			
		||||
                    let childContext = this.$refs.objectFrame.getSelectionContext();
 | 
			
		||||
                    childContext.item = domainObject;
 | 
			
		||||
                    childContext.layoutItem = this.item;
 | 
			
		||||
                    childContext.index = this.index;
 | 
			
		||||
                    this.context = childContext;
 | 
			
		||||
                    this.removeSelectable = this.openmct.selection.selectable(
 | 
			
		||||
                        this.$el, this.context, this.immediatelySelect || this.initSelect);
 | 
			
		||||
                    delete this.immediatelySelect;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,16 +30,15 @@
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="domainObject"
 | 
			
		||||
        class="c-telemetry-view"
 | 
			
		||||
        :class="{
 | 
			
		||||
            styleClass,
 | 
			
		||||
            'is-missing': domainObject.status === 'missing'
 | 
			
		||||
        }"
 | 
			
		||||
        class="c-telemetry-view u-style-receiver"
 | 
			
		||||
        :class="[statusClass]"
 | 
			
		||||
        :style="styleObject"
 | 
			
		||||
        :data-font-size="item.fontSize"
 | 
			
		||||
        :data-font="item.font"
 | 
			
		||||
        @contextmenu.prevent="showContextMenu"
 | 
			
		||||
    >
 | 
			
		||||
        <div class="is-missing__indicator"
 | 
			
		||||
             title="This item is missing"
 | 
			
		||||
        <div class="is-status__indicator"
 | 
			
		||||
             :title="`This item is ${status}`"
 | 
			
		||||
        ></div>
 | 
			
		||||
        <div
 | 
			
		||||
            v-if="showLabel"
 | 
			
		||||
@@ -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) {
 | 
			
		||||
@@ -202,20 +212,41 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.openmct.objects.get(this.item.identifier)
 | 
			
		||||
            .then(this.setObject);
 | 
			
		||||
        if (this.openmct.objects.supportsMutation(this.item.identifier)) {
 | 
			
		||||
            this.openmct.objects.getMutable(this.item.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.openmct.objects.get(this.item.identifier)
 | 
			
		||||
                .then(this.setObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.openmct.time.on("bounds", this.refreshData);
 | 
			
		||||
 | 
			
		||||
        this.status = this.openmct.status.get(this.item.identifier);
 | 
			
		||||
        this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
    beforeDestroy() {
 | 
			
		||||
        this.removeSubscription();
 | 
			
		||||
        this.removeStatusListener();
 | 
			
		||||
 | 
			
		||||
        if (this.removeSelectable) {
 | 
			
		||||
            this.removeSelectable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.openmct.time.off("bounds", this.refreshData);
 | 
			
		||||
 | 
			
		||||
        if (this.domainObject.isMutable) {
 | 
			
		||||
            this.openmct.objects.destroyMutable(this.domainObject);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        formattedValueForCopy() {
 | 
			
		||||
            const timeFormatterKey = this.openmct.time.timeSystem().key;
 | 
			
		||||
            const timeFormatter = this.formats[timeFormatterKey];
 | 
			
		||||
            const unit = this.unit ? ` ${this.unit}` : '';
 | 
			
		||||
 | 
			
		||||
            return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
 | 
			
		||||
        },
 | 
			
		||||
        requestHistoricalData() {
 | 
			
		||||
            let bounds = this.openmct.time.bounds();
 | 
			
		||||
            let options = {
 | 
			
		||||
@@ -253,12 +284,26 @@ export default {
 | 
			
		||||
                this.requestHistoricalData(this.domainObject);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        getView() {
 | 
			
		||||
            return {
 | 
			
		||||
                getViewContext: () => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        viewHistoricalData: true,
 | 
			
		||||
                        formattedValueForCopy: this.formattedValueForCopy
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        setObject(domainObject) {
 | 
			
		||||
            this.domainObject = domainObject;
 | 
			
		||||
            this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
            this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
 | 
			
		||||
            this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
 | 
			
		||||
            this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
 | 
			
		||||
 | 
			
		||||
            const valueMetadata = this.metadata.value(this.item.value);
 | 
			
		||||
            this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
 | 
			
		||||
 | 
			
		||||
            this.requestHistoricalData();
 | 
			
		||||
            this.subscribeToObject();
 | 
			
		||||
 | 
			
		||||
@@ -278,10 +323,37 @@ export default {
 | 
			
		||||
            delete this.immediatelySelect;
 | 
			
		||||
        },
 | 
			
		||||
        updateTelemetryFormat(format) {
 | 
			
		||||
            this.customStringformatter.setFormat(format);
 | 
			
		||||
 | 
			
		||||
            this.$emit('formatChanged', this.item, format);
 | 
			
		||||
        },
 | 
			
		||||
        showContextMenu(event) {
 | 
			
		||||
            this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
 | 
			
		||||
        async getContextMenuActions() {
 | 
			
		||||
            const defaultNotebook = getDefaultNotebook();
 | 
			
		||||
            const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
 | 
			
		||||
            const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
 | 
			
		||||
            const actionsObject = actionCollection.getActionsObject();
 | 
			
		||||
 | 
			
		||||
            let copyToNotebookAction = actionsObject.copyToNotebook;
 | 
			
		||||
 | 
			
		||||
            if (defaultNotebook) {
 | 
			
		||||
                const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
 | 
			
		||||
                copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
 | 
			
		||||
            } else {
 | 
			
		||||
                actionsObject.copyToNotebook = undefined;
 | 
			
		||||
                delete actionsObject.copyToNotebook;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return CONTEXT_MENU_ACTIONS.map(actionKey => {
 | 
			
		||||
                return actionsObject[actionKey];
 | 
			
		||||
            }).filter(action => action !== undefined);
 | 
			
		||||
        },
 | 
			
		||||
        async showContextMenu(event) {
 | 
			
		||||
            const contextMenuActions = await this.getContextMenuActions();
 | 
			
		||||
 | 
			
		||||
            this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
 | 
			
		||||
        },
 | 
			
		||||
        setStatus(status) {
 | 
			
		||||
            this.status = status;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										159
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/plugins/duplicate/DuplicateAction.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import DuplicateTask from './DuplicateTask';
 | 
			
		||||
 | 
			
		||||
export default class DuplicateAction {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        this.name = 'Duplicate';
 | 
			
		||||
        this.key = 'duplicate';
 | 
			
		||||
        this.description = 'Duplicate this object.';
 | 
			
		||||
        this.cssClass = "icon-duplicate";
 | 
			
		||||
        this.group = "action";
 | 
			
		||||
        this.priority = 7;
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async invoke(objectPath) {
 | 
			
		||||
        let duplicationTask = new DuplicateTask(this.openmct);
 | 
			
		||||
        let originalObject = objectPath[0];
 | 
			
		||||
        let parent = objectPath[1];
 | 
			
		||||
        let userInput = await this.getUserInput(originalObject, parent);
 | 
			
		||||
        let newParent = userInput.location;
 | 
			
		||||
        let inNavigationPath = this.inNavigationPath(originalObject);
 | 
			
		||||
 | 
			
		||||
        // legacy check
 | 
			
		||||
        if (this.isLegacyDomainObject(newParent)) {
 | 
			
		||||
            newParent = await this.convertFromLegacy(newParent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if editing, save
 | 
			
		||||
        if (inNavigationPath && this.openmct.editor.isEditing()) {
 | 
			
		||||
            this.openmct.editor.save();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // duplicate
 | 
			
		||||
        let newObject = await duplicationTask.duplicate(originalObject, newParent);
 | 
			
		||||
        this.updateNameCheck(newObject, userInput.name);
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getUserInput(originalObject, parent) {
 | 
			
		||||
        let dialogService = this.openmct.$injector.get('dialogService');
 | 
			
		||||
        let dialogForm = this.getDialogForm(originalObject, parent);
 | 
			
		||||
        let formState = {
 | 
			
		||||
            name: originalObject.name
 | 
			
		||||
        };
 | 
			
		||||
        let userInput = await dialogService.getUserInput(dialogForm, formState);
 | 
			
		||||
 | 
			
		||||
        return userInput;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateNameCheck(object, name) {
 | 
			
		||||
        if (object.name !== name) {
 | 
			
		||||
            this.openmct.objects.mutate(object, 'name', name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inNavigationPath(object) {
 | 
			
		||||
        return this.openmct.router.path
 | 
			
		||||
            .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDialogForm(object, parent) {
 | 
			
		||||
        return {
 | 
			
		||||
            name: "Duplicate Item",
 | 
			
		||||
            sections: [
 | 
			
		||||
                {
 | 
			
		||||
                    rows: [
 | 
			
		||||
                        {
 | 
			
		||||
                            key: "name",
 | 
			
		||||
                            control: "textfield",
 | 
			
		||||
                            name: "Name",
 | 
			
		||||
                            pattern: "\\S+",
 | 
			
		||||
                            required: true,
 | 
			
		||||
                            cssClass: "l-input-lg"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            name: "location",
 | 
			
		||||
                            cssClass: "grows",
 | 
			
		||||
                            control: "locator",
 | 
			
		||||
                            validate: this.validate(object, parent),
 | 
			
		||||
                            key: 'location'
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    validate(object, currentParent) {
 | 
			
		||||
        return (parentCandidate) => {
 | 
			
		||||
            let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
 | 
			
		||||
            let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
 | 
			
		||||
            let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
 | 
			
		||||
 | 
			
		||||
            if (!parentCandidate || !currentParentKeystring) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (parentCandidateKeystring === objectKeystring) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.openmct.composition.checkPolicy(
 | 
			
		||||
                parentCandidate.useCapability('adapter'),
 | 
			
		||||
                object
 | 
			
		||||
            );
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isLegacyDomainObject(domainObject) {
 | 
			
		||||
        return domainObject.getCapability !== undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async convertFromLegacy(legacyDomainObject) {
 | 
			
		||||
        let objectContext = legacyDomainObject.getCapability('context');
 | 
			
		||||
        let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
 | 
			
		||||
 | 
			
		||||
        return domainObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    appliesTo(objectPath) {
 | 
			
		||||
        let parent = objectPath[1];
 | 
			
		||||
        let parentType = parent && this.openmct.types.get(parent.type);
 | 
			
		||||
        let child = objectPath[0];
 | 
			
		||||
        let childType = child && this.openmct.types.get(child.type);
 | 
			
		||||
        let locked = child.locked ? child.locked : parent && parent.locked;
 | 
			
		||||
 | 
			
		||||
        if (locked) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return childType
 | 
			
		||||
            && childType.definition.creatable
 | 
			
		||||
            && parentType
 | 
			
		||||
            && parentType.definition.creatable
 | 
			
		||||
            && Array.isArray(parent.composition);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										273
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/plugins/duplicate/DuplicateTask.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class encapsulates the process of  duplicating/copying a domain object
 | 
			
		||||
 * and all of its children.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {DomainObject} domainObject The object to duplicate
 | 
			
		||||
 * @param {DomainObject} parent The new location of the cloned object tree
 | 
			
		||||
 * @param {src/plugins/duplicate.DuplicateService~filter} filter
 | 
			
		||||
 *        a function used to filter out objects from
 | 
			
		||||
 *        the cloning process
 | 
			
		||||
 * @constructor
 | 
			
		||||
 */
 | 
			
		||||
export default class DuplicateTask {
 | 
			
		||||
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        this.domainObject = undefined;
 | 
			
		||||
        this.parent = undefined;
 | 
			
		||||
        this.firstClone = undefined;
 | 
			
		||||
        this.filter = undefined;
 | 
			
		||||
        this.persisted = 0;
 | 
			
		||||
        this.clones = [];
 | 
			
		||||
        this.idMap = {};
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Execute the duplicate/copy task with the objects provided.
 | 
			
		||||
     * @returns {promise} Which will resolve with a clone of the object
 | 
			
		||||
     * once complete.
 | 
			
		||||
     */
 | 
			
		||||
    async duplicate(domainObject, parent, filter) {
 | 
			
		||||
        this.domainObject = domainObject;
 | 
			
		||||
        this.parent = parent;
 | 
			
		||||
        this.namespace = parent.identifier.namespace;
 | 
			
		||||
        this.filter = filter || this.isCreatable;
 | 
			
		||||
 | 
			
		||||
        await this.buildDuplicationPlan();
 | 
			
		||||
        await this.persistObjects();
 | 
			
		||||
        await this.addClonesToParent();
 | 
			
		||||
 | 
			
		||||
        return this.firstClone;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Will build a graph of an object and all of its child objects in
 | 
			
		||||
     * memory
 | 
			
		||||
     * @private
 | 
			
		||||
     * @param domainObject The original object to be copied
 | 
			
		||||
     * @param parent The parent of the original object to be copied
 | 
			
		||||
     * @returns {Promise} resolved with an array of clones of the models
 | 
			
		||||
     * of the object tree being copied. Duplicating is done in a bottom-up
 | 
			
		||||
     * fashion, so that the last member in the array is a clone of the model
 | 
			
		||||
     * object being copied. The clones are all full composed with
 | 
			
		||||
     * references to their own children.
 | 
			
		||||
     */
 | 
			
		||||
    async buildDuplicationPlan() {
 | 
			
		||||
        let domainObjectClone = await this.duplicateObject(this.domainObject);
 | 
			
		||||
 | 
			
		||||
        if (domainObjectClone !== this.domainObject) {
 | 
			
		||||
            domainObjectClone.location = this.getKeyString(this.parent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.firstClone = domainObjectClone;
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Will persist a list of {@link objectClones}. It will persist all
 | 
			
		||||
     * simultaneously, irrespective of order in the list. This may
 | 
			
		||||
     * result in automatic request batching by the browser.
 | 
			
		||||
     */
 | 
			
		||||
    async persistObjects() {
 | 
			
		||||
        let initialCount = this.clones.length;
 | 
			
		||||
        let dialog = this.openmct.overlays.progressDialog({
 | 
			
		||||
            progressPerc: 0,
 | 
			
		||||
            message: `Duplicating ${initialCount} objects.`,
 | 
			
		||||
            iconClass: 'info',
 | 
			
		||||
            title: 'Duplicating'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let clonesDone = Promise.all(this.clones.map((clone) => {
 | 
			
		||||
            let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
 | 
			
		||||
            let message = `Duplicating ${initialCount - this.persisted} objects.`;
 | 
			
		||||
 | 
			
		||||
            dialog.updateProgress(percentPersisted, message);
 | 
			
		||||
 | 
			
		||||
            return this.openmct.objects.save(clone);
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        await clonesDone;
 | 
			
		||||
 | 
			
		||||
        dialog.dismiss();
 | 
			
		||||
        this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Will add a list of clones to the specified parent's composition
 | 
			
		||||
     */
 | 
			
		||||
    async addClonesToParent() {
 | 
			
		||||
        let parentComposition = this.openmct.composition.get(this.parent);
 | 
			
		||||
        await parentComposition.load();
 | 
			
		||||
        parentComposition.add(this.firstClone);
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A recursive function that will perform a bottom-up duplicate of
 | 
			
		||||
     * the object tree with originalObject at the root. Recurses to
 | 
			
		||||
     * the farthest leaf, then works its way back up again,
 | 
			
		||||
     * cloning objects, and composing them with their child clones
 | 
			
		||||
     * as it goes
 | 
			
		||||
     * @private
 | 
			
		||||
     * @returns {DomainObject} If the type of the original object allows for
 | 
			
		||||
     * duplication, then a duplicate of the object, otherwise the object
 | 
			
		||||
     * itself (to allow linking to non duplicatable objects).
 | 
			
		||||
     */
 | 
			
		||||
    async duplicateObject(originalObject) {
 | 
			
		||||
        // Check if the creatable (or other passed in filter).
 | 
			
		||||
        if (this.filter(originalObject)) {
 | 
			
		||||
            let clone = this.cloneObjectModel(originalObject);
 | 
			
		||||
            let composeesCollection = this.openmct.composition.get(originalObject);
 | 
			
		||||
            let composees;
 | 
			
		||||
 | 
			
		||||
            if (composeesCollection) {
 | 
			
		||||
                composees = await composeesCollection.load();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.duplicateComposees(clone, composees);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Not creatable, creating a link, no need to iterate children
 | 
			
		||||
        return originalObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Given an array of objects composed by a parent, clone them, then
 | 
			
		||||
     * add them to the parent.
 | 
			
		||||
     * @private
 | 
			
		||||
     * @returns {*}
 | 
			
		||||
     */
 | 
			
		||||
    async duplicateComposees(clonedParent, composees = []) {
 | 
			
		||||
        let idMappings = [];
 | 
			
		||||
        let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
 | 
			
		||||
            await previousPromise;
 | 
			
		||||
 | 
			
		||||
            let clonedComposee = await this.duplicateObject(nextComposee);
 | 
			
		||||
 | 
			
		||||
            if (clonedComposee) {
 | 
			
		||||
                idMappings.push({
 | 
			
		||||
                    newId: clonedComposee.identifier,
 | 
			
		||||
                    oldId: nextComposee.identifier
 | 
			
		||||
                });
 | 
			
		||||
                this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }, Promise.resolve());
 | 
			
		||||
 | 
			
		||||
        await allComposeesDuplicated;
 | 
			
		||||
 | 
			
		||||
        clonedParent = this.rewriteIdentifiers(clonedParent, idMappings);
 | 
			
		||||
        this.clones.push(clonedParent);
 | 
			
		||||
 | 
			
		||||
        return clonedParent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update identifiers in a cloned object model (or part of
 | 
			
		||||
     * a cloned object model) to reflect new identifiers after
 | 
			
		||||
     * duplicating.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    rewriteIdentifiers(clonedParent, childIdMappings) {
 | 
			
		||||
        for (let { newId, oldId } of childIdMappings) {
 | 
			
		||||
            let newIdKeyString = this.openmct.objects.makeKeyString(newId);
 | 
			
		||||
            let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
 | 
			
		||||
 | 
			
		||||
            // regex replace keystrings
 | 
			
		||||
            clonedParent = JSON.stringify(clonedParent).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
 | 
			
		||||
 | 
			
		||||
            // parse reviver to replace identifiers
 | 
			
		||||
            clonedParent = JSON.parse(clonedParent, (key, value) => {
 | 
			
		||||
                if (Object.prototype.hasOwnProperty.call(value, 'key')
 | 
			
		||||
                    && Object.prototype.hasOwnProperty.call(value, 'namespace')
 | 
			
		||||
                    && value.key === oldId.key
 | 
			
		||||
                    && value.namespace === oldId.namespace) {
 | 
			
		||||
                    return newId;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return value;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return clonedParent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    composeChild(child, parent, setLocation) {
 | 
			
		||||
        parent.composition.push(child.identifier);
 | 
			
		||||
 | 
			
		||||
        //If a location is not specified, set it.
 | 
			
		||||
        if (setLocation && child.location === undefined) {
 | 
			
		||||
            let parentKeyString = this.getKeyString(parent);
 | 
			
		||||
            child.location = parentKeyString;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTypeDefinition(domainObject, definition) {
 | 
			
		||||
        let typeDefinitions = this.openmct.types.get(domainObject.type).definition;
 | 
			
		||||
 | 
			
		||||
        return typeDefinitions[definition] || false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cloneObjectModel(domainObject) {
 | 
			
		||||
        let clone = JSON.parse(JSON.stringify(domainObject));
 | 
			
		||||
        let identifier = {
 | 
			
		||||
            key: uuid(),
 | 
			
		||||
            namespace: this.namespace // set to NEW parent's namespace
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (clone.modified || clone.persisted || clone.location) {
 | 
			
		||||
            clone.modified = undefined;
 | 
			
		||||
            clone.persisted = undefined;
 | 
			
		||||
            clone.location = undefined;
 | 
			
		||||
            delete clone.modified;
 | 
			
		||||
            delete clone.persisted;
 | 
			
		||||
            delete clone.location;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (clone.composition) {
 | 
			
		||||
            clone.composition = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clone.identifier = identifier;
 | 
			
		||||
 | 
			
		||||
        return clone;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getKeyString(domainObject) {
 | 
			
		||||
        return this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isCreatable(domainObject) {
 | 
			
		||||
        return this.getTypeDefinition(domainObject, 'creatable');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/plugins/duplicate/plugin.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2019, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import DuplicateAction from "./DuplicateAction";
 | 
			
		||||
 | 
			
		||||
export default function () {
 | 
			
		||||
    return function (openmct) {
 | 
			
		||||
        openmct.actions.register(new DuplicateAction(openmct));
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										159
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/plugins/duplicate/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2020, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import DuplicateActionPlugin from './plugin.js';
 | 
			
		||||
import DuplicateAction from './DuplicateAction.js';
 | 
			
		||||
import DuplicateTask from './DuplicateTask.js';
 | 
			
		||||
import {
 | 
			
		||||
    createOpenMct,
 | 
			
		||||
    resetApplicationState,
 | 
			
		||||
    getMockObjects
 | 
			
		||||
} from 'utils/testing';
 | 
			
		||||
 | 
			
		||||
describe("The Duplicate Action plugin", () => {
 | 
			
		||||
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let duplicateTask;
 | 
			
		||||
    let childObject;
 | 
			
		||||
    let parentObject;
 | 
			
		||||
    let anotherParentObject;
 | 
			
		||||
 | 
			
		||||
    // this setups up the app
 | 
			
		||||
    beforeEach((done) => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
 | 
			
		||||
        childObject = getMockObjects({
 | 
			
		||||
            objectKeyStrings: ['folder'],
 | 
			
		||||
            overwrite: {
 | 
			
		||||
                folder: {
 | 
			
		||||
                    name: "Child Folder",
 | 
			
		||||
                    identifier: {
 | 
			
		||||
                        namespace: "",
 | 
			
		||||
                        key: "child-folder-object"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }).folder;
 | 
			
		||||
        parentObject = getMockObjects({
 | 
			
		||||
            objectKeyStrings: ['folder'],
 | 
			
		||||
            overwrite: {
 | 
			
		||||
                folder: {
 | 
			
		||||
                    name: "Parent Folder",
 | 
			
		||||
                    type: "folder",
 | 
			
		||||
                    composition: [childObject.identifier]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }).folder;
 | 
			
		||||
        anotherParentObject = getMockObjects({
 | 
			
		||||
            objectKeyStrings: ['folder'],
 | 
			
		||||
            overwrite: {
 | 
			
		||||
                folder: {
 | 
			
		||||
                    name: "Another Parent Folder"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }).folder;
 | 
			
		||||
 | 
			
		||||
        let objectGet = openmct.objects.get.bind(openmct.objects);
 | 
			
		||||
        spyOn(openmct.objects, 'get').and.callFake((identifier) => {
 | 
			
		||||
            let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key);
 | 
			
		||||
 | 
			
		||||
            if (!obj) {
 | 
			
		||||
                // not one of the mocked objs, callthrough basically
 | 
			
		||||
                return objectGet(identifier);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Promise.resolve(obj);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
 | 
			
		||||
            return {
 | 
			
		||||
                load: async () => {
 | 
			
		||||
                    let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key);
 | 
			
		||||
                    let children = [];
 | 
			
		||||
 | 
			
		||||
                    if (obj) {
 | 
			
		||||
                        for (let i = 0; i < obj.composition.length; i++) {
 | 
			
		||||
                            children.push(await openmct.objects.get(obj.composition[i]));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return Promise.resolve(children);
 | 
			
		||||
                },
 | 
			
		||||
                add: (child) => {
 | 
			
		||||
                    domainObject.composition.push(child.identifier);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // already installed by default, but never hurts, just adds to context menu
 | 
			
		||||
        openmct.install(DuplicateActionPlugin());
 | 
			
		||||
        openmct.types.addType('folder', {creatable: true});
 | 
			
		||||
 | 
			
		||||
        openmct.on('start', done);
 | 
			
		||||
        openmct.startHeadless();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should be defined", () => {
 | 
			
		||||
        expect(DuplicateActionPlugin).toBeDefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("when moving an object to a new parent", () => {
 | 
			
		||||
 | 
			
		||||
        beforeEach(async (done) => {
 | 
			
		||||
            duplicateTask = new DuplicateTask(openmct);
 | 
			
		||||
            await duplicateTask.duplicate(parentObject, anotherParentObject);
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
 | 
			
		||||
            let duplicatedObjectIdentifier = anotherParentObject.composition[0];
 | 
			
		||||
            let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier);
 | 
			
		||||
            let duplicateObjectName = duplicatedObject.name;
 | 
			
		||||
 | 
			
		||||
            expect(duplicateObjectName).toEqual(parentObject.name);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("the duplicate child object's identifier should be new", () => {
 | 
			
		||||
            let duplicatedObjectIdentifier = anotherParentObject.composition[0];
 | 
			
		||||
 | 
			
		||||
            expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("when a new name is provided for the duplicated object", () => {
 | 
			
		||||
        const NEW_NAME = 'New Name';
 | 
			
		||||
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            duplicateTask = new DuplicateAction(openmct);
 | 
			
		||||
            duplicateTask.updateNameCheck(parentObject, NEW_NAME);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("the name is updated", () => {
 | 
			
		||||
            let childName = parentObject.name;
 | 
			
		||||
            expect(childName).toEqual(NEW_NAME);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,27 @@
 | 
			
		||||
<template>
 | 
			
		||||
<tr
 | 
			
		||||
    class="c-list-item"
 | 
			
		||||
    :class="{ 'is-alias': item.isAlias === true }"
 | 
			
		||||
    :class="{
 | 
			
		||||
        'is-alias': item.isAlias === true
 | 
			
		||||
    }"
 | 
			
		||||
    @click="navigate"
 | 
			
		||||
>
 | 
			
		||||
    <td class="c-list-item__name">
 | 
			
		||||
        <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 +41,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();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user