mirror of
https://github.com/nasa/openmct.git
synced 2025-06-26 03:00:13 +00:00
Compare commits
31 Commits
fix-plot-s
...
tests-acti
Author | SHA1 | Date | |
---|---|---|---|
c567692c23 | |||
4a0728a55b | |||
2e1d57aa8c | |||
e20c838837 | |||
1c2b0678be | |||
6f810add43 | |||
12727adb16 | |||
9da750c3bb | |||
176226ddef | |||
acea18fa70 | |||
d1656f8561 | |||
87751e882c | |||
dff393a714 | |||
fd9c9aee03 | |||
59bf981fb0 | |||
4bbdac759f | |||
13fe7509de | |||
6fd8f6cd43 | |||
2c838c0acd | |||
c669f34ebc | |||
e15110ae97 | |||
6375ecda34 | |||
d232dacc65 | |||
59946e89ef | |||
d75c4b4049 | |||
30ca4b707d | |||
27704c9a48 | |||
b0203f2272 | |||
77b720d00d | |||
ba982671b2 | |||
5df7d92d64 |
32
index.html
32
index.html
@ -30,6 +30,38 @@
|
|||||||
<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-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-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">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.3.3-SNAPSHOT",
|
"version": "1.4.1-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -143,8 +143,8 @@ define([
|
|||||||
"$window"
|
"$window"
|
||||||
],
|
],
|
||||||
"group": "windowing",
|
"group": "windowing",
|
||||||
"cssClass": "icon-new-window",
|
"priority": 10,
|
||||||
"priority": "preferred"
|
"cssClass": "icon-new-window"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"runs": [
|
"runs": [
|
||||||
|
@ -139,7 +139,9 @@ define([
|
|||||||
],
|
],
|
||||||
"description": "Edit",
|
"description": "Edit",
|
||||||
"category": "view-control",
|
"category": "view-control",
|
||||||
"cssClass": "major icon-pencil"
|
"cssClass": "major icon-pencil",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "properties",
|
"key": "properties",
|
||||||
@ -150,6 +152,8 @@ define([
|
|||||||
"implementation": PropertiesAction,
|
"implementation": PropertiesAction,
|
||||||
"cssClass": "major icon-pencil",
|
"cssClass": "major icon-pencil",
|
||||||
"name": "Edit Properties...",
|
"name": "Edit Properties...",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 10,
|
||||||
"description": "Edit properties of this object.",
|
"description": "Edit properties of this object.",
|
||||||
"depends": [
|
"depends": [
|
||||||
"dialogService"
|
"dialogService"
|
||||||
|
@ -20,12 +20,12 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<div class="c-object-label"
|
<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()}}"
|
<div class="c-object-label__type-icon {{type.getCssClass()}}"
|
||||||
ng-class="{ 'l-icon-link':location.isLink() }"
|
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>
|
||||||
<div class='c-object-label__name'>{{model.name}}</div>
|
<div class='c-object-label__name'>{{model.name}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,32 +21,24 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
"./src/actions/MoveAction",
|
|
||||||
"./src/actions/CopyAction",
|
|
||||||
"./src/actions/LinkAction",
|
"./src/actions/LinkAction",
|
||||||
"./src/actions/SetPrimaryLocationAction",
|
"./src/actions/SetPrimaryLocationAction",
|
||||||
"./src/services/LocatingCreationDecorator",
|
"./src/services/LocatingCreationDecorator",
|
||||||
"./src/services/LocatingObjectDecorator",
|
"./src/services/LocatingObjectDecorator",
|
||||||
"./src/policies/CopyPolicy",
|
"./src/policies/CopyPolicy",
|
||||||
"./src/policies/CrossSpacePolicy",
|
"./src/policies/CrossSpacePolicy",
|
||||||
"./src/policies/MovePolicy",
|
|
||||||
"./src/capabilities/LocationCapability",
|
"./src/capabilities/LocationCapability",
|
||||||
"./src/services/MoveService",
|
|
||||||
"./src/services/LinkService",
|
"./src/services/LinkService",
|
||||||
"./src/services/CopyService",
|
"./src/services/CopyService",
|
||||||
"./src/services/LocationService"
|
"./src/services/LocationService"
|
||||||
], function (
|
], function (
|
||||||
MoveAction,
|
|
||||||
CopyAction,
|
|
||||||
LinkAction,
|
LinkAction,
|
||||||
SetPrimaryLocationAction,
|
SetPrimaryLocationAction,
|
||||||
LocatingCreationDecorator,
|
LocatingCreationDecorator,
|
||||||
LocatingObjectDecorator,
|
LocatingObjectDecorator,
|
||||||
CopyPolicy,
|
CopyPolicy,
|
||||||
CrossSpacePolicy,
|
CrossSpacePolicy,
|
||||||
MovePolicy,
|
|
||||||
LocationCapability,
|
LocationCapability,
|
||||||
MoveService,
|
|
||||||
LinkService,
|
LinkService,
|
||||||
CopyService,
|
CopyService,
|
||||||
LocationService
|
LocationService
|
||||||
@ -60,41 +52,14 @@ define([
|
|||||||
"configuration": {},
|
"configuration": {},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"actions": [
|
"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",
|
"key": "link",
|
||||||
"name": "Create Link",
|
"name": "Create Link",
|
||||||
"description": "Create Link to object in another location.",
|
"description": "Create Link to object in another location.",
|
||||||
"cssClass": "icon-link",
|
"cssClass": "icon-link",
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 7,
|
||||||
"implementation": LinkAction,
|
"implementation": LinkAction,
|
||||||
"depends": [
|
"depends": [
|
||||||
"policyService",
|
"policyService",
|
||||||
@ -135,10 +100,6 @@ define([
|
|||||||
{
|
{
|
||||||
"category": "action",
|
"category": "action",
|
||||||
"implementation": CopyPolicy
|
"implementation": CopyPolicy
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "action",
|
|
||||||
"implementation": MovePolicy
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@ -154,17 +115,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": [
|
"services": [
|
||||||
{
|
|
||||||
"key": "moveService",
|
|
||||||
"name": "Move Service",
|
|
||||||
"description": "Provides a service for moving objects",
|
|
||||||
"implementation": MoveService,
|
|
||||||
"depends": [
|
|
||||||
"openmct",
|
|
||||||
"linkService",
|
|
||||||
"$q"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "linkService",
|
"key": "linkService",
|
||||||
"name": "Link Service",
|
"name": "Link Service",
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
['./AbstractComposeAction', './CancelError'],
|
|
||||||
function (AbstractComposeAction, CancelError) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The CopyAction is available from context menus and allows a user to
|
|
||||||
* deep copy an object to another location of their choosing.
|
|
||||||
*
|
|
||||||
* @implements {Action}
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/entanglement
|
|
||||||
*/
|
|
||||||
function CopyAction(
|
|
||||||
$log,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
) {
|
|
||||||
this.dialog = undefined;
|
|
||||||
this.notification = undefined;
|
|
||||||
this.dialogService = dialogService;
|
|
||||||
this.notificationService = notificationService;
|
|
||||||
this.$log = $log;
|
|
||||||
//Extend the behaviour of the Abstract Compose Action
|
|
||||||
AbstractComposeAction.call(
|
|
||||||
this,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
context,
|
|
||||||
"Duplicate",
|
|
||||||
"To a Location"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates user about progress of copy. Should not be invoked by
|
|
||||||
* client code under any circumstances.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param phase
|
|
||||||
* @param totalObjects
|
|
||||||
* @param processed
|
|
||||||
*/
|
|
||||||
CopyAction.prototype.progress = function (phase, totalObjects, processed) {
|
|
||||||
/*
|
|
||||||
Copy has two distinct phases. In the first phase a copy plan is
|
|
||||||
made in memory. During this phase of execution, the user is
|
|
||||||
shown a blocking 'modal' dialog.
|
|
||||||
|
|
||||||
In the second phase, the copying is taking place, and the user
|
|
||||||
is shown non-invasive banner notifications at the bottom of the screen.
|
|
||||||
*/
|
|
||||||
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
|
|
||||||
this.dialog = this.dialogService.showBlockingMessage({
|
|
||||||
title: "Preparing to copy objects",
|
|
||||||
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
|
|
||||||
unknownProgress: true,
|
|
||||||
severity: "info"
|
|
||||||
});
|
|
||||||
} else if (phase.toLowerCase() === "copying") {
|
|
||||||
if (this.dialog) {
|
|
||||||
this.dialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.notification) {
|
|
||||||
this.notification = this.notificationService
|
|
||||||
.notify({
|
|
||||||
title: "Copying objects",
|
|
||||||
unknownProgress: false,
|
|
||||||
severity: "info"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notification.model.progress = (processed / totalObjects) * 100;
|
|
||||||
this.notification.model.title = ["Copied ", processed, "of ",
|
|
||||||
totalObjects, "objects"].join(" ");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the CopyAction. The CopyAction uses the default behaviour of
|
|
||||||
* the AbstractComposeAction, but extends it to support notification
|
|
||||||
* updates of progress on copy.
|
|
||||||
*/
|
|
||||||
CopyAction.prototype.perform = function () {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function success(domainObject) {
|
|
||||||
var domainObjectName = domainObject.model.name;
|
|
||||||
|
|
||||||
self.notification.dismiss();
|
|
||||||
self.notificationService.info(domainObjectName + " copied successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(errorDetails) {
|
|
||||||
// No need to notify user of their own cancellation
|
|
||||||
if (errorDetails instanceof CancelError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorDialog,
|
|
||||||
errorMessage = {
|
|
||||||
title: "Error copying objects.",
|
|
||||||
severity: "error",
|
|
||||||
hint: errorDetails.message,
|
|
||||||
minimized: true, // want the notification to be minimized initially (don't show banner)
|
|
||||||
options: [{
|
|
||||||
label: "OK",
|
|
||||||
callback: function () {
|
|
||||||
errorDialog.dismiss();
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
self.dialog.dismiss();
|
|
||||||
if (self.notification) {
|
|
||||||
self.notification.dismiss(); // Clear the progress notification
|
|
||||||
}
|
|
||||||
|
|
||||||
self.$log.error("Error copying objects. ", errorDetails);
|
|
||||||
//Show a minimized notification of error for posterity
|
|
||||||
self.notificationService.notify(errorMessage);
|
|
||||||
//Display a blocking message
|
|
||||||
errorDialog = self.dialogService.showBlockingMessage(errorMessage);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function notification(details) {
|
|
||||||
self.progress(details.phase, details.totalObjects, details.processed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AbstractComposeAction.prototype.perform.call(this)
|
|
||||||
.then(success, error, notification);
|
|
||||||
};
|
|
||||||
|
|
||||||
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
|
|
||||||
|
|
||||||
return CopyAction;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,104 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
function () {
|
|
||||||
/**
|
|
||||||
* MoveService provides an interface for moving objects from one
|
|
||||||
* location to another. It also provides a method for determining if
|
|
||||||
* an object can be copied to a specific location.
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/entanglement
|
|
||||||
* @implements {platform/entanglement.AbstractComposeService}
|
|
||||||
*/
|
|
||||||
function MoveService(openmct, linkService) {
|
|
||||||
this.openmct = openmct;
|
|
||||||
this.linkService = linkService;
|
|
||||||
}
|
|
||||||
|
|
||||||
MoveService.prototype.validate = function (object, parentCandidate) {
|
|
||||||
var currentParent = object
|
|
||||||
.getCapability('context')
|
|
||||||
.getParent();
|
|
||||||
|
|
||||||
if (!parentCandidate || !parentCandidate.getId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getId() === currentParent.getId()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getId() === object.getId()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.openmct.composition.checkPolicy(
|
|
||||||
parentCandidate.useCapability('adapter'),
|
|
||||||
object.useCapability('adapter')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MoveService.prototype.perform = function (object, parentObject) {
|
|
||||||
function relocate(objectInNewContext) {
|
|
||||||
var newLocationCapability = objectInNewContext
|
|
||||||
.getCapability('location'),
|
|
||||||
oldLocationCapability = object
|
|
||||||
.getCapability('location');
|
|
||||||
|
|
||||||
if (!newLocationCapability
|
|
||||||
|| !oldLocationCapability) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldLocationCapability.isOriginal()) {
|
|
||||||
return newLocationCapability.setPrimaryLocation(
|
|
||||||
newLocationCapability
|
|
||||||
.getContextualLocation()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.validate(object, parentObject)) {
|
|
||||||
throw new Error(
|
|
||||||
"Tried to move objects without validating first."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.linkService
|
|
||||||
.perform(object, parentObject)
|
|
||||||
.then(relocate)
|
|
||||||
.then(function () {
|
|
||||||
return object
|
|
||||||
.getCapability('action')
|
|
||||||
.perform('remove', true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return MoveService;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
[
|
|
||||||
'../../src/actions/CopyAction',
|
|
||||||
'../services/MockCopyService',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
],
|
|
||||||
function (CopyAction, MockCopyService, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("Copy Action", function () {
|
|
||||||
|
|
||||||
var copyAction,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
locationServicePromise,
|
|
||||||
copyService,
|
|
||||||
context,
|
|
||||||
selectedObject,
|
|
||||||
selectedObjectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
newParent,
|
|
||||||
notificationService,
|
|
||||||
notification,
|
|
||||||
dialogService,
|
|
||||||
mockDialog,
|
|
||||||
mockLog,
|
|
||||||
abstractComposePromise,
|
|
||||||
domainObject = {model: {name: "mockObject"}},
|
|
||||||
progress = {
|
|
||||||
phase: "copying",
|
|
||||||
totalObjects: 10,
|
|
||||||
processed: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
|
|
||||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
|
||||||
'selectedObjectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedObject = domainObjectFactory({
|
|
||||||
name: 'selectedObject',
|
|
||||||
model: {
|
|
||||||
name: 'selectedObject'
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
context: selectedObjectContextCapability
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedObjectContextCapability
|
|
||||||
.getParent
|
|
||||||
.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
newParent = domainObjectFactory({
|
|
||||||
name: 'newParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService = jasmine.createSpyObj(
|
|
||||||
'locationService',
|
|
||||||
[
|
|
||||||
'getLocationFromUser'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationServicePromise = jasmine.createSpyObj(
|
|
||||||
'locationServicePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
abstractComposePromise = jasmine.createSpyObj(
|
|
||||||
'abstractComposePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
abstractComposePromise.then.and.callFake(function (success, error, notify) {
|
|
||||||
notify(progress);
|
|
||||||
success(domainObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
locationServicePromise.then.and.callFake(function (callback) {
|
|
||||||
callback(newParent);
|
|
||||||
|
|
||||||
return abstractComposePromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService
|
|
||||||
.getLocationFromUser
|
|
||||||
.and.returnValue(locationServicePromise);
|
|
||||||
|
|
||||||
dialogService = jasmine.createSpyObj('dialogService',
|
|
||||||
['showBlockingMessage']
|
|
||||||
);
|
|
||||||
|
|
||||||
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
|
|
||||||
dialogService.showBlockingMessage.and.returnValue(mockDialog);
|
|
||||||
|
|
||||||
notification = jasmine.createSpyObj('notification',
|
|
||||||
['dismiss', 'model']
|
|
||||||
);
|
|
||||||
|
|
||||||
notificationService = jasmine.createSpyObj('notificationService',
|
|
||||||
['notify', 'info']
|
|
||||||
);
|
|
||||||
|
|
||||||
notificationService.notify.and.returnValue(notification);
|
|
||||||
|
|
||||||
mockLog = jasmine.createSpyObj('log', ['error']);
|
|
||||||
|
|
||||||
copyService = new MockCopyService();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from context-action", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
domainObject: selectedObject
|
|
||||||
};
|
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
|
||||||
mockLog,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(copyAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when performed it", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
spyOn(copyAction, 'progress').and.callThrough();
|
|
||||||
copyAction.perform();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts for location", function () {
|
|
||||||
expect(locationService.getLocationFromUser)
|
|
||||||
.toHaveBeenCalledWith(
|
|
||||||
"Duplicate selectedObject To a Location",
|
|
||||||
"Duplicate To",
|
|
||||||
jasmine.any(Function),
|
|
||||||
currentParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for location and handles cancellation by user", function () {
|
|
||||||
expect(locationServicePromise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("copies object to selected location", function () {
|
|
||||||
locationServicePromise
|
|
||||||
.then
|
|
||||||
.calls.mostRecent()
|
|
||||||
.args[0](newParent);
|
|
||||||
|
|
||||||
expect(copyService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("notifies the user of progress", function () {
|
|
||||||
expect(notificationService.info).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("notifies the user with name of object copied", function () {
|
|
||||||
expect(notificationService.info)
|
|
||||||
.toHaveBeenCalledWith("mockObject copied successfully.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from drag-drop", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
selectedObject: selectedObject,
|
|
||||||
domainObject: newParent
|
|
||||||
};
|
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
|
||||||
mockLog,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(copyAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("performs copy immediately", function () {
|
|
||||||
copyAction.perform();
|
|
||||||
expect(copyService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,178 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
[
|
|
||||||
'../../src/actions/MoveAction',
|
|
||||||
'../services/MockMoveService',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
],
|
|
||||||
function (MoveAction, MockMoveService, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("Move Action", function () {
|
|
||||||
|
|
||||||
var moveAction,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
locationServicePromise,
|
|
||||||
moveService,
|
|
||||||
context,
|
|
||||||
selectedObject,
|
|
||||||
selectedObjectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
newParent;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
|
|
||||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
|
||||||
'selectedObjectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedObject = domainObjectFactory({
|
|
||||||
name: 'selectedObject',
|
|
||||||
model: {
|
|
||||||
name: 'selectedObject'
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
context: selectedObjectContextCapability
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedObjectContextCapability
|
|
||||||
.getParent
|
|
||||||
.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
newParent = domainObjectFactory({
|
|
||||||
name: 'newParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService = jasmine.createSpyObj(
|
|
||||||
'locationService',
|
|
||||||
[
|
|
||||||
'getLocationFromUser'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationServicePromise = jasmine.createSpyObj(
|
|
||||||
'locationServicePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationService
|
|
||||||
.getLocationFromUser
|
|
||||||
.and.returnValue(locationServicePromise);
|
|
||||||
|
|
||||||
moveService = new MockMoveService();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from context-action", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
domainObject: selectedObject
|
|
||||||
};
|
|
||||||
|
|
||||||
moveAction = new MoveAction(
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
moveService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(moveAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when performed it", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
moveAction.perform();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts for location", function () {
|
|
||||||
expect(locationService.getLocationFromUser)
|
|
||||||
.toHaveBeenCalledWith(
|
|
||||||
"Move selectedObject To a New Location",
|
|
||||||
"Move To",
|
|
||||||
jasmine.any(Function),
|
|
||||||
currentParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for location and handles cancellation by user", function () {
|
|
||||||
expect(locationServicePromise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moves object to selected location", function () {
|
|
||||||
locationServicePromise
|
|
||||||
.then
|
|
||||||
.calls.mostRecent()
|
|
||||||
.args[0](newParent);
|
|
||||||
|
|
||||||
expect(moveService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from drag-drop", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
selectedObject: selectedObject,
|
|
||||||
domainObject: newParent
|
|
||||||
};
|
|
||||||
|
|
||||||
moveAction = new MoveAction(
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
moveService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(moveAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("performs move immediately", function () {
|
|
||||||
moveAction.perform();
|
|
||||||
expect(moveService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,124 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define([
|
|
||||||
'../../src/policies/MovePolicy',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
], function (MovePolicy, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("MovePolicy", function () {
|
|
||||||
var testMetadata,
|
|
||||||
testContext,
|
|
||||||
mockDomainObject,
|
|
||||||
mockParent,
|
|
||||||
mockParentType,
|
|
||||||
mockType,
|
|
||||||
mockAction,
|
|
||||||
policy;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
var mockContextCapability =
|
|
||||||
jasmine.createSpyObj('context', ['getParent']);
|
|
||||||
|
|
||||||
mockType =
|
|
||||||
jasmine.createSpyObj('type', ['hasFeature']);
|
|
||||||
mockParentType =
|
|
||||||
jasmine.createSpyObj('parent-type', ['hasFeature']);
|
|
||||||
|
|
||||||
testMetadata = {};
|
|
||||||
|
|
||||||
mockDomainObject = domainObjectFactory({
|
|
||||||
capabilities: {
|
|
||||||
context: mockContextCapability,
|
|
||||||
type: mockType
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mockParent = domainObjectFactory({
|
|
||||||
capabilities: {
|
|
||||||
type: mockParentType
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mockContextCapability.getParent.and.returnValue(mockParent);
|
|
||||||
|
|
||||||
mockType.hasFeature.and.callFake(function (feature) {
|
|
||||||
return feature === 'creation';
|
|
||||||
});
|
|
||||||
mockParentType.hasFeature.and.callFake(function (feature) {
|
|
||||||
return feature === 'creation';
|
|
||||||
});
|
|
||||||
|
|
||||||
mockAction = jasmine.createSpyObj('action', ['getMetadata']);
|
|
||||||
mockAction.getMetadata.and.returnValue(testMetadata);
|
|
||||||
|
|
||||||
testContext = { domainObject: mockDomainObject };
|
|
||||||
|
|
||||||
policy = new MovePolicy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for move actions", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
testMetadata.key = 'move';
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when an object is non-modifiable", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
mockType.hasFeature.and.returnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disallows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when a parent is non-modifiable", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
mockParentType.hasFeature.and.returnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disallows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when an object and its parent are modifiable", function () {
|
|
||||||
it("allows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for other actions", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
testMetadata.key = 'foo';
|
|
||||||
});
|
|
||||||
|
|
||||||
it("simply allows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
mockType.hasFeature.and.returnValue(false);
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
mockParentType.hasFeature.and.returnValue(false);
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,96 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
function () {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MockMoveService provides the same interface as the moveService,
|
|
||||||
* returning promises where it would normally do so. At it's core,
|
|
||||||
* it is a jasmine spy object, but it also tracks the promises it
|
|
||||||
* returns and provides shortcut methods for resolving those promises
|
|
||||||
* synchronously.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* var moveService = new MockMoveService();
|
|
||||||
*
|
|
||||||
* // validate is a standard jasmine spy.
|
|
||||||
* moveService.validate.and.returnValue(true);
|
|
||||||
* var isValid = moveService.validate(object, parentCandidate);
|
|
||||||
* expect(isValid).toBe(true);
|
|
||||||
*
|
|
||||||
* // perform returns promises and tracks them.
|
|
||||||
* var whenCopied = jasmine.createSpy('whenCopied');
|
|
||||||
* moveService.perform(object, parentObject).then(whenCopied);
|
|
||||||
* expect(whenCopied).not.toHaveBeenCalled();
|
|
||||||
* moveService.perform.calls.mostRecent().resolve('someArg');
|
|
||||||
* expect(whenCopied).toHaveBeenCalledWith('someArg');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
function MockMoveService() {
|
|
||||||
// track most recent call of a function,
|
|
||||||
// perform automatically returns
|
|
||||||
var mockMoveService = jasmine.createSpyObj(
|
|
||||||
'MockMoveService',
|
|
||||||
[
|
|
||||||
'validate',
|
|
||||||
'perform'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
mockMoveService.perform.and.callFake(() => {
|
|
||||||
var performPromise,
|
|
||||||
callExtensions,
|
|
||||||
spy;
|
|
||||||
|
|
||||||
performPromise = jasmine.createSpyObj(
|
|
||||||
'performPromise',
|
|
||||||
['then']
|
|
||||||
);
|
|
||||||
|
|
||||||
callExtensions = {
|
|
||||||
promise: performPromise,
|
|
||||||
resolve: function (resolveWith) {
|
|
||||||
performPromise.then.calls.all().forEach(function (call) {
|
|
||||||
call.args[0](resolveWith);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
spy = mockMoveService.perform;
|
|
||||||
|
|
||||||
Object.keys(callExtensions).forEach(function (key) {
|
|
||||||
spy.calls.mostRecent()[key] = callExtensions[key];
|
|
||||||
spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return performPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mockMoveService;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MockMoveService;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,260 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
define(
|
|
||||||
[
|
|
||||||
'../../src/services/MoveService',
|
|
||||||
'../services/MockLinkService',
|
|
||||||
'../DomainObjectFactory',
|
|
||||||
'../ControlledPromise'
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
MoveService,
|
|
||||||
MockLinkService,
|
|
||||||
domainObjectFactory,
|
|
||||||
ControlledPromise
|
|
||||||
) {
|
|
||||||
|
|
||||||
xdescribe("MoveService", function () {
|
|
||||||
|
|
||||||
var moveService,
|
|
||||||
policyService,
|
|
||||||
object,
|
|
||||||
objectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
parentCandidate,
|
|
||||||
linkService;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
objectContextCapability = jasmine.createSpyObj(
|
|
||||||
'objectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
object = domainObjectFactory({
|
|
||||||
name: 'object',
|
|
||||||
id: 'a',
|
|
||||||
capabilities: {
|
|
||||||
context: objectContextCapability,
|
|
||||||
type: { type: 'object' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent',
|
|
||||||
id: 'b'
|
|
||||||
});
|
|
||||||
|
|
||||||
objectContextCapability.getParent.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
parentCandidate = domainObjectFactory({
|
|
||||||
name: 'parentCandidate',
|
|
||||||
model: { composition: [] },
|
|
||||||
id: 'c',
|
|
||||||
capabilities: {
|
|
||||||
type: { type: 'parentCandidate' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
linkService = new MockLinkService();
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
moveService = new MoveService(policyService, linkService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validate", function () {
|
|
||||||
var validate;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
validate = function () {
|
|
||||||
return moveService.validate(object, parentCandidate);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow an invalid parent", function () {
|
|
||||||
parentCandidate = undefined;
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
parentCandidate = {};
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to current parent", function () {
|
|
||||||
parentCandidate.id = currentParent.id = 'xyz';
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to self", function () {
|
|
||||||
object.id = parentCandidate.id = 'xyz';
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to the same location", function () {
|
|
||||||
object.id = 'abc';
|
|
||||||
parentCandidate.model.composition = ['abc'];
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("defers to policyService", function () {
|
|
||||||
|
|
||||||
it("calls policy service with correct args", function () {
|
|
||||||
validate();
|
|
||||||
expect(policyService.allow).toHaveBeenCalledWith(
|
|
||||||
"composition",
|
|
||||||
parentCandidate,
|
|
||||||
object
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("and returns false", function () {
|
|
||||||
policyService.allow.and.returnValue(false);
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("and returns true", function () {
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
expect(validate()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("perform", function () {
|
|
||||||
|
|
||||||
var actionCapability,
|
|
||||||
locationCapability,
|
|
||||||
locationPromise,
|
|
||||||
newParent,
|
|
||||||
moveResult;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
newParent = parentCandidate;
|
|
||||||
|
|
||||||
actionCapability = jasmine.createSpyObj(
|
|
||||||
'actionCapability',
|
|
||||||
['perform']
|
|
||||||
);
|
|
||||||
|
|
||||||
locationCapability = jasmine.createSpyObj(
|
|
||||||
'locationCapability',
|
|
||||||
[
|
|
||||||
'isOriginal',
|
|
||||||
'setPrimaryLocation',
|
|
||||||
'getContextualLocation'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationPromise = new ControlledPromise();
|
|
||||||
locationCapability.setPrimaryLocation
|
|
||||||
.and.returnValue(locationPromise);
|
|
||||||
|
|
||||||
object = domainObjectFactory({
|
|
||||||
name: 'object',
|
|
||||||
capabilities: {
|
|
||||||
action: actionCapability,
|
|
||||||
location: locationCapability,
|
|
||||||
context: objectContextCapability,
|
|
||||||
type: { type: 'object' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
moveResult = moveService.perform(object, newParent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("links object to newParent", function () {
|
|
||||||
expect(linkService.perform).toHaveBeenCalledWith(
|
|
||||||
object,
|
|
||||||
newParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise", function () {
|
|
||||||
expect(moveResult.then).toEqual(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for result of link", function () {
|
|
||||||
expect(linkService.perform.calls.mostRecent().promise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws an error when performed on invalid inputs", function () {
|
|
||||||
function perform() {
|
|
||||||
moveService.perform(object, newParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
spyOn(moveService, "validate");
|
|
||||||
moveService.validate.and.returnValue(true);
|
|
||||||
expect(perform).not.toThrow();
|
|
||||||
moveService.validate.and.returnValue(false);
|
|
||||||
expect(perform).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when moving an original", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationCapability.getContextualLocation
|
|
||||||
.and.returnValue('new-location');
|
|
||||||
locationCapability.isOriginal.and.returnValue(true);
|
|
||||||
linkService.perform.calls.mostRecent().promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates location", function () {
|
|
||||||
expect(locationCapability.setPrimaryLocation)
|
|
||||||
.toHaveBeenCalledWith('new-location');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("after location update", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationPromise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes object from parent without user warning dialog", function () {
|
|
||||||
expect(actionCapability.perform)
|
|
||||||
.toHaveBeenCalledWith('remove', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when moving a link", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationCapability.isOriginal.and.returnValue(false);
|
|
||||||
linkService.perform.calls.mostRecent().promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not update location", function () {
|
|
||||||
expect(locationCapability.setPrimaryLocation)
|
|
||||||
.not
|
|
||||||
.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes object from parent without user warning dialog", function () {
|
|
||||||
expect(actionCapability.perform)
|
|
||||||
.toHaveBeenCalledWith('remove', true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -47,6 +47,8 @@ define([
|
|||||||
"implementation": ExportAsJSONAction,
|
"implementation": ExportAsJSONAction,
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"cssClass": "icon-export",
|
"cssClass": "icon-export",
|
||||||
|
"group": "json",
|
||||||
|
"priority": 2,
|
||||||
"depends": [
|
"depends": [
|
||||||
"openmct",
|
"openmct",
|
||||||
"exportService",
|
"exportService",
|
||||||
@ -61,6 +63,8 @@ define([
|
|||||||
"implementation": ImportAsJSONAction,
|
"implementation": ImportAsJSONAction,
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"cssClass": "icon-import",
|
"cssClass": "icon-import",
|
||||||
|
"group": "json",
|
||||||
|
"priority": 2,
|
||||||
"depends": [
|
"depends": [
|
||||||
"exportService",
|
"exportService",
|
||||||
"identifierService",
|
"identifierService",
|
||||||
|
14
src/MCT.js
14
src/MCT.js
@ -46,6 +46,8 @@ define([
|
|||||||
'./api/Branding',
|
'./api/Branding',
|
||||||
'./plugins/licenses/plugin',
|
'./plugins/licenses/plugin',
|
||||||
'./plugins/remove/plugin',
|
'./plugins/remove/plugin',
|
||||||
|
'./plugins/move/plugin',
|
||||||
|
'./plugins/duplicate/plugin',
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -73,6 +75,8 @@ define([
|
|||||||
BrandingAPI,
|
BrandingAPI,
|
||||||
LicensesPlugin,
|
LicensesPlugin,
|
||||||
RemoveActionPlugin,
|
RemoveActionPlugin,
|
||||||
|
MoveActionPlugin,
|
||||||
|
DuplicateActionPlugin,
|
||||||
Vue
|
Vue
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
@ -242,7 +246,11 @@ define([
|
|||||||
|
|
||||||
this.overlays = new OverlayAPI.default();
|
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();
|
this.router = new ApplicationRouter();
|
||||||
|
|
||||||
@ -259,6 +267,8 @@ define([
|
|||||||
this.install(LegacyIndicatorsPlugin());
|
this.install(LegacyIndicatorsPlugin());
|
||||||
this.install(LicensesPlugin.default());
|
this.install(LicensesPlugin.default());
|
||||||
this.install(RemoveActionPlugin.default());
|
this.install(RemoveActionPlugin.default());
|
||||||
|
this.install(MoveActionPlugin.default());
|
||||||
|
this.install(DuplicateActionPlugin.default());
|
||||||
this.install(this.plugins.FolderView());
|
this.install(this.plugins.FolderView());
|
||||||
this.install(this.plugins.Tabs());
|
this.install(this.plugins.Tabs());
|
||||||
this.install(ImageryPlugin.default());
|
this.install(ImageryPlugin.default());
|
||||||
@ -271,6 +281,8 @@ define([
|
|||||||
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
||||||
this.install(this.plugins.NotificationIndicator());
|
this.install(this.plugins.NotificationIndicator());
|
||||||
this.install(this.plugins.NewFolderAction());
|
this.install(this.plugins.NewFolderAction());
|
||||||
|
this.install(this.plugins.ViewDatumAction());
|
||||||
|
this.install(this.plugins.ObjectInterceptors());
|
||||||
}
|
}
|
||||||
|
|
||||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||||
|
@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
|
|||||||
|
|
||||||
legacyActions.filter(contextualCategoryOnly)
|
legacyActions.filter(contextualCategoryOnly)
|
||||||
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
|
.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.description = LegacyAction.definition.description;
|
||||||
this.cssClass = LegacyAction.definition.cssClass;
|
this.cssClass = LegacyAction.definition.cssClass;
|
||||||
this.LegacyAction = LegacyAction;
|
this.LegacyAction = LegacyAction;
|
||||||
|
this.group = LegacyAction.definition.group;
|
||||||
|
this.priority = LegacyAction.definition.priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoke(objectPath) {
|
invoke(objectPath) {
|
||||||
|
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;
|
225
src/api/actions/ActionCollectionSpec.js
Normal file
225
src/api/actions/ActionCollectionSpec.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import ActionCollection from './ActionCollection';
|
||||||
|
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||||
|
|
||||||
|
describe('The ActionCollection', () => {
|
||||||
|
let openmct;
|
||||||
|
let actionCollection;
|
||||||
|
let mockApplicableActions;
|
||||||
|
let mockObjectPath;
|
||||||
|
let mockView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
mockObjectPath = [
|
||||||
|
{
|
||||||
|
name: 'mock folder',
|
||||||
|
type: 'fake-folder',
|
||||||
|
identifier: {
|
||||||
|
key: 'mock-folder',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mock parent folder',
|
||||||
|
type: 'fake-folder',
|
||||||
|
identifier: {
|
||||||
|
key: 'mock-parent-folder',
|
||||||
|
namespace: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
mockView = {
|
||||||
|
getViewContext: () => {
|
||||||
|
return {
|
||||||
|
onlyAppliesToTestCase: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockApplicableActions = {
|
||||||
|
'test-action-object-path': {
|
||||||
|
name: 'Test Action Object Path',
|
||||||
|
key: 'test-action-object-path',
|
||||||
|
cssClass: 'test-action-object-path',
|
||||||
|
description: 'This is a test action for object path',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
appliesTo: (objectPath) => {
|
||||||
|
if (objectPath.length) {
|
||||||
|
return objectPath[0].type === 'fake-folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
invoke: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'test-action-view': {
|
||||||
|
name: 'Test Action View',
|
||||||
|
key: 'test-action-view',
|
||||||
|
cssClass: 'test-action-view',
|
||||||
|
description: 'This is a test action for view',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
showInStatusBar: true,
|
||||||
|
appliesTo: (objectPath, view = {}) => {
|
||||||
|
if (view.getViewContext) {
|
||||||
|
let viewContext = view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext.onlyAppliesToTestCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
invoke: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
actionCollection.destroy();
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("disable method invoked with action keys", () => {
|
||||||
|
it("marks those actions as isDisabled", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeFalsy();
|
||||||
|
|
||||||
|
actionCollection.disable([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("enable method invoked with action keys", () => {
|
||||||
|
it("marks the isDisabled property as false", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
|
||||||
|
actionCollection.disable([actionKey]);
|
||||||
|
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeTrue();
|
||||||
|
|
||||||
|
actionCollection.enable([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hide method invoked with action keys", () => {
|
||||||
|
it("marks those actions as isHidden", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeFalsy();
|
||||||
|
|
||||||
|
actionCollection.hide([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("show method invoked with action keys", () => {
|
||||||
|
it("marks the isHidden property as false", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
|
||||||
|
actionCollection.hide([actionKey]);
|
||||||
|
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeTrue();
|
||||||
|
|
||||||
|
actionCollection.show([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getVisibleActions method", () => {
|
||||||
|
it("returns an array of non hidden actions", () => {
|
||||||
|
let action1Key = 'test-action-object-path';
|
||||||
|
let action2Key = 'test-action-view';
|
||||||
|
|
||||||
|
actionCollection.hide([action1Key]);
|
||||||
|
|
||||||
|
let visibleActions = actionCollection.getVisibleActions();
|
||||||
|
|
||||||
|
expect(Array.isArray(visibleActions)).toBeTrue();
|
||||||
|
expect(visibleActions.length).toEqual(1);
|
||||||
|
expect(visibleActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.show([action1Key]);
|
||||||
|
visibleActions = actionCollection.getVisibleActions();
|
||||||
|
|
||||||
|
expect(visibleActions.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getStatusBarActions method", () => {
|
||||||
|
it("returns an array of non disabled, non hidden statusBar actions", () => {
|
||||||
|
let action2Key = 'test-action-view';
|
||||||
|
|
||||||
|
let statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(Array.isArray(statusBarActions)).toBeTrue();
|
||||||
|
expect(statusBarActions.length).toEqual(1);
|
||||||
|
expect(statusBarActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.disable([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(0);
|
||||||
|
|
||||||
|
actionCollection.enable([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(1);
|
||||||
|
expect(statusBarActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.hide([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
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',
|
'./telemetry/TelemetryAPI',
|
||||||
'./indicators/IndicatorAPI',
|
'./indicators/IndicatorAPI',
|
||||||
'./notifications/NotificationAPI',
|
'./notifications/NotificationAPI',
|
||||||
'./contextMenu/ContextMenuAPI',
|
'./Editor',
|
||||||
'./Editor'
|
'./menu/MenuAPI',
|
||||||
|
'./actions/ActionsAPI',
|
||||||
|
'./status/StatusAPI'
|
||||||
], function (
|
], function (
|
||||||
TimeAPI,
|
TimeAPI,
|
||||||
ObjectAPI,
|
ObjectAPI,
|
||||||
@ -39,8 +40,10 @@ define([
|
|||||||
TelemetryAPI,
|
TelemetryAPI,
|
||||||
IndicatorAPI,
|
IndicatorAPI,
|
||||||
NotificationAPI,
|
NotificationAPI,
|
||||||
ContextMenuAPI,
|
EditorAPI,
|
||||||
EditorAPI
|
MenuAPI,
|
||||||
|
ActionsAPI,
|
||||||
|
StatusAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI,
|
TimeAPI: TimeAPI,
|
||||||
@ -51,6 +54,8 @@ define([
|
|||||||
IndicatorAPI: IndicatorAPI,
|
IndicatorAPI: IndicatorAPI,
|
||||||
NotificationAPI: NotificationAPI.default,
|
NotificationAPI: NotificationAPI.default,
|
||||||
EditorAPI: EditorAPI,
|
EditorAPI: EditorAPI,
|
||||||
ContextMenuRegistry: ContextMenuAPI.default
|
MenuAPI: MenuAPI.default,
|
||||||
|
ActionsAPI: ActionsAPI.default,
|
||||||
|
StatusAPI: StatusAPI.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="c-menu">
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="action in actions"
|
|
||||||
:key="action.name"
|
|
||||||
:class="action.cssClass"
|
|
||||||
:title="action.description"
|
|
||||||
@click="action.invoke(objectPath)"
|
|
||||||
>
|
|
||||||
{{ action.name }}
|
|
||||||
</li>
|
|
||||||
<li v-if="actions.length === 0">
|
|
||||||
No actions defined.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
inject: ['actions', 'objectPath']
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,159 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
import ContextMenuComponent from './ContextMenu.vue';
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
|
|
||||||
* custom HTML elements.
|
|
||||||
* @interface ContextMenuAPI
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
|
||||||
class ContextMenuAPI {
|
|
||||||
constructor() {
|
|
||||||
this._allActions = [];
|
|
||||||
this._activeContextMenu = undefined;
|
|
||||||
|
|
||||||
this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
|
|
||||||
this.registerAction = this.registerAction.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
|
|
||||||
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
|
|
||||||
*
|
|
||||||
* @interface ContextMenuAction
|
|
||||||
* @memberof module:openmct
|
|
||||||
* @property {string} name the human-readable name of this view
|
|
||||||
* @property {string} description a longer-form description (typically
|
|
||||||
* a single sentence or short paragraph) of this kind of view
|
|
||||||
* @property {string} cssClass the CSS class to apply to labels for this
|
|
||||||
* view (to add icons, for instance)
|
|
||||||
* @property {string} key unique key to identify the context menu action
|
|
||||||
* (used in custom context menu eg table rows, to identify which actions to include)
|
|
||||||
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @method appliesTo
|
|
||||||
* @memberof module:openmct.ContextMenuAction#
|
|
||||||
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
|
|
||||||
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Code to be executed when the action is selected from a context menu
|
|
||||||
* @method invoke
|
|
||||||
* @memberof module:openmct.ContextMenuAction#
|
|
||||||
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @param {ContextMenuAction} actionDefinition
|
|
||||||
*/
|
|
||||||
registerAction(actionDefinition) {
|
|
||||||
this._allActions.push(actionDefinition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
|
|
||||||
|
|
||||||
let applicableActions = this._allActions.filter((action) => {
|
|
||||||
|
|
||||||
if (actionsToBeIncluded) {
|
|
||||||
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
|
|
||||||
} else {
|
|
||||||
if (action.appliesTo === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this._activeContextMenu) {
|
|
||||||
this._hideActiveContextMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
|
|
||||||
this._activeContextMenu.$mount();
|
|
||||||
document.body.appendChild(this._activeContextMenu.$el);
|
|
||||||
|
|
||||||
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
|
|
||||||
this._activeContextMenu.$el.style.left = `${position.x}px`;
|
|
||||||
this._activeContextMenu.$el.style.top = `${position.y}px`;
|
|
||||||
|
|
||||||
document.addEventListener('click', this._hideActiveContextMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
|
|
||||||
let menuDimensions = menuElement.getBoundingClientRect();
|
|
||||||
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
|
||||||
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
|
||||||
|
|
||||||
if (overflowX > 0) {
|
|
||||||
eventPosX = eventPosX - overflowX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overflowY > 0) {
|
|
||||||
eventPosY = eventPosY - overflowY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: eventPosX,
|
|
||||||
y: eventPosY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_hideActiveContextMenu() {
|
|
||||||
document.removeEventListener('click', this._hideActiveContextMenu);
|
|
||||||
document.body.removeChild(this._activeContextMenu.$el);
|
|
||||||
this._activeContextMenu.$destroy();
|
|
||||||
this._activeContextMenu = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_createContextMenuForObject(objectPath, actions) {
|
|
||||||
return new Vue({
|
|
||||||
components: {
|
|
||||||
ContextMenu: ContextMenuComponent
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
actions: actions,
|
|
||||||
objectPath: objectPath
|
|
||||||
},
|
|
||||||
template: '<ContextMenu></ContextMenu>'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default ContextMenuAPI;
|
|
67
src/api/menu/MenuAPI.js
Normal file
67
src/api/menu/MenuAPI.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import Menu from './menu.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
|
||||||
|
* custom HTML elements.
|
||||||
|
* @interface MenuAPI
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MenuAPI {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.showMenu = this.showMenu.bind(this);
|
||||||
|
this._clearMenuComponent = this._clearMenuComponent.bind(this);
|
||||||
|
this._showObjectMenu = this._showObjectMenu.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenu(x, y, actions) {
|
||||||
|
if (this.menuComponent) {
|
||||||
|
this.menuComponent.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
actions
|
||||||
|
};
|
||||||
|
|
||||||
|
this.menuComponent = new Menu(options);
|
||||||
|
this.menuComponent.once('destroy', this._clearMenuComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearMenuComponent() {
|
||||||
|
this.menuComponent = undefined;
|
||||||
|
delete this.menuComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
|
||||||
|
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
|
||||||
|
|
||||||
|
this.showMenu(x, y, applicableActions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default MenuAPI;
|
125
src/api/menu/MenuAPISpec.js
Normal file
125
src/api/menu/MenuAPISpec.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import MenuAPI from './MenuAPI';
|
||||||
|
import Menu from './menu';
|
||||||
|
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||||
|
|
||||||
|
describe ('The Menu API', () => {
|
||||||
|
let openmct;
|
||||||
|
let menuAPI;
|
||||||
|
let actionsArray;
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
menuAPI = new MenuAPI(openmct);
|
||||||
|
actionsArray = [
|
||||||
|
{
|
||||||
|
name: 'Test Action 1',
|
||||||
|
cssClass: 'test-css-class-1',
|
||||||
|
description: 'This is a test action',
|
||||||
|
callBack: () => {
|
||||||
|
result = 'Test Action 1 Invoked';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Test Action 2',
|
||||||
|
cssClass: 'test-css-class-2',
|
||||||
|
description: 'This is a test action',
|
||||||
|
callBack: () => {
|
||||||
|
result = 'Test Action 2 Invoked';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
x = 8;
|
||||||
|
y = 16;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showMenu method", () => {
|
||||||
|
it("creates an instance of Menu when invoked", () => {
|
||||||
|
menuAPI.showMenu(x, y, actionsArray);
|
||||||
|
|
||||||
|
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("creates a menu component", () => {
|
||||||
|
let menuComponent;
|
||||||
|
let vueComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
menuAPI.showMenu(x, y, actionsArray);
|
||||||
|
vueComponent = menuAPI.menuComponent.component;
|
||||||
|
menuComponent = document.querySelector(".c-menu");
|
||||||
|
|
||||||
|
spyOn(vueComponent, '$destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a menu component in the expected x and y coordinates", () => {
|
||||||
|
let boundingClientRect = menuComponent.getBoundingClientRect();
|
||||||
|
let left = boundingClientRect.left;
|
||||||
|
let top = boundingClientRect.top;
|
||||||
|
|
||||||
|
expect(left).toEqual(x);
|
||||||
|
expect(top).toEqual(y);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with all the actions passed in", () => {
|
||||||
|
expect(menuComponent).toBeDefined();
|
||||||
|
|
||||||
|
let listItems = menuComponent.children[0].children;
|
||||||
|
|
||||||
|
expect(listItems.length).toEqual(actionsArray.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with click-able menu items, that will invoke the correct callBacks", () => {
|
||||||
|
let listItem1 = menuComponent.children[0].children[0];
|
||||||
|
|
||||||
|
listItem1.click();
|
||||||
|
|
||||||
|
expect(result).toEqual("Test Action 1 Invoked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dismisses the menu when action is clicked on", () => {
|
||||||
|
let listItem1 = menuComponent.children[0].children[0];
|
||||||
|
|
||||||
|
listItem1.click();
|
||||||
|
|
||||||
|
let menu = document.querySelector('.c-menu');
|
||||||
|
|
||||||
|
expect(menu).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes the destroy method when menu is dismissed", () => {
|
||||||
|
document.body.click();
|
||||||
|
|
||||||
|
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
52
src/api/menu/components/Menu.vue
Normal file
52
src/api/menu/components/Menu.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="c-menu">
|
||||||
|
<ul v-if="actions.length && actions[0].length">
|
||||||
|
<template
|
||||||
|
v-for="(actionGroups, index) in actions"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="action in actionGroups"
|
||||||
|
:key="action.name"
|
||||||
|
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||||
|
:title="action.description"
|
||||||
|
@click="action.callBack"
|
||||||
|
>
|
||||||
|
{{ action.name }}
|
||||||
|
</li>
|
||||||
|
<div
|
||||||
|
v-if="index !== actions.length - 1"
|
||||||
|
:key="index"
|
||||||
|
class="c-menu__section-separator"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<li
|
||||||
|
v-if="actionGroups.length === 0"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
No actions defined.
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul v-else>
|
||||||
|
<li
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.name"
|
||||||
|
:class="action.cssClass"
|
||||||
|
:title="action.description"
|
||||||
|
@click="action.callBack"
|
||||||
|
>
|
||||||
|
{{ action.name }}
|
||||||
|
</li>
|
||||||
|
<li v-if="actions.length === 0">
|
||||||
|
No actions defined.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['actions']
|
||||||
|
};
|
||||||
|
</script>
|
94
src/api/menu/menu.js
Normal file
94
src/api/menu/menu.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import EventEmitter from 'EventEmitter';
|
||||||
|
import MenuComponent from './components/Menu.vue';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
class Menu extends EventEmitter {
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
this.component = new Vue({
|
||||||
|
provide: {
|
||||||
|
actions: options.actions
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MenuComponent
|
||||||
|
},
|
||||||
|
template: '<menu-component />'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.onDestroy) {
|
||||||
|
this.once('destroy', options.onDestroy);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dismiss = this.dismiss.bind(this);
|
||||||
|
this.show = this.show.bind(this);
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.emit('destroy');
|
||||||
|
document.body.removeChild(this.component.$el);
|
||||||
|
document.removeEventListener('click', this.dismiss);
|
||||||
|
this.component.$destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.component.$mount();
|
||||||
|
document.body.appendChild(this.component.$el);
|
||||||
|
|
||||||
|
let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el);
|
||||||
|
|
||||||
|
this.component.$el.style.left = `${position.x}px`;
|
||||||
|
this.component.$el.style.top = `${position.y}px`;
|
||||||
|
|
||||||
|
document.addEventListener('click', this.dismiss);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
|
||||||
|
let menuDimensions = menuElement.getBoundingClientRect();
|
||||||
|
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
||||||
|
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
||||||
|
|
||||||
|
if (overflowX > 0) {
|
||||||
|
eventPosX = eventPosX - overflowX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overflowY > 0) {
|
||||||
|
eventPosY = eventPosY - overflowY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: eventPosX,
|
||||||
|
y: eventPosY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Menu;
|
66
src/api/objects/InterceptorRegistry.js
Normal file
66
src/api/objects/InterceptorRegistry.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
export default class InterceptorRegistry {
|
||||||
|
/**
|
||||||
|
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
|
||||||
|
* @interface InterceptorRegistry
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.interceptors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface InterceptorDef
|
||||||
|
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
|
||||||
|
* @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
|
||||||
|
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
|
||||||
|
* @memberof module:openmct InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new object interceptor.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
|
||||||
|
* @method addInterceptor
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
addInterceptor(interceptorDef) {
|
||||||
|
//TODO: sort by priority
|
||||||
|
this.interceptors.push(interceptorDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all interceptors applicable to a domain object.
|
||||||
|
* @method getInterceptors
|
||||||
|
* @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
getInterceptors(identifier, object) {
|
||||||
|
return this.interceptors.filter(interceptor => {
|
||||||
|
return typeof interceptor.appliesTo === 'function'
|
||||||
|
&& interceptor.appliesTo(identifier, object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
@ -26,6 +26,7 @@ define([
|
|||||||
'./MutableObject',
|
'./MutableObject',
|
||||||
'./RootRegistry',
|
'./RootRegistry',
|
||||||
'./RootObjectProvider',
|
'./RootObjectProvider',
|
||||||
|
'./InterceptorRegistry',
|
||||||
'EventEmitter'
|
'EventEmitter'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
@ -33,6 +34,7 @@ define([
|
|||||||
MutableObject,
|
MutableObject,
|
||||||
RootRegistry,
|
RootRegistry,
|
||||||
RootObjectProvider,
|
RootObjectProvider,
|
||||||
|
InterceptorRegistry,
|
||||||
EventEmitter
|
EventEmitter
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -48,6 +50,7 @@ define([
|
|||||||
this.rootRegistry = new RootRegistry();
|
this.rootRegistry = new RootRegistry();
|
||||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
|
this.interceptorRegistry = new InterceptorRegistry.default();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,6 +180,10 @@ define([
|
|||||||
|
|
||||||
return objectPromise.then(result => {
|
return objectPromise.then(result => {
|
||||||
delete this.cache[keystring];
|
delete this.cache[keystring];
|
||||||
|
const interceptors = this.listGetInterceptors(identifier, result);
|
||||||
|
interceptors.forEach(interceptor => {
|
||||||
|
result = interceptor.invoke(identifier, result);
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@ -312,6 +319,27 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
|
||||||
|
* The domain object will be transformed after it is retrieved from the persistence store
|
||||||
|
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
|
||||||
|
*
|
||||||
|
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
|
||||||
|
* @method addGetInterceptor
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
|
||||||
|
this.interceptorRegistry.addInterceptor(interceptorDef);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the interceptors for a given domain object.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||||
|
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uniquely identifies a domain object.
|
* Uniquely identifies a domain object.
|
||||||
*
|
*
|
||||||
|
@ -63,12 +63,51 @@ describe("The Object API", () => {
|
|||||||
describe("The get function", () => {
|
describe("The get function", () => {
|
||||||
describe("when a provider is available", () => {
|
describe("when a provider is available", () => {
|
||||||
let mockProvider;
|
let mockProvider;
|
||||||
|
let mockInterceptor;
|
||||||
|
let anotherMockInterceptor;
|
||||||
|
let notApplicableMockInterceptor;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||||
"get"
|
"get"
|
||||||
]);
|
]);
|
||||||
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
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.addProvider(TEST_NAMESPACE, mockProvider);
|
||||||
|
objectAPI.addGetInterceptor(mockInterceptor);
|
||||||
|
objectAPI.addGetInterceptor(anotherMockInterceptor);
|
||||||
|
objectAPI.addGetInterceptor(notApplicableMockInterceptor);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Caches multiple requests for the same object", () => {
|
it("Caches multiple requests for the same object", () => {
|
||||||
@ -78,6 +117,15 @@ describe("The Object API", () => {
|
|||||||
objectAPI.get(mockDomainObject.identifier);
|
objectAPI.get(mockDomainObject.identifier);
|
||||||
expect(mockProvider.get.calls.count()).toBe(1);
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -22,6 +22,7 @@ class OverlayAPI {
|
|||||||
this.dismissLastOverlay();
|
this.dismissLastOverlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,6 +128,7 @@ class OverlayAPI {
|
|||||||
|
|
||||||
return progressDialog;
|
return progressDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OverlayAPI;
|
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([
|
define([
|
||||||
|
'../../plugins/displayLayout/CustomStringFormatter',
|
||||||
'./TelemetryMetadataManager',
|
'./TelemetryMetadataManager',
|
||||||
'./TelemetryValueFormatter',
|
'./TelemetryValueFormatter',
|
||||||
'./DefaultMetadataProvider',
|
'./DefaultMetadataProvider',
|
||||||
'objectUtils',
|
'objectUtils',
|
||||||
'lodash'
|
'lodash'
|
||||||
], function (
|
], function (
|
||||||
|
CustomStringFormatter,
|
||||||
TelemetryMetadataManager,
|
TelemetryMetadataManager,
|
||||||
TelemetryValueFormatter,
|
TelemetryValueFormatter,
|
||||||
DefaultMetadataProvider,
|
DefaultMetadataProvider,
|
||||||
@ -142,6 +144,17 @@ define([
|
|||||||
this.valueFormatterCache = new WeakMap();
|
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
|
* Return true if the given domainObject is a telemetry object. A telemetry
|
||||||
* object is any object which has telemetry metadata-- regardless of whether
|
* object is any object which has telemetry metadata-- regardless of whether
|
||||||
@ -400,6 +413,17 @@ define([
|
|||||||
return _.sortBy(options, sortKeys);
|
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.
|
* Get a value formatter for a given valueMetadata.
|
||||||
*
|
*
|
||||||
@ -407,19 +431,27 @@ define([
|
|||||||
*/
|
*/
|
||||||
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
|
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
|
||||||
if (!this.valueFormatterCache.has(valueMetadata)) {
|
if (!this.valueFormatterCache.has(valueMetadata)) {
|
||||||
if (!this.formatService) {
|
|
||||||
this.formatService = this.openmct.$injector.get('formatService');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.valueFormatterCache.set(
|
this.valueFormatterCache.set(
|
||||||
valueMetadata,
|
valueMetadata,
|
||||||
new TelemetryValueFormatter(valueMetadata, this.formatService)
|
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.valueFormatterCache.get(valueMetadata);
|
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
|
* Get a format map of all value formatters for a given piece of telemetry
|
||||||
* metadata.
|
* metadata.
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
const CONTEXT_MENU_ACTIONS = [
|
const CONTEXT_MENU_ACTIONS = [
|
||||||
|
'viewDatumAction',
|
||||||
'viewHistoricalData',
|
'viewHistoricalData',
|
||||||
'remove'
|
'remove'
|
||||||
];
|
];
|
||||||
@ -129,6 +130,7 @@ export default {
|
|||||||
let limit;
|
let limit;
|
||||||
|
|
||||||
if (this.shouldUpdate(newTimestamp)) {
|
if (this.shouldUpdate(newTimestamp)) {
|
||||||
|
this.datum = datum;
|
||||||
this.timestamp = newTimestamp;
|
this.timestamp = newTimestamp;
|
||||||
this.value = this.formats[this.valueKey].format(datum);
|
this.value = this.formats[this.valueKey].format(datum);
|
||||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||||
@ -175,8 +177,25 @@ export default {
|
|||||||
this.resetValues();
|
this.resetValues();
|
||||||
this.timestampKey = timeSystem.key;
|
this.timestampKey = timeSystem.key;
|
||||||
},
|
},
|
||||||
|
getView() {
|
||||||
|
return {
|
||||||
|
getViewContext: () => {
|
||||||
|
return {
|
||||||
|
viewHistoricalData: true,
|
||||||
|
viewDatumAction: true,
|
||||||
|
getDatum: () => {
|
||||||
|
return this.datum;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
showContextMenu(event) {
|
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() {
|
resetValues() {
|
||||||
this.value = '---';
|
this.value = '---';
|
||||||
|
@ -19,23 +19,25 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
|
||||||
|
import AutoflowTabularConstants from './AutoflowTabularConstants';
|
||||||
|
import $ from 'zepto';
|
||||||
|
import DOMObserver from './dom-observer';
|
||||||
|
import {
|
||||||
|
createOpenMct,
|
||||||
|
resetApplicationState,
|
||||||
|
spyOnBuiltins
|
||||||
|
} from 'utils/testing';
|
||||||
|
|
||||||
define([
|
describe("AutoflowTabularPlugin", () => {
|
||||||
'./AutoflowTabularPlugin',
|
|
||||||
'./AutoflowTabularConstants',
|
|
||||||
'../../MCT',
|
|
||||||
'zepto',
|
|
||||||
'./dom-observer'
|
|
||||||
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) {
|
|
||||||
describe("AutoflowTabularPlugin", function () {
|
|
||||||
let testType;
|
let testType;
|
||||||
let testObject;
|
let testObject;
|
||||||
let mockmct;
|
let mockmct;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
testType = "some-type";
|
testType = "some-type";
|
||||||
testObject = { type: testType };
|
testObject = { type: testType };
|
||||||
mockmct = new MCT();
|
mockmct = createOpenMct();
|
||||||
spyOn(mockmct.composition, 'get');
|
spyOn(mockmct.composition, 'get');
|
||||||
spyOn(mockmct.objectViews, 'addProvider');
|
spyOn(mockmct.objectViews, 'addProvider');
|
||||||
spyOn(mockmct.telemetry, 'getMetadata');
|
spyOn(mockmct.telemetry, 'getMetadata');
|
||||||
@ -48,27 +50,31 @@ define([
|
|||||||
plugin(mockmct);
|
plugin(mockmct);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("installs a view provider", function () {
|
afterEach(() => {
|
||||||
|
resetApplicationState(mockmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs a view provider", () => {
|
||||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("installs a view provider which", function () {
|
describe("installs a view provider which", () => {
|
||||||
let provider;
|
let provider;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
provider =
|
provider =
|
||||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies its view to the type from options", function () {
|
it("applies its view to the type from options", () => {
|
||||||
expect(provider.canView(testObject)).toBe(true);
|
expect(provider.canView(testObject)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not apply to other types", function () {
|
it("does not apply to other types", () => {
|
||||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("provides a view which", function () {
|
describe("provides a view which", () => {
|
||||||
let testKeys;
|
let testKeys;
|
||||||
let testChildren;
|
let testChildren;
|
||||||
let testContainer;
|
let testContainer;
|
||||||
@ -88,19 +94,24 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitEvent(mockEmitter, type, event) {
|
function emitEvent(mockEmitter, type, event) {
|
||||||
mockEmitter.on.calls.all().forEach(function (call) {
|
mockEmitter.on.calls.all().forEach((call) => {
|
||||||
if (call.args[0] === type) {
|
if (call.args[0] === type) {
|
||||||
call.args[1](event);
|
call.args[1](event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach((done) => {
|
||||||
callbacks = {};
|
callbacks = {};
|
||||||
|
|
||||||
|
spyOnBuiltins(['requestAnimationFrame']);
|
||||||
|
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||||
|
callBack();
|
||||||
|
});
|
||||||
|
|
||||||
testObject = { type: 'some-type' };
|
testObject = { type: 'some-type' };
|
||||||
testKeys = ['abc', 'def', 'xyz'];
|
testKeys = ['abc', 'def', 'xyz'];
|
||||||
testChildren = testKeys.map(function (key) {
|
testChildren = testKeys.map((key) => {
|
||||||
return {
|
return {
|
||||||
identifier: {
|
identifier: {
|
||||||
namespace: "test",
|
namespace: "test",
|
||||||
@ -112,7 +123,7 @@ define([
|
|||||||
testContainer = $('<div>')[0];
|
testContainer = $('<div>')[0];
|
||||||
domObserver = new DOMObserver(testContainer);
|
domObserver = new DOMObserver(testContainer);
|
||||||
|
|
||||||
testHistories = testKeys.reduce(function (histories, key, index) {
|
testHistories = testKeys.reduce((histories, key, index) => {
|
||||||
histories[key] = {
|
histories[key] = {
|
||||||
key: key,
|
key: key,
|
||||||
range: index + 10,
|
range: index + 10,
|
||||||
@ -128,84 +139,84 @@ define([
|
|||||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||||
|
|
||||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||||
mockUnsubscribes = testKeys.reduce(function (map, key) {
|
mockUnsubscribes = testKeys.reduce((map, key) => {
|
||||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
mockmct.composition.get.and.returnValue(mockComposition);
|
mockmct.composition.get.and.returnValue(mockComposition);
|
||||||
mockComposition.load.and.callFake(function () {
|
mockComposition.load.and.callFake(() => {
|
||||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||||
|
|
||||||
return Promise.resolve(testChildren);
|
return Promise.resolve(testChildren);
|
||||||
});
|
});
|
||||||
|
|
||||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||||
mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) {
|
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||||
mockFormatter.format.and.callFake(function (datum) {
|
mockFormatter.format.and.callFake((datum) => {
|
||||||
return datum[metadatum.hint];
|
return datum[metadatum.hint];
|
||||||
});
|
});
|
||||||
|
|
||||||
return mockFormatter;
|
return mockFormatter;
|
||||||
});
|
});
|
||||||
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||||
mockmct.telemetry.subscribe.and.callFake(function (obj, callback) {
|
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
|
||||||
const key = obj.identifier.key;
|
const key = obj.identifier.key;
|
||||||
callbacks[key] = callback;
|
callbacks[key] = callback;
|
||||||
|
|
||||||
return mockUnsubscribes[key];
|
return mockUnsubscribes[key];
|
||||||
});
|
});
|
||||||
mockmct.telemetry.request.and.callFake(function (obj, request) {
|
mockmct.telemetry.request.and.callFake((obj, request) => {
|
||||||
const key = obj.identifier.key;
|
const key = obj.identifier.key;
|
||||||
|
|
||||||
return Promise.resolve([testHistories[key]]);
|
return Promise.resolve([testHistories[key]]);
|
||||||
});
|
});
|
||||||
mockMetadata.valuesForHints.and.callFake(function (hints) {
|
mockMetadata.valuesForHints.and.callFake((hints) => {
|
||||||
return [{ hint: hints[0] }];
|
return [{ hint: hints[0] }];
|
||||||
});
|
});
|
||||||
|
|
||||||
view = provider.view(testObject);
|
view = provider.view(testObject);
|
||||||
view.show(testContainer);
|
view.show(testContainer);
|
||||||
|
|
||||||
return waitsForChange();
|
return done();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(() => {
|
||||||
domObserver.destroy();
|
domObserver.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("populates its container", function () {
|
it("populates its container", () => {
|
||||||
expect(testContainer.children.length > 0).toBe(true);
|
expect(testContainer.children.length > 0).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when rows have been populated", function () {
|
describe("when rows have been populated", () => {
|
||||||
function rowsMatch() {
|
function rowsMatch() {
|
||||||
const rows = $(testContainer).find(".l-autoflow-row").length;
|
const rows = $(testContainer).find(".l-autoflow-row").length;
|
||||||
|
|
||||||
return rows === testChildren.length;
|
return rows === testChildren.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("shows one row per child object", function () {
|
it("shows one row per child object", () => {
|
||||||
return domObserver.when(rowsMatch);
|
return domObserver.when(rowsMatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds rows on composition change", function () {
|
// it("adds rows on composition change", () => {
|
||||||
const child = {
|
// const child = {
|
||||||
identifier: {
|
// identifier: {
|
||||||
namespace: "test",
|
// namespace: "test",
|
||||||
key: "123"
|
// key: "123"
|
||||||
},
|
// },
|
||||||
name: "Object 123"
|
// name: "Object 123"
|
||||||
};
|
// };
|
||||||
testChildren.push(child);
|
// testChildren.push(child);
|
||||||
emitEvent(mockComposition, 'add', child);
|
// emitEvent(mockComposition, 'add', child);
|
||||||
|
|
||||||
return domObserver.when(rowsMatch);
|
// return domObserver.when(rowsMatch);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it("removes rows on composition change", function () {
|
it("removes rows on composition change", () => {
|
||||||
const child = testChildren.pop();
|
const child = testChildren.pop();
|
||||||
emitEvent(mockComposition, 'remove', child.identifier);
|
emitEvent(mockComposition, 'remove', child.identifier);
|
||||||
|
|
||||||
@ -213,17 +224,17 @@ define([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes subscriptions when destroyed", function () {
|
it("removes subscriptions when destroyed", () => {
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((key) => {
|
||||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
view.destroy();
|
view.destroy();
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((key) => {
|
||||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("provides a button to change column width", function () {
|
it("provides a button to change column width", () => {
|
||||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
||||||
const nextWidth =
|
const nextWidth =
|
||||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
||||||
@ -240,25 +251,25 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
return domObserver.when(widthHasChanged)
|
return domObserver.when(widthHasChanged)
|
||||||
.then(function () {
|
.then(() => {
|
||||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||||
.toEqual(nextWidth + 'px');
|
.toEqual(nextWidth + 'px');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("subscribes to all child objects", function () {
|
it("subscribes to all child objects", () => {
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((key) => {
|
||||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays historical telemetry", function () {
|
it("displays historical telemetry", () => {
|
||||||
function rowTextDefined() {
|
function rowTextDefined() {
|
||||||
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return domObserver.when(rowTextDefined).then(function () {
|
return domObserver.when(rowTextDefined).then(() => {
|
||||||
testKeys.forEach(function (key, index) {
|
testKeys.forEach((key, index) => {
|
||||||
const datum = testHistories[key];
|
const datum = testHistories[key];
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
expect($cell.text()).toEqual(String(datum.range));
|
expect($cell.text()).toEqual(String(datum.range));
|
||||||
@ -266,8 +277,8 @@ define([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays incoming telemetry", function () {
|
it("displays incoming telemetry", () => {
|
||||||
const testData = testKeys.map(function (key, index) {
|
const testData = testKeys.map((key, index) => {
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
range: index * 100,
|
range: index * 100,
|
||||||
@ -275,37 +286,37 @@ define([
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
testData.forEach(function (datum) {
|
testData.forEach((datum) => {
|
||||||
callbacks[datum.key](datum);
|
callbacks[datum.key](datum);
|
||||||
});
|
});
|
||||||
|
|
||||||
return waitsForChange().then(function () {
|
return waitsForChange().then(() => {
|
||||||
testData.forEach(function (datum, index) {
|
testData.forEach((datum, index) => {
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
expect($cell.text()).toEqual(String(datum.range));
|
expect($cell.text()).toEqual(String(datum.range));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates classes for limit violations", function () {
|
it("updates classes for limit violations", () => {
|
||||||
const testClass = "some-limit-violation";
|
const testClass = "some-limit-violation";
|
||||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((key) => {
|
||||||
callbacks[key]({
|
callbacks[key]({
|
||||||
range: 'foo',
|
range: 'foo',
|
||||||
domain: 'bar'
|
domain: 'bar'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return waitsForChange().then(function () {
|
return waitsForChange().then(() => {
|
||||||
testKeys.forEach(function (datum, index) {
|
testKeys.forEach((datum, index) => {
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
expect($cell.hasClass(testClass)).toBe(true);
|
expect($cell.hasClass(testClass)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("automatically flows to new columns", function () {
|
it("automatically flows to new columns", () => {
|
||||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||||
const count = testKeys.length;
|
const count = testKeys.length;
|
||||||
@ -343,12 +354,12 @@ define([
|
|||||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
return promiseChain.then(function () {
|
return promiseChain.then(() => {
|
||||||
$container.remove();
|
$container.remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads composition exactly once", function () {
|
it("loads composition exactly once", () => {
|
||||||
const testObj = testChildren.pop();
|
const testObj = testChildren.pop();
|
||||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||||
testChildren.push(testObj);
|
testChildren.push(testObj);
|
||||||
@ -358,4 +369,3 @@ define([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
export default class ClearDataAction {
|
export default class ClearDataAction {
|
||||||
constructor(openmct, appliesToObjects) {
|
constructor(openmct, appliesToObjects) {
|
||||||
this.name = 'Clear Data for Object';
|
this.name = 'Clear Data for Object';
|
||||||
|
this.key = 'clear-data-action';
|
||||||
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
||||||
this.cssClass = 'icon-clear-data';
|
this.cssClass = 'icon-clear-data';
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ define([
|
|||||||
openmct.indicators.add(indicator);
|
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 () {
|
describe('When the Clear Data Plugin is installed,', function () {
|
||||||
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
||||||
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
||||||
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
|
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
||||||
|
|
||||||
const openmct = {
|
const openmct = {
|
||||||
objectViews: mockObjectViews,
|
objectViews: mockObjectViews,
|
||||||
indicators: mockIndicatorProvider,
|
indicators: mockIndicatorProvider,
|
||||||
contextMenu: mockContextMenuProvider,
|
actions: mockActionsProvider,
|
||||||
install: function (plugin) {
|
install: function (plugin) {
|
||||||
plugin(this);
|
plugin(this);
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
|
|||||||
it('Clear Data context menu action is installed', function () {
|
it('Clear Data context menu action is installed', function () {
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
openmct.install(ClearDataActionPlugin([]));
|
||||||
|
|
||||||
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
|
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clear data action emits a clearData event when invoked', function () {
|
it('clear data action emits a clearData event when invoked', function () {
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
.c-cs {
|
.c-cs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -64,9 +64,16 @@ define([
|
|||||||
components: {
|
components: {
|
||||||
AlphanumericFormatView: AlphanumericFormatView.default
|
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 () {
|
destroy: function () {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
component = undefined;
|
component = undefined;
|
||||||
|
38
src/plugins/displayLayout/CustomStringFormatter.js
Normal file
38
src/plugins/displayLayout/CustomStringFormatter.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import printj from 'printj';
|
||||||
|
|
||||||
|
export default class CustomStringFormatter {
|
||||||
|
constructor(openmct, valueMetadata, itemFormat) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.itemFormat = itemFormat;
|
||||||
|
this.valueMetadata = valueMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
format(datum) {
|
||||||
|
if (!this.itemFormat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.itemFormat.startsWith('&')) {
|
||||||
|
return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = this.itemFormat.slice(1);
|
||||||
|
const customFormatter = this.openmct.telemetry.getFormatter(key);
|
||||||
|
if (!customFormatter) {
|
||||||
|
throw new Error('Custom Formatter not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return customFormatter.format(datum[this.valueMetadata.key]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return datum[this.valueMetadata.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormat(itemFormat) {
|
||||||
|
this.itemFormat = itemFormat;
|
||||||
|
}
|
||||||
|
}
|
82
src/plugins/displayLayout/CustomStringFormatterSpec.js
Normal file
82
src/plugins/displayLayout/CustomStringFormatterSpec.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import CustomStringFormatter from './CustomStringFormatter';
|
||||||
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
|
const CUSTOM_FORMATS = [
|
||||||
|
{
|
||||||
|
key: 'sclk',
|
||||||
|
format: (value) => 2 * value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lts',
|
||||||
|
format: (value) => 3 * value
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const valueMetadata = {
|
||||||
|
key: "sin",
|
||||||
|
name: "Sine",
|
||||||
|
unit: "Hz",
|
||||||
|
formatString: "%0.2f",
|
||||||
|
hints: {
|
||||||
|
range: 1,
|
||||||
|
priority: 3
|
||||||
|
},
|
||||||
|
source: "sin"
|
||||||
|
};
|
||||||
|
|
||||||
|
const datum = {
|
||||||
|
name: "1 Sine Wave Generator",
|
||||||
|
utc: 1603930354000,
|
||||||
|
yesterday: 1603843954000,
|
||||||
|
sin: 0.587785209686822,
|
||||||
|
cos: -0.8090170253297632
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CustomStringFormatter', function () {
|
||||||
|
let element;
|
||||||
|
let child;
|
||||||
|
let openmct;
|
||||||
|
let customStringFormatter;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
child = document.createElement('div');
|
||||||
|
element.appendChild(child);
|
||||||
|
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
|
||||||
|
spyOn(openmct.telemetry, 'getFormatter');
|
||||||
|
openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key));
|
||||||
|
|
||||||
|
customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds custom format sclk', () => {
|
||||||
|
const format = openmct.telemetry.getFormatter('sclk');
|
||||||
|
expect(format.key).toEqual('sclk');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds custom format lts', () => {
|
||||||
|
const format = openmct.telemetry.getFormatter('lts');
|
||||||
|
expect(format.key).toEqual('lts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for custom format sclk', () => {
|
||||||
|
customStringFormatter.setFormat('&sclk');
|
||||||
|
const value = customStringFormatter.format(datum, valueMetadata);
|
||||||
|
expect(datum.sin * 2).toEqual(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for custom format lts', () => {
|
||||||
|
customStringFormatter.setFormat('<s');
|
||||||
|
const value = customStringFormatter.format(datum, valueMetadata);
|
||||||
|
expect(datum.sin * 3).toEqual(value);
|
||||||
|
});
|
||||||
|
});
|
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import clipboard from '@/utils/clipboard';
|
||||||
|
|
||||||
|
export default class CopyToClipboardAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.cssClass = 'icon-duplicate';
|
||||||
|
this.description = 'Copy value to clipboard';
|
||||||
|
this.group = "action";
|
||||||
|
this.key = 'copyToClipboard';
|
||||||
|
this.name = 'Copy to Clipboard';
|
||||||
|
this.priority = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke(objectPath, view = {}) {
|
||||||
|
const viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
const formattedValue = viewContext.formattedValueForCopy();
|
||||||
|
|
||||||
|
clipboard.updateClipboard(formattedValue)
|
||||||
|
.then(() => {
|
||||||
|
this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath, view = {}) {
|
||||||
|
let viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext && viewContext.formattedValueForCopy
|
||||||
|
&& typeof viewContext.formattedValueForCopy === 'function';
|
||||||
|
}
|
||||||
|
}
|
@ -142,6 +142,9 @@ export default {
|
|||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
|
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
let reference = this.$refs.objectFrame;
|
||||||
|
|
||||||
|
if (reference) {
|
||||||
let childContext = this.$refs.objectFrame.getSelectionContext();
|
let childContext = this.$refs.objectFrame.getSelectionContext();
|
||||||
childContext.item = domainObject;
|
childContext.item = domainObject;
|
||||||
childContext.layoutItem = this.item;
|
childContext.layoutItem = this.item;
|
||||||
@ -150,6 +153,7 @@ export default {
|
|||||||
this.removeSelectable = this.openmct.selection.selectable(
|
this.removeSelectable = this.openmct.selection.selectable(
|
||||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||||
delete this.immediatelySelect;
|
delete this.immediatelySelect;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,18 +30,15 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="domainObject"
|
v-if="domainObject"
|
||||||
class="u-style-receiver c-telemetry-view"
|
class="c-telemetry-view u-style-receiver"
|
||||||
:class="{
|
:class="[statusClass]"
|
||||||
styleClass,
|
|
||||||
'is-missing': domainObject.status === 'missing'
|
|
||||||
}"
|
|
||||||
:style="styleObject"
|
:style="styleObject"
|
||||||
:data-font-size="item.fontSize"
|
:data-font-size="item.fontSize"
|
||||||
:data-font="item.font"
|
:data-font="item.font"
|
||||||
@contextmenu.prevent="showContextMenu"
|
@contextmenu.prevent="showContextMenu"
|
||||||
>
|
>
|
||||||
<div class="is-missing__indicator"
|
<div class="is-status__indicator"
|
||||||
title="This item is missing"
|
:title="`This item is ${status}`"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-if="showLabel"
|
v-if="showLabel"
|
||||||
@ -74,12 +71,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LayoutFrame from './LayoutFrame.vue';
|
import LayoutFrame from './LayoutFrame.vue';
|
||||||
import printj from 'printj';
|
|
||||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||||
|
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||||
|
|
||||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||||
const DEFAULT_POSITION = [1, 1];
|
const DEFAULT_POSITION = [1, 1];
|
||||||
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
|
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
makeDefinition(openmct, gridSize, domainObject, position) {
|
makeDefinition(openmct, gridSize, domainObject, position) {
|
||||||
@ -129,13 +126,18 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
currentObjectPath: undefined,
|
||||||
datum: undefined,
|
datum: undefined,
|
||||||
formats: undefined,
|
|
||||||
domainObject: undefined,
|
domainObject: undefined,
|
||||||
currentObjectPath: undefined
|
formats: undefined,
|
||||||
|
viewKey: `alphanumeric-format-${Math.random()}`,
|
||||||
|
status: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
statusClass() {
|
||||||
|
return (this.status) ? `is-status--${this.status}` : '';
|
||||||
|
},
|
||||||
showLabel() {
|
showLabel() {
|
||||||
let displayMode = this.item.displayMode;
|
let displayMode = this.item.displayMode;
|
||||||
|
|
||||||
@ -169,7 +171,11 @@ export default {
|
|||||||
valueMetadata() {
|
valueMetadata() {
|
||||||
return this.datum && this.metadata.value(this.item.value);
|
return this.datum && this.metadata.value(this.item.value);
|
||||||
},
|
},
|
||||||
valueFormatter() {
|
formatter() {
|
||||||
|
if (this.item.format) {
|
||||||
|
return this.customStringformatter;
|
||||||
|
}
|
||||||
|
|
||||||
return this.formats[this.item.value];
|
return this.formats[this.item.value];
|
||||||
},
|
},
|
||||||
telemetryValue() {
|
telemetryValue() {
|
||||||
@ -177,11 +183,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.item.format) {
|
return this.formatter && this.formatter.format(this.datum);
|
||||||
return printj.sprintf(this.item.format, this.datum[this.valueMetadata.key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.valueFormatter && this.valueFormatter.format(this.datum);
|
|
||||||
},
|
},
|
||||||
telemetryClass() {
|
telemetryClass() {
|
||||||
if (!this.datum) {
|
if (!this.datum) {
|
||||||
@ -213,9 +215,13 @@ export default {
|
|||||||
this.openmct.objects.get(this.item.identifier)
|
this.openmct.objects.get(this.item.identifier)
|
||||||
.then(this.setObject);
|
.then(this.setObject);
|
||||||
this.openmct.time.on("bounds", this.refreshData);
|
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() {
|
destroyed() {
|
||||||
this.removeSubscription();
|
this.removeSubscription();
|
||||||
|
this.removeStatusListener();
|
||||||
|
|
||||||
if (this.removeSelectable) {
|
if (this.removeSelectable) {
|
||||||
this.removeSelectable();
|
this.removeSelectable();
|
||||||
@ -224,6 +230,12 @@ export default {
|
|||||||
this.openmct.time.off("bounds", this.refreshData);
|
this.openmct.time.off("bounds", this.refreshData);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formattedValueForCopy() {
|
||||||
|
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||||
|
const timeFormatter = this.formats[timeFormatterKey];
|
||||||
|
|
||||||
|
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
|
||||||
|
},
|
||||||
requestHistoricalData() {
|
requestHistoricalData() {
|
||||||
let bounds = this.openmct.time.bounds();
|
let bounds = this.openmct.time.bounds();
|
||||||
let options = {
|
let options = {
|
||||||
@ -261,12 +273,26 @@ export default {
|
|||||||
this.requestHistoricalData(this.domainObject);
|
this.requestHistoricalData(this.domainObject);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getView() {
|
||||||
|
return {
|
||||||
|
getViewContext: () => {
|
||||||
|
return {
|
||||||
|
viewHistoricalData: true,
|
||||||
|
formattedValueForCopy: this.formattedValueForCopy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
setObject(domainObject) {
|
setObject(domainObject) {
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
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.requestHistoricalData();
|
||||||
this.subscribeToObject();
|
this.subscribeToObject();
|
||||||
|
|
||||||
@ -286,10 +312,37 @@ export default {
|
|||||||
delete this.immediatelySelect;
|
delete this.immediatelySelect;
|
||||||
},
|
},
|
||||||
updateTelemetryFormat(format) {
|
updateTelemetryFormat(format) {
|
||||||
|
this.customStringformatter.setFormat(format);
|
||||||
|
|
||||||
this.$emit('formatChanged', this.item, format);
|
this.$emit('formatChanged', this.item, format);
|
||||||
},
|
},
|
||||||
showContextMenu(event) {
|
async getContextMenuActions() {
|
||||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
// justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: $interiorMargin;
|
padding: $interiorMargin;
|
||||||
@ -27,14 +26,13 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include isMissing($absPos: true);
|
.is-status__indicator {
|
||||||
|
position: absolute;
|
||||||
.is-missing__indicator {
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-missing {
|
&[class*='is-status'] {
|
||||||
border: $borderMissing;
|
border: $borderMissing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
|
|||||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||||
|
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||||
|
|
||||||
export default function DisplayLayoutPlugin(options) {
|
export default function DisplayLayoutPlugin(options) {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
|
openmct.actions.register(new CopyToClipboardAction(openmct));
|
||||||
|
|
||||||
openmct.objectViews.addProvider({
|
openmct.objectViews.addProvider({
|
||||||
key: 'layout.view',
|
key: 'layout.view',
|
||||||
canView: function (domainObject) {
|
canView: function (domainObject) {
|
||||||
|
159
src/plugins/duplicate/DuplicateAction.js
Normal file
159
src/plugins/duplicate/DuplicateAction.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import DuplicateTask from './DuplicateTask';
|
||||||
|
|
||||||
|
export default class DuplicateAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.name = 'Duplicate';
|
||||||
|
this.key = 'duplicate';
|
||||||
|
this.description = 'Duplicate this object.';
|
||||||
|
this.cssClass = "icon-duplicate";
|
||||||
|
this.group = "action";
|
||||||
|
this.priority = 7;
|
||||||
|
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
|
||||||
|
async invoke(objectPath) {
|
||||||
|
let duplicationTask = new DuplicateTask(this.openmct);
|
||||||
|
let originalObject = objectPath[0];
|
||||||
|
let parent = objectPath[1];
|
||||||
|
let userInput = await this.getUserInput(originalObject, parent);
|
||||||
|
let newParent = userInput.location;
|
||||||
|
let inNavigationPath = this.inNavigationPath(originalObject);
|
||||||
|
|
||||||
|
// legacy check
|
||||||
|
if (this.isLegacyDomainObject(newParent)) {
|
||||||
|
newParent = await this.convertFromLegacy(newParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if editing, save
|
||||||
|
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||||
|
this.openmct.editor.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate
|
||||||
|
let newObject = await duplicationTask.duplicate(originalObject, newParent);
|
||||||
|
this.updateNameCheck(newObject, userInput.name);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserInput(originalObject, parent) {
|
||||||
|
let dialogService = this.openmct.$injector.get('dialogService');
|
||||||
|
let dialogForm = this.getDialogForm(originalObject, parent);
|
||||||
|
let formState = {
|
||||||
|
name: originalObject.name
|
||||||
|
};
|
||||||
|
let userInput = await dialogService.getUserInput(dialogForm, formState);
|
||||||
|
|
||||||
|
return userInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNameCheck(object, name) {
|
||||||
|
if (object.name !== name) {
|
||||||
|
this.openmct.objects.mutate(object, 'name', name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inNavigationPath(object) {
|
||||||
|
return this.openmct.router.path
|
||||||
|
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
getDialogForm(object, parent) {
|
||||||
|
return {
|
||||||
|
name: "Duplicate Item",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
control: "textfield",
|
||||||
|
name: "Folder Name",
|
||||||
|
pattern: "\\S+",
|
||||||
|
required: true,
|
||||||
|
cssClass: "l-input-lg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "location",
|
||||||
|
cssClass: "grows",
|
||||||
|
control: "locator",
|
||||||
|
validate: this.validate(object, parent),
|
||||||
|
key: 'location'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(object, currentParent) {
|
||||||
|
return (parentCandidate) => {
|
||||||
|
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||||
|
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
|
||||||
|
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
|
||||||
|
|
||||||
|
if (!parentCandidate || !currentParentKeystring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCandidateKeystring === objectKeystring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.openmct.composition.checkPolicy(
|
||||||
|
parentCandidate.useCapability('adapter'),
|
||||||
|
object
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isLegacyDomainObject(domainObject) {
|
||||||
|
return domainObject.getCapability !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertFromLegacy(legacyDomainObject) {
|
||||||
|
let objectContext = legacyDomainObject.getCapability('context');
|
||||||
|
let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath) {
|
||||||
|
let parent = objectPath[1];
|
||||||
|
let parentType = parent && this.openmct.types.get(parent.type);
|
||||||
|
let child = objectPath[0];
|
||||||
|
let 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);
|
||||||
|
}
|
||||||
|
}
|
270
src/plugins/duplicate/DuplicateTask.js
Normal file
270
src/plugins/duplicate/DuplicateTask.js
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class encapsulates the process of duplicating/copying a domain object
|
||||||
|
* and all of its children.
|
||||||
|
*
|
||||||
|
* @param {DomainObject} domainObject The object to duplicate
|
||||||
|
* @param {DomainObject} parent The new location of the cloned object tree
|
||||||
|
* @param {src/plugins/duplicate.DuplicateService~filter} filter
|
||||||
|
* a function used to filter out objects from
|
||||||
|
* the cloning process
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export default class DuplicateTask {
|
||||||
|
|
||||||
|
constructor(openmct) {
|
||||||
|
this.domainObject = undefined;
|
||||||
|
this.parent = undefined;
|
||||||
|
this.firstClone = undefined;
|
||||||
|
this.filter = undefined;
|
||||||
|
this.persisted = 0;
|
||||||
|
this.clones = [];
|
||||||
|
this.idMap = {};
|
||||||
|
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the duplicate/copy task with the objects provided in the constructor.
|
||||||
|
* @returns {promise} Which will resolve with a clone of the object
|
||||||
|
* once complete.
|
||||||
|
*/
|
||||||
|
async duplicate(domainObject, parent, filter) {
|
||||||
|
this.domainObject = domainObject;
|
||||||
|
this.parent = parent;
|
||||||
|
this.filter = filter || this.isCreatable;
|
||||||
|
|
||||||
|
await this.buildDuplicationPlan();
|
||||||
|
await this.persistObjects();
|
||||||
|
await this.addClonesToParent();
|
||||||
|
|
||||||
|
return this.firstClone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will build a graph of an object and all of its child objects in
|
||||||
|
* memory
|
||||||
|
* @private
|
||||||
|
* @param domainObject The original object to be copied
|
||||||
|
* @param parent The parent of the original object to be copied
|
||||||
|
* @returns {Promise} resolved with an array of clones of the models
|
||||||
|
* of the object tree being copied. Duplicating is done in a bottom-up
|
||||||
|
* fashion, so that the last member in the array is a clone of the model
|
||||||
|
* object being copied. The clones are all full composed with
|
||||||
|
* references to their own children.
|
||||||
|
*/
|
||||||
|
async buildDuplicationPlan() {
|
||||||
|
let domainObjectClone = await this.duplicateObject(this.domainObject);
|
||||||
|
if (domainObjectClone !== this.domainObject) {
|
||||||
|
domainObjectClone.location = this.getId(this.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.firstClone = domainObjectClone;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will persist a list of {@link objectClones}. It will persist all
|
||||||
|
* simultaneously, irrespective of order in the list. This may
|
||||||
|
* result in automatic request batching by the browser.
|
||||||
|
*/
|
||||||
|
async persistObjects() {
|
||||||
|
let initialCount = this.clones.length;
|
||||||
|
let dialog = this.openmct.overlays.progressDialog({
|
||||||
|
progressPerc: 0,
|
||||||
|
message: `Duplicating ${initialCount} files.`,
|
||||||
|
iconClass: 'info',
|
||||||
|
title: 'Duplicating'
|
||||||
|
});
|
||||||
|
let clonesDone = Promise.all(this.clones.map(clone => {
|
||||||
|
let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
|
||||||
|
let message = `Duplicating ${initialCount - this.persisted} files.`;
|
||||||
|
|
||||||
|
dialog.updateProgress(percentPersisted, message);
|
||||||
|
|
||||||
|
return this.openmct.objects.save(clone);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await clonesDone;
|
||||||
|
dialog.dismiss();
|
||||||
|
this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will add a list of clones to the specified parent's composition
|
||||||
|
*/
|
||||||
|
async addClonesToParent() {
|
||||||
|
let parentComposition = this.openmct.composition.get(this.parent);
|
||||||
|
await parentComposition.load();
|
||||||
|
parentComposition.add(this.firstClone);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recursive function that will perform a bottom-up duplicate of
|
||||||
|
* the object tree with originalObject at the root. Recurses to
|
||||||
|
* the farthest leaf, then works its way back up again,
|
||||||
|
* cloning objects, and composing them with their child clones
|
||||||
|
* as it goes
|
||||||
|
* @private
|
||||||
|
* @returns {DomainObject} If the type of the original object allows for
|
||||||
|
* duplication, then a duplicate of the object, otherwise the object
|
||||||
|
* itself (to allow linking to non duplicatable objects).
|
||||||
|
*/
|
||||||
|
async duplicateObject(originalObject) {
|
||||||
|
// Check if the creatable (or other passed in filter).
|
||||||
|
if (this.filter(originalObject)) {
|
||||||
|
// Clone original object
|
||||||
|
let clone = this.cloneObjectModel(originalObject);
|
||||||
|
|
||||||
|
// Get children, if any
|
||||||
|
let composeesCollection = this.openmct.composition.get(originalObject);
|
||||||
|
let composees;
|
||||||
|
|
||||||
|
if (composeesCollection) {
|
||||||
|
composees = await composeesCollection.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively duplicate children
|
||||||
|
return this.duplicateComposees(clone, composees);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not creatable, creating a link, no need to iterate children
|
||||||
|
return originalObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update identifiers in a cloned object model (or part of
|
||||||
|
* a cloned object model) to reflect new identifiers after
|
||||||
|
* duplicating.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
rewriteIdentifiers(obj, idMap) {
|
||||||
|
function lookupValue(value) {
|
||||||
|
return (typeof value === 'string' && idMap[value]) || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((value, index) => {
|
||||||
|
obj[index] = lookupValue(value);
|
||||||
|
this.rewriteIdentifiers(obj[index], idMap);
|
||||||
|
});
|
||||||
|
} else if (obj && typeof obj === 'object') {
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
let value = obj[key];
|
||||||
|
obj[key] = lookupValue(value);
|
||||||
|
if (idMap[key]) {
|
||||||
|
delete obj[key];
|
||||||
|
obj[idMap[key]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rewriteIdentifiers(value, idMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of objects composed by a parent, clone them, then
|
||||||
|
* add them to the parent.
|
||||||
|
* @private
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
async duplicateComposees(clonedParent, composees = []) {
|
||||||
|
let idMap = {};
|
||||||
|
|
||||||
|
let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
|
||||||
|
await previousPromise;
|
||||||
|
let clonedComposee = await this.duplicateObject(nextComposee);
|
||||||
|
idMap[this.getId(nextComposee)] = this.getId(clonedComposee);
|
||||||
|
await this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}, Promise.resolve());
|
||||||
|
|
||||||
|
await allComposeesDuplicated;
|
||||||
|
|
||||||
|
this.rewriteIdentifiers(clonedParent, idMap);
|
||||||
|
this.clones.push(clonedParent);
|
||||||
|
|
||||||
|
return clonedParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async composeChild(child, parent, setLocation) {
|
||||||
|
const PERSIST_BOOL = false;
|
||||||
|
let parentComposition = this.openmct.composition.get(parent);
|
||||||
|
await parentComposition.load();
|
||||||
|
parentComposition.add(child, PERSIST_BOOL);
|
||||||
|
|
||||||
|
//If a location is not specified, set it.
|
||||||
|
if (setLocation && child.location === undefined) {
|
||||||
|
let parentKeyString = this.getId(parent);
|
||||||
|
child.location = parentKeyString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypeDefinition(domainObject, definition) {
|
||||||
|
let typeDefinitions = this.openmct.types.get(domainObject.type).definition;
|
||||||
|
|
||||||
|
return typeDefinitions[definition] || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneObjectModel(domainObject) {
|
||||||
|
let clone = JSON.parse(JSON.stringify(domainObject));
|
||||||
|
let identifier = {
|
||||||
|
key: uuid(),
|
||||||
|
namespace: domainObject.identifier.namespace
|
||||||
|
};
|
||||||
|
|
||||||
|
if (clone.modified || clone.persisted || clone.location) {
|
||||||
|
clone.modified = undefined;
|
||||||
|
clone.persisted = undefined;
|
||||||
|
clone.location = undefined;
|
||||||
|
delete clone.modified;
|
||||||
|
delete clone.persisted;
|
||||||
|
delete clone.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clone.composition) {
|
||||||
|
clone.composition = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.identifier = identifier;
|
||||||
|
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(domainObject) {
|
||||||
|
return this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatable(domainObject) {
|
||||||
|
return this.getTypeDefinition(domainObject, 'creatable');
|
||||||
|
}
|
||||||
|
}
|
28
src/plugins/duplicate/plugin.js
Normal file
28
src/plugins/duplicate/plugin.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2019, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import DuplicateAction from "./DuplicateAction";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return function (openmct) {
|
||||||
|
openmct.actions.register(new DuplicateAction(openmct));
|
||||||
|
};
|
||||||
|
}
|
157
src/plugins/duplicate/pluginSpec.js
Normal file
157
src/plugins/duplicate/pluginSpec.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import DuplicateActionPlugin from './plugin.js';
|
||||||
|
import DuplicateAction from './DuplicateAction.js';
|
||||||
|
import DuplicateTask from './DuplicateTask.js';
|
||||||
|
import {
|
||||||
|
createOpenMct,
|
||||||
|
resetApplicationState,
|
||||||
|
getMockObjects
|
||||||
|
} from 'utils/testing';
|
||||||
|
|
||||||
|
describe("The Duplicate Action plugin", () => {
|
||||||
|
|
||||||
|
let openmct;
|
||||||
|
let duplicateTask;
|
||||||
|
let childObject;
|
||||||
|
let parentObject;
|
||||||
|
let anotherParentObject;
|
||||||
|
|
||||||
|
// this setups up the app
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
childObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Child Folder",
|
||||||
|
identifier: {
|
||||||
|
namespace: "",
|
||||||
|
key: "child-folder-object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
parentObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Parent Folder",
|
||||||
|
composition: [childObject.identifier]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
anotherParentObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Another Parent Folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
|
||||||
|
let objectGet = openmct.objects.get.bind(openmct.objects);
|
||||||
|
spyOn(openmct.objects, 'get').and.callFake((identifier) => {
|
||||||
|
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key);
|
||||||
|
|
||||||
|
if (!obj) {
|
||||||
|
// not one of the mocked objs, callthrough basically
|
||||||
|
return objectGet(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
|
||||||
|
return {
|
||||||
|
load: async () => {
|
||||||
|
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key);
|
||||||
|
let children = [];
|
||||||
|
|
||||||
|
if (obj) {
|
||||||
|
for (let i = 0; i < obj.composition.length; i++) {
|
||||||
|
children.push(await openmct.objects.get(obj.composition[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(children);
|
||||||
|
},
|
||||||
|
add: (child) => {
|
||||||
|
domainObject.composition.push(child.identifier);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// already installed by default, but never hurts, just adds to context menu
|
||||||
|
openmct.install(DuplicateActionPlugin());
|
||||||
|
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(DuplicateActionPlugin).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when moving an object to a new parent", () => {
|
||||||
|
|
||||||
|
beforeEach(async (done) => {
|
||||||
|
duplicateTask = new DuplicateTask(openmct);
|
||||||
|
await duplicateTask.duplicate(parentObject, anotherParentObject);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
|
||||||
|
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
|
||||||
|
let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier);
|
||||||
|
let duplicateObjectName = duplicatedObject.name;
|
||||||
|
|
||||||
|
expect(duplicateObjectName).toEqual(parentObject.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the duplicate child object's identifier should be new", () => {
|
||||||
|
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
|
||||||
|
|
||||||
|
expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when a new name is provided for the duplicated object", () => {
|
||||||
|
const NEW_NAME = 'New Name';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
duplicateTask = new DuplicateAction(openmct);
|
||||||
|
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the name is updated", () => {
|
||||||
|
let childName = parentObject.name;
|
||||||
|
expect(childName).toEqual(NEW_NAME);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
class="l-grid-view__item c-grid-item"
|
class="l-grid-view__item c-grid-item"
|
||||||
:class="{
|
:class="[{
|
||||||
'is-alias': item.isAlias === true,
|
'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
|
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
|
||||||
}"
|
}, statusClass]"
|
||||||
:href="objectLink"
|
:href="objectLink"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -27,8 +26,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="c-grid-item__controls">
|
<div class="c-grid-item__controls">
|
||||||
<div class="is-missing__indicator"
|
<div class="is-status__indicator"
|
||||||
title="This item is missing"
|
:title="`This item is ${status}`"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="icon-people"
|
class="icon-people"
|
||||||
@ -46,9 +45,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||||
import objectLink from '../../../ui/mixins/object-link';
|
import objectLink from '../../../ui/mixins/object-link';
|
||||||
|
import statusListener from './status-listener';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [contextMenuGesture, objectLink],
|
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -8,18 +8,18 @@
|
|||||||
<a
|
<a
|
||||||
ref="objectLink"
|
ref="objectLink"
|
||||||
class="c-object-label"
|
class="c-object-label"
|
||||||
:class="{ 'is-missing': item.model.status === 'missing' }"
|
:class="[statusClass]"
|
||||||
:href="objectLink"
|
:href="objectLink"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
:class="item.type.cssClass"
|
||||||
>
|
>
|
||||||
<span class="is-missing__indicator"
|
<span class="is-status__indicator"
|
||||||
title="This item is missing"
|
:title="`This item is ${status}`"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="c-list-item__type">
|
<td class="c-list-item__type">
|
||||||
@ -39,9 +39,10 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||||
import objectLink from '../../../ui/mixins/object-link';
|
import objectLink from '../../../ui/mixins/object-link';
|
||||||
|
import statusListener from './status-listener';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [contextMenuGesture, objectLink],
|
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
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 & {
|
body.desktop & {
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
height: $gridItemDesk;
|
height: $gridItemDesk;
|
||||||
width: $gridItemDesk;
|
width: $gridItemDesk;
|
||||||
@ -41,9 +43,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-missing {
|
&.is-status--notebook-default {
|
||||||
@include isMissing();
|
.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*='__type-icon'],
|
||||||
[class*='__details'] {
|
[class*='__details'] {
|
||||||
opacity: $opacityMissing;
|
opacity: $opacityMissing;
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
/******************************* LIST ITEM */
|
/******************************* LIST ITEM */
|
||||||
.c-list-item {
|
.c-list-item {
|
||||||
&__type-icon {
|
&__name__type-icon {
|
||||||
color: $colorItemTreeIcon;
|
color: $colorItemTreeIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__name__name {
|
||||||
@include ellipsize();
|
@include ellipsize();
|
||||||
|
|
||||||
|
a & {
|
||||||
|
color: $colorItemFg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.c-list-item__name) {
|
||||||
|
color: $colorItemFgDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-alias {
|
&.is-alias {
|
||||||
|
@ -28,9 +28,5 @@
|
|||||||
padding-top: $p;
|
padding-top: $p;
|
||||||
padding-bottom: $p;
|
padding-bottom: $p;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
|
|
||||||
&:not(.c-list-item__name) {
|
|
||||||
color: $colorItemFgDetails;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/plugins/folderView/components/status-listener.js
Normal file
33
src/plugins/folderView/components/status-listener.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export default {
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
statusClass() {
|
||||||
|
return (this.status) ? `is-status--${this.status}` : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setStatus(status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let identifier = this.item.model.identifier;
|
||||||
|
|
||||||
|
this.status = this.openmct.status.get(identifier);
|
||||||
|
this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.removeStatusListener();
|
||||||
|
}
|
||||||
|
};
|
@ -25,6 +25,8 @@ export default class GoToOriginalAction {
|
|||||||
this.name = 'Go To Original';
|
this.name = 'Go To Original';
|
||||||
this.key = 'goToOriginal';
|
this.key = 'goToOriginal';
|
||||||
this.description = 'Go to the original unlinked instance of this object';
|
this.description = 'Go to the original unlinked instance of this object';
|
||||||
|
this.group = 'action';
|
||||||
|
this.priority = 4;
|
||||||
|
|
||||||
this._openmct = openmct;
|
this._openmct = openmct;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
|
|||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct));
|
openmct.actions.register(new GoToOriginalAction(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
.c-imagery {
|
.c-imagery {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -19,14 +19,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__main-image {
|
&__main-image {
|
||||||
&__bg,
|
|
||||||
&__image {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__bg {
|
&__bg {
|
||||||
background-color: $colorPlotBg;
|
background-color: $colorPlotBg;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
&.unnsynced{
|
&.unnsynced{
|
||||||
@include sUnsynced();
|
@include sUnsynced();
|
||||||
@ -34,6 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
|
@include abs(); // Safari fix
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
@ -20,40 +20,21 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(
|
export default function MissingObjectInterceptor(openmct) {
|
||||||
['./AbstractComposeAction'],
|
openmct.objects.addGetInterceptor({
|
||||||
function (AbstractComposeAction) {
|
appliesTo: (identifier, domainObject) => {
|
||||||
|
return identifier.key !== 'mine';
|
||||||
/**
|
},
|
||||||
* The MoveAction is available from context menus and allows a user to
|
invoke: (identifier, object) => {
|
||||||
* move an object to another location of their choosing.
|
if (object === undefined) {
|
||||||
*
|
return {
|
||||||
* @implements {Action}
|
identifier,
|
||||||
* @constructor
|
type: 'unknown',
|
||||||
* @memberof platform/entanglement
|
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
|
||||||
*/
|
|
||||||
function MoveAction(policyService, locationService, moveService, context) {
|
|
||||||
AbstractComposeAction.apply(
|
|
||||||
this,
|
|
||||||
[policyService, locationService, moveService, context, "Move"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MoveAction.prototype = Object.create(AbstractComposeAction.prototype);
|
|
||||||
|
|
||||||
MoveAction.appliesTo = function (context) {
|
|
||||||
var applicableObject =
|
|
||||||
context.selectedObject || context.domainObject;
|
|
||||||
|
|
||||||
if (applicableObject && applicableObject.model.locked) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(applicableObject
|
|
||||||
&& applicableObject.hasCapability('context'));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return MoveAction;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -20,44 +20,24 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([], function () {
|
export default function MyItemsInterceptor(openmct) {
|
||||||
|
|
||||||
/**
|
openmct.objects.addGetInterceptor({
|
||||||
* Disallow moves when either the parent or the child are not
|
appliesTo: (identifier, domainObject) => {
|
||||||
* modifiable by users.
|
return identifier.key === 'mine';
|
||||||
* @constructor
|
},
|
||||||
* @implements {Policy}
|
invoke: (identifier, object) => {
|
||||||
* @memberof platform/entanglement
|
if (object === undefined) {
|
||||||
*/
|
return {
|
||||||
function MovePolicy() {
|
identifier,
|
||||||
}
|
"name": "My Items",
|
||||||
|
"type": "folder",
|
||||||
function parentOf(domainObject) {
|
"composition": [],
|
||||||
var context = domainObject.getCapability('context');
|
"location": "ROOT"
|
||||||
|
|
||||||
return context && context.getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
function allowMutation(domainObject) {
|
|
||||||
var type = domainObject && domainObject.getCapability('type');
|
|
||||||
|
|
||||||
return Boolean(type && type.hasFeature('creation'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectedObject(context) {
|
|
||||||
return context.selectedObject || context.domainObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
MovePolicy.prototype.allow = function (action, context) {
|
|
||||||
var key = action.getMetadata().key;
|
|
||||||
|
|
||||||
if (key === 'move') {
|
|
||||||
return allowMutation(selectedObject(context))
|
|
||||||
&& allowMutation(parentOf(selectedObject(context)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return MovePolicy;
|
return object;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
9
src/plugins/interceptors/plugin.js
Normal file
9
src/plugins/interceptors/plugin.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import missingObjectInterceptor from "./missingObjectInterceptor";
|
||||||
|
import myItemsInterceptor from "./myItemsInterceptor";
|
||||||
|
|
||||||
|
export default function plugin() {
|
||||||
|
return function install(openmct) {
|
||||||
|
myItemsInterceptor(openmct);
|
||||||
|
missingObjectInterceptor(openmct);
|
||||||
|
};
|
||||||
|
}
|
169
src/plugins/move/MoveAction.js
Normal file
169
src/plugins/move/MoveAction.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
export default class MoveAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.name = 'Move';
|
||||||
|
this.key = 'move';
|
||||||
|
this.description = 'Move this object from its containing object to another object.';
|
||||||
|
this.cssClass = "icon-move";
|
||||||
|
this.group = "action";
|
||||||
|
this.priority = 7;
|
||||||
|
|
||||||
|
this.openmct = openmct;
|
||||||
|
}
|
||||||
|
|
||||||
|
async invoke(objectPath) {
|
||||||
|
let object = objectPath[0];
|
||||||
|
let inNavigationPath = this.inNavigationPath(object);
|
||||||
|
let oldParent = objectPath[1];
|
||||||
|
let dialogService = this.openmct.$injector.get('dialogService');
|
||||||
|
let dialogForm = this.getDialogForm(object, oldParent);
|
||||||
|
let userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
|
||||||
|
|
||||||
|
// if we need to update name
|
||||||
|
if (object.name !== userInput.name) {
|
||||||
|
this.openmct.objects.mutate(object, 'name', userInput.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentContext = userInput.location.getCapability('context');
|
||||||
|
let newParent = await this.openmct.objects.get(parentContext.domainObject.id);
|
||||||
|
|
||||||
|
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||||
|
this.openmct.editor.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addToNewParent(object, newParent);
|
||||||
|
this.removeFromOldParent(oldParent, object);
|
||||||
|
|
||||||
|
if (inNavigationPath) {
|
||||||
|
let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier);
|
||||||
|
let root = await this.openmct.objects.getRoot();
|
||||||
|
let rootChildCount = root.composition.length;
|
||||||
|
|
||||||
|
// if not multiple root children, remove root from path
|
||||||
|
if (rootChildCount < 2) {
|
||||||
|
newObjectPath.pop(); // remove ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
this.navigateTo(newObjectPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inNavigationPath(object) {
|
||||||
|
return this.openmct.router.path
|
||||||
|
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(objectPath) {
|
||||||
|
let urlPath = objectPath.reverse()
|
||||||
|
.map(object => this.openmct.objects.makeKeyString(object.identifier))
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
window.location.href = '#/browse/' + urlPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToNewParent(child, newParent) {
|
||||||
|
let newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier);
|
||||||
|
let compositionCollection = this.openmct.composition.get(newParent);
|
||||||
|
|
||||||
|
this.openmct.objects.mutate(child, 'location', newParentKeyString);
|
||||||
|
compositionCollection.add(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromOldParent(parent, child) {
|
||||||
|
let compositionCollection = this.openmct.composition.get(parent);
|
||||||
|
|
||||||
|
compositionCollection.remove(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDialogForm(object, parent) {
|
||||||
|
return {
|
||||||
|
name: "Move Item",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
control: "textfield",
|
||||||
|
name: "Folder Name",
|
||||||
|
pattern: "\\S+",
|
||||||
|
required: true,
|
||||||
|
cssClass: "l-input-lg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "location",
|
||||||
|
control: "locator",
|
||||||
|
validate: this.validate(object, parent),
|
||||||
|
key: 'location'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(object, currentParent) {
|
||||||
|
return (parentCandidate) => {
|
||||||
|
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||||
|
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
|
||||||
|
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
|
||||||
|
|
||||||
|
if (!parentCandidateKeystring || !currentParentKeystring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCandidateKeystring === currentParentKeystring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCandidateKeystring === objectKeystring) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCandidate.getModel().composition.indexOf(objectKeystring) !== -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.openmct.composition.checkPolicy(
|
||||||
|
parentCandidate.useCapability('adapter'),
|
||||||
|
object
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath) {
|
||||||
|
let parent = objectPath[1];
|
||||||
|
let parentType = parent && this.openmct.types.get(parent.type);
|
||||||
|
let child = objectPath[0];
|
||||||
|
let childType = child && this.openmct.types.get(child.type);
|
||||||
|
|
||||||
|
if (child.locked || (parent && parent.locked)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentType
|
||||||
|
&& parentType.definition.creatable
|
||||||
|
&& childType
|
||||||
|
&& childType.definition.creatable
|
||||||
|
&& Array.isArray(parent.composition);
|
||||||
|
}
|
||||||
|
}
|
28
src/plugins/move/plugin.js
Normal file
28
src/plugins/move/plugin.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import MoveAction from "./MoveAction";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
return function (openmct) {
|
||||||
|
openmct.actions.register(new MoveAction(openmct));
|
||||||
|
};
|
||||||
|
}
|
110
src/plugins/move/pluginSpec.js
Normal file
110
src/plugins/move/pluginSpec.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
import MoveActionPlugin from './plugin.js';
|
||||||
|
import MoveAction from './MoveAction.js';
|
||||||
|
import {
|
||||||
|
createOpenMct,
|
||||||
|
resetApplicationState,
|
||||||
|
getMockObjects
|
||||||
|
} from 'utils/testing';
|
||||||
|
|
||||||
|
describe("The Move Action plugin", () => {
|
||||||
|
|
||||||
|
let openmct;
|
||||||
|
let moveAction;
|
||||||
|
let childObject;
|
||||||
|
let parentObject;
|
||||||
|
let anotherParentObject;
|
||||||
|
|
||||||
|
// this setups up the app
|
||||||
|
beforeEach((done) => {
|
||||||
|
const appHolder = document.createElement('div');
|
||||||
|
appHolder.style.width = '640px';
|
||||||
|
appHolder.style.height = '480px';
|
||||||
|
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
childObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Child Folder",
|
||||||
|
identifier: {
|
||||||
|
namespace: "",
|
||||||
|
key: "child-folder-object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
parentObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Parent Folder",
|
||||||
|
composition: [childObject.identifier]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
anotherParentObject = getMockObjects({
|
||||||
|
objectKeyStrings: ['folder'],
|
||||||
|
overwrite: {
|
||||||
|
folder: {
|
||||||
|
name: "Another Parent Folder"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).folder;
|
||||||
|
|
||||||
|
// already installed by default, but never hurts, just adds to context menu
|
||||||
|
openmct.install(MoveActionPlugin());
|
||||||
|
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless(appHolder);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(MoveActionPlugin).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when moving an object to a new parent and removing from the old parent", () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
moveAction = new MoveAction(openmct);
|
||||||
|
moveAction.addToNewParent(childObject, anotherParentObject);
|
||||||
|
moveAction.removeFromOldParent(parentObject, childObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the child object's identifier should be in the new parent's composition", () => {
|
||||||
|
let newParentChild = anotherParentObject.composition[0];
|
||||||
|
expect(newParentChild).toEqual(childObject.identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the child object's identifier should be removed from the old parent's composition", () => {
|
||||||
|
let oldParentComposition = parentObject.composition;
|
||||||
|
expect(oldParentComposition.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -28,6 +28,8 @@ export default class NewFolderAction {
|
|||||||
this.key = 'newFolder';
|
this.key = 'newFolder';
|
||||||
this.description = 'Create a new folder';
|
this.description = 'Create a new folder';
|
||||||
this.cssClass = 'icon-folder-new';
|
this.cssClass = 'icon-folder-new';
|
||||||
|
this.group = "action";
|
||||||
|
this.priority = 9;
|
||||||
|
|
||||||
this._openmct = openmct;
|
this._openmct = openmct;
|
||||||
this._dialogForm = {
|
this._dialogForm = {
|
||||||
|
@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
|
|||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
|
openmct.actions.register(new NewFolderAction(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,7 @@ describe("the plugin", () => {
|
|||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
|
|
||||||
newFolderAction = openmct.contextMenu._allActions.filter(action => {
|
newFolderAction = openmct.actions._allActions.newFolder;
|
||||||
return action.key === 'newFolder';
|
|
||||||
})[0];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
40
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal file
40
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||||
|
import { addNotebookEntry } from '../utils/notebook-entries';
|
||||||
|
|
||||||
|
export default class CopyToNotebookAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.cssClass = 'icon-duplicate';
|
||||||
|
this.description = 'Copy value to notebook as an entry';
|
||||||
|
this.group = "action";
|
||||||
|
this.key = 'copyToNotebook';
|
||||||
|
this.name = 'Copy to Notebook';
|
||||||
|
this.priority = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToNotebook(entryText) {
|
||||||
|
const notebookStorage = getDefaultNotebook();
|
||||||
|
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
|
||||||
|
.then(domainObject => {
|
||||||
|
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
|
||||||
|
|
||||||
|
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||||
|
const msg = `Saved to Notebook ${defaultPath}`;
|
||||||
|
this.openmct.notifications.info(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke(objectPath, view = {}) {
|
||||||
|
let viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
|
||||||
|
this.copyToNotebook(viewContext.formattedValueForCopy());
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath, view = {}) {
|
||||||
|
let viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext && viewContext.formattedValueForCopy
|
||||||
|
&& typeof viewContext.formattedValueForCopy === 'function';
|
||||||
|
}
|
||||||
|
}
|
@ -24,12 +24,12 @@
|
|||||||
:default-section-id="defaultSectionId"
|
:default-section-id="defaultSectionId"
|
||||||
:domain-object="internalDomainObject"
|
:domain-object="internalDomainObject"
|
||||||
:page-title="internalDomainObject.configuration.pageTitle"
|
:page-title="internalDomainObject.configuration.pageTitle"
|
||||||
:pages="pages"
|
|
||||||
:section-title="internalDomainObject.configuration.sectionTitle"
|
:section-title="internalDomainObject.configuration.sectionTitle"
|
||||||
:sections="sections"
|
:sections="sections"
|
||||||
|
:selected-section="selectedSection"
|
||||||
:sidebar-covers-entries="sidebarCoversEntries"
|
:sidebar-covers-entries="sidebarCoversEntries"
|
||||||
@updatePage="updatePage"
|
@pagesChanged="pagesChanged"
|
||||||
@updateSection="updateSection"
|
@sectionsChanged="sectionsChanged"
|
||||||
@toggleNav="toggleNav"
|
@toggleNav="toggleNav"
|
||||||
/>
|
/>
|
||||||
<div class="c-notebook__page-view">
|
<div class="c-notebook__page-view">
|
||||||
@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue';
|
|||||||
import SearchResults from './SearchResults.vue';
|
import SearchResults from './SearchResults.vue';
|
||||||
import Sidebar from './Sidebar.vue';
|
import Sidebar from './Sidebar.vue';
|
||||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||||
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
|
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||||
import objectUtils from 'objectUtils';
|
import objectUtils from 'objectUtils';
|
||||||
|
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
@ -220,7 +220,7 @@ export default {
|
|||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateSection({ sections });
|
this.sectionsChanged({ sections });
|
||||||
this.throttledSearchItem('');
|
this.throttledSearchItem('');
|
||||||
},
|
},
|
||||||
createNotebookStorageObject() {
|
createNotebookStorageObject() {
|
||||||
@ -309,7 +309,7 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d);
|
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
|
||||||
},
|
},
|
||||||
getPage(section, id) {
|
getPage(section, id) {
|
||||||
return section.pages.find(p => p.id === id);
|
return section.pages.find(p => p.id === id);
|
||||||
@ -379,9 +379,6 @@ export default {
|
|||||||
|
|
||||||
return this.sections.find(section => section.isSelected);
|
return this.sections.find(section => section.isSelected);
|
||||||
},
|
},
|
||||||
mutateObject(key, value) {
|
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, key, value);
|
|
||||||
},
|
|
||||||
navigateToSectionPage() {
|
navigateToSectionPage() {
|
||||||
const { pageId, sectionId } = this.openmct.router.getParams();
|
const { pageId, sectionId } = this.openmct.router.getParams();
|
||||||
if (!pageId || !sectionId) {
|
if (!pageId || !sectionId) {
|
||||||
@ -398,7 +395,7 @@ export default {
|
|||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateSection({ sections });
|
this.sectionsChanged({ sections });
|
||||||
},
|
},
|
||||||
newEntry(embed = null) {
|
newEntry(embed = null) {
|
||||||
this.search = '';
|
this.search = '';
|
||||||
@ -411,19 +408,30 @@ export default {
|
|||||||
orientationChange() {
|
orientationChange() {
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
},
|
},
|
||||||
|
pagesChanged({ pages = [], id = null}) {
|
||||||
|
const selectedSection = this.getSelectedSection();
|
||||||
|
if (!selectedSection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedSection.pages = pages;
|
||||||
|
const sections = this.sections.map(section => {
|
||||||
|
if (section.id === selectedSection.id) {
|
||||||
|
section = selectedSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return section;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sectionsChanged({ sections });
|
||||||
|
this.updateDefaultNotebookPage(pages, id);
|
||||||
|
},
|
||||||
removeDefaultClass(domainObject) {
|
removeDefaultClass(domainObject) {
|
||||||
if (!domainObject) {
|
if (!domainObject) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classList = domainObject.classList || [];
|
this.openmct.status.delete(domainObject.identifier);
|
||||||
const index = classList.indexOf(DEFAULT_CLASS);
|
|
||||||
if (!classList.length || index < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
classList.splice(index, 1);
|
|
||||||
this.openmct.objects.mutate(domainObject, 'classList', classList);
|
|
||||||
},
|
},
|
||||||
searchItem(input) {
|
searchItem(input) {
|
||||||
this.search = input;
|
this.search = input;
|
||||||
@ -440,12 +448,14 @@ export default {
|
|||||||
setDefaultNotebook(this.openmct, notebookStorage);
|
setDefaultNotebook(this.openmct, notebookStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||||
this.defaultSectionId = notebookStorage.section.id;
|
this.defaultSectionId = notebookStorage.section.id;
|
||||||
|
setDefaultNotebookSection(notebookStorage.section);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
||||||
this.defaultPageId = notebookStorage.page.id;
|
this.defaultPageId = notebookStorage.page.id;
|
||||||
|
setDefaultNotebookPage(notebookStorage.page);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDefaultNotebookPage(pages, id) {
|
updateDefaultNotebookPage(pages, id) {
|
||||||
@ -509,29 +519,11 @@ export default {
|
|||||||
const notebookEntries = configuration.entries || {};
|
const notebookEntries = configuration.entries || {};
|
||||||
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
|
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
|
||||||
|
|
||||||
this.mutateObject('configuration.entries', notebookEntries);
|
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries);
|
||||||
},
|
},
|
||||||
updateInternalDomainObject(domainObject) {
|
updateInternalDomainObject(domainObject) {
|
||||||
this.internalDomainObject = domainObject;
|
this.internalDomainObject = domainObject;
|
||||||
},
|
},
|
||||||
updatePage({ pages = [], id = null}) {
|
|
||||||
const selectedSection = this.getSelectedSection();
|
|
||||||
if (!selectedSection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedSection.pages = pages;
|
|
||||||
const sections = this.sections.map(section => {
|
|
||||||
if (section.id === selectedSection.id) {
|
|
||||||
section = selectedSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
return section;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateSection({ sections });
|
|
||||||
this.updateDefaultNotebookPage(pages, id);
|
|
||||||
},
|
|
||||||
updateParams(sections) {
|
updateParams(sections) {
|
||||||
const selectedSection = sections.find(s => s.isSelected);
|
const selectedSection = sections.find(s => s.isSelected);
|
||||||
if (!selectedSection) {
|
if (!selectedSection) {
|
||||||
@ -555,8 +547,8 @@ export default {
|
|||||||
pageId
|
pageId
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateSection({ sections, id = null }) {
|
sectionsChanged({ sections, id = null }) {
|
||||||
this.mutateObject('configuration.sections', sections);
|
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections);
|
||||||
|
|
||||||
this.updateParams(sections);
|
this.updateParams(sections);
|
||||||
this.updateDefaultNotebookSection(sections, id);
|
this.updateDefaultNotebookSection(sections, id);
|
||||||
|
@ -118,7 +118,7 @@ export default {
|
|||||||
painterroInstance.show(this.embed.snapshot.src);
|
painterroInstance.show(this.embed.snapshot.src);
|
||||||
},
|
},
|
||||||
changeLocation() {
|
changeLocation() {
|
||||||
const link = this.embed.historicLink;
|
const hash = this.embed.historicLink;
|
||||||
|
|
||||||
const bounds = this.openmct.time.bounds();
|
const bounds = this.openmct.time.bounds();
|
||||||
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
||||||
@ -143,8 +143,7 @@ export default {
|
|||||||
this.openmct.notifications.alert(message);
|
this.openmct.notifications.alert(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(link);
|
window.location.hash = hash;
|
||||||
window.location.href = url.hash;
|
|
||||||
},
|
},
|
||||||
formatTime(unixTime, timeFormat) {
|
formatTime(unixTime, timeFormat) {
|
||||||
return Moment.utc(unixTime).format(timeFormat);
|
return Moment.utc(unixTime).format(timeFormat);
|
||||||
|
@ -1,35 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||||
<button
|
<button
|
||||||
class="c-button--menu icon-notebook"
|
class="c-icon-button c-button--menu icon-camera"
|
||||||
title="Take a Notebook Snapshot"
|
title="Take a Notebook Snapshot"
|
||||||
@click="setNotebookTypes"
|
@click.stop.prevent="showMenu"
|
||||||
@click.stop="toggleMenu"
|
|
||||||
>
|
>
|
||||||
<span class="c-button__label"></span>
|
<span
|
||||||
|
title="Take Notebook Snapshot"
|
||||||
|
class="c-icon-button__label"
|
||||||
|
>
|
||||||
|
Snapshot
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
|
||||||
v-show="showMenu"
|
|
||||||
class="c-menu"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="(type, index) in notebookTypes"
|
|
||||||
:key="index"
|
|
||||||
:class="type.cssClass"
|
|
||||||
:title="type.name"
|
|
||||||
@click="snapshot(type)"
|
|
||||||
>
|
|
||||||
{{ type.name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Snapshot from '../snapshot';
|
import Snapshot from '../snapshot';
|
||||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
|
||||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -57,22 +45,22 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
notebookSnapshot: null,
|
notebookSnapshot: null,
|
||||||
notebookTypes: [],
|
notebookTypes: []
|
||||||
showMenu: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
validateNotebookStorageObject();
|
||||||
|
|
||||||
document.addEventListener('click', this.hideMenu);
|
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||||
},
|
this.setDefaultNotebookStatus();
|
||||||
destroyed() {
|
|
||||||
document.removeEventListener('click', this.hideMenu);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setNotebookTypes() {
|
showMenu(event) {
|
||||||
const notebookTypes = [];
|
const notebookTypes = [];
|
||||||
const defaultNotebook = getDefaultNotebook();
|
const defaultNotebook = getDefaultNotebook();
|
||||||
|
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||||
|
const x = elementBoundingClientRect.x;
|
||||||
|
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||||
|
|
||||||
if (defaultNotebook) {
|
if (defaultNotebook) {
|
||||||
const domainObject = defaultNotebook.domainObject;
|
const domainObject = defaultNotebook.domainObject;
|
||||||
@ -83,28 +71,24 @@ export default {
|
|||||||
notebookTypes.push({
|
notebookTypes.push({
|
||||||
cssClass: 'icon-notebook',
|
cssClass: 'icon-notebook',
|
||||||
name: `Save to Notebook ${defaultPath}`,
|
name: `Save to Notebook ${defaultPath}`,
|
||||||
type: NOTEBOOK_DEFAULT
|
callBack: () => {
|
||||||
|
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notebookTypes.push({
|
notebookTypes.push({
|
||||||
cssClass: 'icon-notebook',
|
cssClass: 'icon-camera',
|
||||||
name: 'Save to Notebook Snapshots',
|
name: 'Save to Notebook Snapshots',
|
||||||
type: NOTEBOOK_SNAPSHOT
|
callBack: () => {
|
||||||
|
return this.snapshot(NOTEBOOK_SNAPSHOT);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notebookTypes = notebookTypes;
|
this.openmct.menus.showMenu(x, y, notebookTypes);
|
||||||
},
|
},
|
||||||
toggleMenu() {
|
snapshot(notebookType) {
|
||||||
this.showMenu = !this.showMenu;
|
|
||||||
},
|
|
||||||
hideMenu() {
|
|
||||||
this.showMenu = false;
|
|
||||||
},
|
|
||||||
snapshot(notebook) {
|
|
||||||
this.hideMenu();
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const element = document.querySelector('.c-overlay__contents')
|
const element = document.querySelector('.c-overlay__contents')
|
||||||
|| document.getElementsByClassName('l-shell__main-container')[0];
|
|| document.getElementsByClassName('l-shell__main-container')[0];
|
||||||
@ -122,8 +106,17 @@ export default {
|
|||||||
openmct: this.openmct
|
openmct: this.openmct
|
||||||
};
|
};
|
||||||
|
|
||||||
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
|
this.notebookSnapshot.capture(snapshotMeta, notebookType, element);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
setDefaultNotebookStatus() {
|
||||||
|
let defaultNotebookObject = getDefaultNotebook();
|
||||||
|
|
||||||
|
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
|
||||||
|
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
|
||||||
|
|
||||||
|
this.openmct.status.set(notebookIdentifier, 'notebook-default');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="l-browse-bar__start">
|
<div class="l-browse-bar__start">
|
||||||
<div class="l-browse-bar__object-name--w">
|
<div class="l-browse-bar__object-name--w">
|
||||||
<div class="l-browse-bar__object-name c-object-label">
|
<div class="l-browse-bar__object-name c-object-label">
|
||||||
<div class="c-object-label__type-icon icon-notebook"></div>
|
<div class="c-object-label__type-icon icon-camera"></div>
|
||||||
<div class="c-object-label__name">
|
<div class="c-object-label__name">
|
||||||
Notebook Snapshots
|
Notebook Snapshots
|
||||||
<span v-if="snapshots.length"
|
<span v-if="snapshots.length"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-indicator c-indicator--clickable icon-notebook"
|
<div class="c-indicator c-indicator--clickable icon-camera"
|
||||||
:class="[
|
:class="[
|
||||||
{ 's-status-off': snapshotCount === 0 },
|
{ 's-status-off': snapshotCount === 0 },
|
||||||
{ 's-status-on': snapshotCount > 0 },
|
{ 's-status-on': snapshotCount > 0 },
|
||||||
|
@ -66,14 +66,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
deletePage(id) {
|
deletePage(id) {
|
||||||
const selectedSection = this.sections.find(s => s.isSelected);
|
const selectedSection = this.sections.find(s => s.isSelected);
|
||||||
const page = this.pages.filter(p => p.id !== id);
|
const page = this.pages.find(p => p.id !== id);
|
||||||
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
|
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
|
||||||
|
|
||||||
const selectedPage = this.pages.find(p => p.isSelected);
|
const selectedPage = this.pages.find(p => p.isSelected);
|
||||||
|
@ -53,10 +53,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
deleteSection(id) {
|
deleteSection(id) {
|
||||||
const section = this.sections.find(s => s.id === id);
|
const section = this.sections.find(s => s.id === id);
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
:domain-object="domainObject"
|
:domain-object="domainObject"
|
||||||
:sections="sections"
|
:sections="sections"
|
||||||
:section-title="sectionTitle"
|
:section-title="sectionTitle"
|
||||||
@updateSection="updateSection"
|
@updateSection="sectionsChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
:sidebar-covers-entries="sidebarCoversEntries"
|
:sidebar-covers-entries="sidebarCoversEntries"
|
||||||
:page-title="pageTitle"
|
:page-title="pageTitle"
|
||||||
@toggleNav="toggleNav"
|
@toggleNav="toggleNav"
|
||||||
@updatePage="updatePage"
|
@updatePage="pagesChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,13 +85,6 @@ export default {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
default() {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pageTitle: {
|
pageTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
default() {
|
default() {
|
||||||
@ -122,9 +115,16 @@ export default {
|
|||||||
return {
|
return {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
pages() {
|
||||||
|
const selectedSection = this.sections.find(section => section.isSelected);
|
||||||
|
|
||||||
|
return selectedSection && selectedSection.pages || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
pages(newpages) {
|
pages(newPages) {
|
||||||
if (!newpages.length) {
|
if (!newPages.length) {
|
||||||
this.addPage();
|
this.addPage();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -141,55 +141,79 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addPage() {
|
addPage() {
|
||||||
|
const newPage = this.createNewPage();
|
||||||
|
const pages = this.addNewPage(newPage);
|
||||||
|
|
||||||
|
this.pagesChanged({
|
||||||
|
pages,
|
||||||
|
id: newPage.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addSection() {
|
||||||
|
const newSection = this.createNewSection();
|
||||||
|
const sections = this.addNewSection(newSection);
|
||||||
|
|
||||||
|
this.sectionsChanged({
|
||||||
|
sections,
|
||||||
|
id: newSection.id
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addNewPage(page) {
|
||||||
|
const pages = this.pages.map(p => {
|
||||||
|
p.isSelected = false;
|
||||||
|
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pages.concat(page);
|
||||||
|
},
|
||||||
|
addNewSection(section) {
|
||||||
|
const sections = this.sections.map(s => {
|
||||||
|
s.isSelected = false;
|
||||||
|
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sections.concat(section);
|
||||||
|
},
|
||||||
|
createNewPage() {
|
||||||
const pageTitle = this.pageTitle;
|
const pageTitle = this.pageTitle;
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
const page = {
|
|
||||||
|
return {
|
||||||
id,
|
id,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
name: `Unnamed ${pageTitle}`,
|
name: `Unnamed ${pageTitle}`,
|
||||||
pageTitle
|
pageTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pages.forEach(p => p.isSelected = false);
|
|
||||||
const pages = this.pages.concat(page);
|
|
||||||
|
|
||||||
this.updatePage({
|
|
||||||
pages,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
addSection() {
|
createNewSection() {
|
||||||
const sectionTitle = this.sectionTitle;
|
const sectionTitle = this.sectionTitle;
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
const section = {
|
const page = this.createNewPage();
|
||||||
|
const pages = [page];
|
||||||
|
|
||||||
|
return {
|
||||||
id,
|
id,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
name: `Unnamed ${sectionTitle}`,
|
name: `Unnamed ${sectionTitle}`,
|
||||||
pages: [],
|
pages,
|
||||||
sectionTitle
|
sectionTitle
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sections.forEach(s => s.isSelected = false);
|
|
||||||
const sections = this.sections.concat(section);
|
|
||||||
|
|
||||||
this.updateSection({
|
|
||||||
sections,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
toggleNav() {
|
toggleNav() {
|
||||||
this.$emit('toggleNav');
|
this.$emit('toggleNav');
|
||||||
},
|
},
|
||||||
updatePage({ pages, id }) {
|
pagesChanged({ pages, id }) {
|
||||||
this.$emit('updatePage', {
|
this.$emit('pagesChanged', {
|
||||||
pages,
|
pages,
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateSection({ sections, id }) {
|
sectionsChanged({ sections, id }) {
|
||||||
this.$emit('updateSection', {
|
this.$emit('sectionsChanged', {
|
||||||
sections,
|
sections,
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
||||||
import Notebook from './components/Notebook.vue';
|
import Notebook from './components/Notebook.vue';
|
||||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||||
import SnapshotContainer from './snapshot-container';
|
import SnapshotContainer from './snapshot-container';
|
||||||
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
|
|||||||
|
|
||||||
installed = true;
|
installed = true;
|
||||||
|
|
||||||
|
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||||
|
|
||||||
const notebookType = {
|
const notebookType = {
|
||||||
name: 'Notebook',
|
name: 'Notebook',
|
||||||
description: 'Create and save timestamped notes with embedded object snapshots.',
|
description: 'Create and save timestamped notes with embedded object snapshots.',
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
import NotebookPlugin from './plugin';
|
import NotebookPlugin from './plugin';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
@ -133,90 +133,4 @@ describe("Notebook plugin:", () => {
|
|||||||
expect(hasMajorElements).toBe(true);
|
expect(hasMajorElements).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Notebook Snapshots view:", () => {
|
|
||||||
let snapshotIndicator;
|
|
||||||
let drawerElement;
|
|
||||||
|
|
||||||
function clickSnapshotIndicator() {
|
|
||||||
const indicator = element.querySelector('.icon-notebook');
|
|
||||||
const button = indicator.querySelector('button');
|
|
||||||
const clickEvent = createMouseEvent('click');
|
|
||||||
|
|
||||||
button.dispatchEvent(clickEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
snapshotIndicator = openmct.indicators.indicatorObjects
|
|
||||||
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
|
||||||
|
|
||||||
element.append(snapshotIndicator);
|
|
||||||
|
|
||||||
return Vue.nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
snapshotIndicator.remove();
|
|
||||||
if (drawerElement) {
|
|
||||||
drawerElement.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
drawerElement = document.querySelector('.l-shell__drawer');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
if (drawerElement) {
|
|
||||||
drawerElement.classList.remove('is-expanded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has Snapshots indicator", () => {
|
|
||||||
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
|
||||||
expect(hasSnapshotIndicator).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("snapshots container has class isExpanded", () => {
|
|
||||||
let classes = drawerElement.classList;
|
|
||||||
const isExpandedBefore = classes.contains('is-expanded');
|
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
|
||||||
classes = drawerElement.classList;
|
|
||||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
|
||||||
|
|
||||||
const success = isExpandedBefore === false
|
|
||||||
&& isExpandedAfterFirstClick === true;
|
|
||||||
|
|
||||||
expect(success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("snapshots container does not have class isExpanded", () => {
|
|
||||||
let classes = drawerElement.classList;
|
|
||||||
const isExpandedBefore = classes.contains('is-expanded');
|
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
|
||||||
classes = drawerElement.classList;
|
|
||||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
|
||||||
classes = drawerElement.classList;
|
|
||||||
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
|
||||||
|
|
||||||
const success = isExpandedBefore === false
|
|
||||||
&& isExpandedAfterFirstClick === true
|
|
||||||
&& isExpandedAfterSecondClick === false;
|
|
||||||
|
|
||||||
expect(success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("show notebook snapshots container text", () => {
|
|
||||||
clickSnapshotIndicator();
|
|
||||||
|
|
||||||
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
|
||||||
const snapshotsText = notebookSnapshots.textContent.trim();
|
|
||||||
|
|
||||||
expect(snapshotsText).toBe('Notebook Snapshots');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -49,7 +49,7 @@ export default class Snapshot {
|
|||||||
.then(domainObject => {
|
.then(domainObject => {
|
||||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
||||||
|
|
||||||
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
|
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||||
const msg = `Saved to Notebook ${defaultPath}`;
|
const msg = `Saved to Notebook ${defaultPath}`;
|
||||||
this._showNotification(msg);
|
this._showNotification(msg);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import objectLink from '../../../ui/mixins/object-link';
|
import objectLink from '../../../ui/mixins/object-link';
|
||||||
|
|
||||||
export const DEFAULT_CLASS = 'is-notebook-default';
|
export const DEFAULT_CLASS = 'notebook-default';
|
||||||
const TIME_BOUNDS = {
|
const TIME_BOUNDS = {
|
||||||
START_BOUND: 'tc.startBound',
|
START_BOUND: 'tc.startBound',
|
||||||
END_BOUND: 'tc.endBound',
|
END_BOUND: 'tc.endBound',
|
||||||
@ -8,6 +8,29 @@ const TIME_BOUNDS = {
|
|||||||
END_DELTA: 'tc.endDelta'
|
END_DELTA: 'tc.endDelta'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function addEntryIntoPage(notebookStorage, entries, entry) {
|
||||||
|
const defaultSection = notebookStorage.section;
|
||||||
|
const defaultPage = notebookStorage.page;
|
||||||
|
if (!defaultSection || !defaultPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEntries = JSON.parse(JSON.stringify(entries));
|
||||||
|
let section = newEntries[defaultSection.id];
|
||||||
|
if (!section) {
|
||||||
|
newEntries[defaultSection.id] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let page = newEntries[defaultSection.id][defaultPage.id];
|
||||||
|
if (!page) {
|
||||||
|
newEntries[defaultSection.id][defaultPage.id] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
newEntries[defaultSection.id][defaultPage.id].push(entry);
|
||||||
|
|
||||||
|
return newEntries;
|
||||||
|
}
|
||||||
|
|
||||||
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
|
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
|
||||||
if (historicLink.includes('tc.mode=fixed')) {
|
if (historicLink.includes('tc.mode=fixed')) {
|
||||||
return historicLink;
|
return historicLink;
|
||||||
@ -38,35 +61,6 @@ export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
|
|||||||
return params.join('&');
|
return params.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNotebookDefaultEntries(notebookStorage, domainObject) {
|
|
||||||
if (!notebookStorage || !domainObject) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultSection = notebookStorage.section;
|
|
||||||
const defaultPage = notebookStorage.page;
|
|
||||||
if (!defaultSection || !defaultPage) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration = domainObject.configuration;
|
|
||||||
const entries = configuration.entries || {};
|
|
||||||
|
|
||||||
let section = entries[defaultSection.id];
|
|
||||||
if (!section) {
|
|
||||||
section = {};
|
|
||||||
entries[defaultSection.id] = section;
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = entries[defaultSection.id][defaultPage.id];
|
|
||||||
if (!page) {
|
|
||||||
page = [];
|
|
||||||
entries[defaultSection.id][defaultPage.id] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries[defaultSection.id][defaultPage.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createNewEmbed(snapshotMeta, snapshot = '') {
|
export function createNewEmbed(snapshotMeta, snapshot = '') {
|
||||||
const {
|
const {
|
||||||
bounds,
|
bounds,
|
||||||
@ -103,7 +97,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) {
|
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') {
|
||||||
if (!openmct || !domainObject || !notebookStorage) {
|
if (!openmct || !domainObject || !notebookStorage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -120,24 +114,25 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
|||||||
? [embed]
|
? [embed]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject);
|
|
||||||
const id = `entry-${date}`;
|
const id = `entry-${date}`;
|
||||||
defaultEntries.push({
|
const entry = {
|
||||||
id,
|
id,
|
||||||
createdOn: date,
|
createdOn: date,
|
||||||
text: '',
|
text: entryText,
|
||||||
embeds
|
embeds
|
||||||
});
|
};
|
||||||
|
|
||||||
addDefaultClass(domainObject);
|
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
||||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
|
||||||
|
addDefaultClass(domainObject, openmct);
|
||||||
|
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
|
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
|
||||||
if (!domainObject || !selectedSection || !selectedPage) {
|
if (!domainObject || !selectedSection || !selectedPage) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configuration = domainObject.configuration;
|
const configuration = domainObject.configuration;
|
||||||
@ -145,12 +140,12 @@ export function getNotebookEntries(domainObject, selectedSection, selectedPage)
|
|||||||
|
|
||||||
let section = entries[selectedSection.id];
|
let section = entries[selectedSection.id];
|
||||||
if (!section) {
|
if (!section) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let page = entries[selectedSection.id][selectedPage.id];
|
let page = entries[selectedSection.id][selectedPage.id];
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries[selectedSection.id][selectedPage.id];
|
return entries[selectedSection.id][selectedPage.id];
|
||||||
@ -196,14 +191,13 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
|
|||||||
|
|
||||||
delete entries[selectedSection.id][selectedPage.id];
|
delete entries[selectedSection.id][selectedPage.id];
|
||||||
|
|
||||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
mutateObject(openmct, domainObject, 'configuration.entries', entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDefaultClass(domainObject) {
|
export function mutateObject(openmct, object, key, value) {
|
||||||
const classList = domainObject.classList || [];
|
openmct.objects.mutate(object, key, value);
|
||||||
if (classList.includes(DEFAULT_CLASS)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
classList.push(DEFAULT_CLASS);
|
function addDefaultClass(domainObject, openmct) {
|
||||||
|
openmct.status.set(domainObject.identifier, DEFAULT_CLASS);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import * as NotebookEntries from './notebook-entries';
|
import * as NotebookEntries from './notebook-entries';
|
||||||
import { createOpenMct, spyOnBuiltins, resetApplicationState } from 'utils/testing';
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
const notebookStorage = {
|
const notebookStorage = {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
@ -121,7 +121,6 @@ describe('Notebook Entries:', () => {
|
|||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
window.localStorage.setItem('notebook-storage', null);
|
window.localStorage.setItem('notebook-storage', null);
|
||||||
spyOnBuiltins(['mutate'], openmct.objects);
|
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@ -137,24 +136,16 @@ describe('Notebook Entries:', () => {
|
|||||||
expect(entries.length).toEqual(0);
|
expect(entries.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('addNotebookEntry mutates object', () => {
|
it('addNotebookEntry adds entry', (done) => {
|
||||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {
|
||||||
|
|
||||||
expect(openmct.objects.mutate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('addNotebookEntry adds entry', () => {
|
|
||||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
|
||||||
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||||
|
|
||||||
expect(entries.length).toEqual(1);
|
expect(entries.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
unlisten();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getEntryPosById returns valid position', () => {
|
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||||
const entryId = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
|
||||||
const position = NotebookEntries.getEntryPosById(entryId, notebookDomainObject, selectedSection, selectedPage);
|
|
||||||
|
|
||||||
expect(position).toEqual(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getEntryPosById returns valid position', () => {
|
it('getEntryPosById returns valid position', () => {
|
||||||
@ -174,22 +165,13 @@ describe('Notebook Entries:', () => {
|
|||||||
expect(success).toBe(true);
|
expect(success).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteNotebookEntries mutates object', () => {
|
it('deleteNotebookEntries deletes correct page entries', () => {
|
||||||
openmct.objects.mutate.calls.reset();
|
|
||||||
|
|
||||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
|
||||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
|
||||||
|
|
||||||
expect(openmct.objects.mutate).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteNotebookEntries deletes correct entry', () => {
|
|
||||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||||
|
|
||||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
||||||
const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||||
|
|
||||||
expect(afterEntries).toEqual(null);
|
expect(afterEntries).toEqual(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -67,3 +67,24 @@ export function setDefaultNotebookPage(page) {
|
|||||||
notebookStorage.page = page;
|
notebookStorage.page = page;
|
||||||
saveDefaultNotebook(notebookStorage);
|
saveDefaultNotebook(notebookStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateNotebookStorageObject() {
|
||||||
|
const notebookStorage = getDefaultNotebook();
|
||||||
|
|
||||||
|
let valid = false;
|
||||||
|
if (notebookStorage) {
|
||||||
|
Object.entries(notebookStorage).forEach(([key, value]) => {
|
||||||
|
const validKey = key !== undefined && key !== null;
|
||||||
|
const validValue = value !== undefined && value !== null;
|
||||||
|
valid = validKey && validValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
return notebookStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Invalid Notebook object, clearing default notebook storage');
|
||||||
|
|
||||||
|
clearDefaultNotebook();
|
||||||
|
}
|
||||||
|
@ -87,7 +87,8 @@ export default class CouchObjectProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
||||||
if (!this.objectQueue[key].pending) {
|
//Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates.
|
||||||
|
if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) {
|
||||||
this.objectQueue[key].updateRevision(response[REV]);
|
this.objectQueue[key].updateRevision(response[REV]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
|
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
|
||||||
<div class="plot-legend-item"
|
<div class="plot-legend-item"
|
||||||
ng-class="{
|
ng-class="{
|
||||||
'is-missing': series.domainObject.status === 'missing'
|
'is-status--missing': series.domainObject.status === 'missing'
|
||||||
}"
|
}"
|
||||||
ng-repeat="series in series track by $index"
|
ng-repeat="series in series track by $index"
|
||||||
>
|
>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<span class="plot-series-color-swatch"
|
<span class="plot-series-color-swatch"
|
||||||
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
||||||
</span>
|
</span>
|
||||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||||
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
|
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
|
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
|
||||||
@ -95,14 +95,14 @@
|
|||||||
<tr ng-repeat="series in series"
|
<tr ng-repeat="series in series"
|
||||||
class="plot-legend-item"
|
class="plot-legend-item"
|
||||||
ng-class="{
|
ng-class="{
|
||||||
'is-missing': series.domainObject.status === 'missing'
|
'is-status--missing': series.domainObject.status === 'missing'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<td class="plot-series-swatch-and-name">
|
<td class="plot-series-swatch-and-name">
|
||||||
<span class="plot-series-color-swatch"
|
<span class="plot-series-color-swatch"
|
||||||
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
||||||
</span>
|
</span>
|
||||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||||
<span class="plot-series-name">{{ series.get('name') }}</span>
|
<span class="plot-series-name">{{ series.get('name') }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
@ -171,6 +171,7 @@ define([
|
|||||||
* Update yAxis format, values, and label from known series.
|
* Update yAxis format, values, and label from known series.
|
||||||
*/
|
*/
|
||||||
updateFromSeries: function (series) {
|
updateFromSeries: function (series) {
|
||||||
|
this.unset('displayRange');
|
||||||
const plotModel = this.plot.get('domainObject');
|
const plotModel = this.plot.get('domainObject');
|
||||||
const label = _.get(plotModel, 'configuration.yAxis.label');
|
const label = _.get(plotModel, 'configuration.yAxis.label');
|
||||||
const sampleSeries = series.first();
|
const sampleSeries = series.first();
|
||||||
|
@ -117,8 +117,10 @@ define(
|
|||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
ExportImageService.prototype.exportJPG = function (element, filename, className) {
|
ExportImageService.prototype.exportJPG = function (element, filename, className) {
|
||||||
|
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||||
|
|
||||||
return this.renderElement(element, "jpg", className).then(function (img) {
|
return this.renderElement(element, "jpg", className).then(function (img) {
|
||||||
saveAs(img, filename);
|
saveAs(img, processedFilename);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -130,8 +132,10 @@ define(
|
|||||||
* @returns {promise}
|
* @returns {promise}
|
||||||
*/
|
*/
|
||||||
ExportImageService.prototype.exportPNG = function (element, filename, className) {
|
ExportImageService.prototype.exportPNG = function (element, filename, className) {
|
||||||
|
const processedFilename = replaceDotsWithUnderscores(filename);
|
||||||
|
|
||||||
return this.renderElement(element, "png", className).then(function (img) {
|
return this.renderElement(element, "png", className).then(function (img) {
|
||||||
saveAs(img, filename);
|
saveAs(img, processedFilename);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -146,6 +150,12 @@ define(
|
|||||||
return this.renderElement(element, "png", className);
|
return this.renderElement(element, "png", className);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function replaceDotsWithUnderscores(filename) {
|
||||||
|
const regex = /\./gi;
|
||||||
|
|
||||||
|
return filename.replace(regex, '_');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
|
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
|
||||||
* implements the method in browsers that would not otherwise support it.
|
* implements the method in browsers that would not otherwise support it.
|
||||||
|
@ -58,7 +58,9 @@ define([
|
|||||||
'./newFolderAction/plugin',
|
'./newFolderAction/plugin',
|
||||||
'./persistence/couch/plugin',
|
'./persistence/couch/plugin',
|
||||||
'./defaultRootName/plugin',
|
'./defaultRootName/plugin',
|
||||||
'./timeline/plugin'
|
'./timeline/plugin',
|
||||||
|
'./viewDatumAction/plugin',
|
||||||
|
'./interceptors/plugin'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
UTCTimeSystem,
|
UTCTimeSystem,
|
||||||
@ -97,7 +99,9 @@ define([
|
|||||||
NewFolderAction,
|
NewFolderAction,
|
||||||
CouchDBPlugin,
|
CouchDBPlugin,
|
||||||
DefaultRootName,
|
DefaultRootName,
|
||||||
Timeline
|
Timeline,
|
||||||
|
ViewDatumAction,
|
||||||
|
ObjectInterceptors
|
||||||
) {
|
) {
|
||||||
const bundleMap = {
|
const bundleMap = {
|
||||||
LocalStorage: 'platform/persistence/local',
|
LocalStorage: 'platform/persistence/local',
|
||||||
@ -191,6 +195,8 @@ define([
|
|||||||
plugins.ISOTimeFormat = ISOTimeFormat.default;
|
plugins.ISOTimeFormat = ISOTimeFormat.default;
|
||||||
plugins.DefaultRootName = DefaultRootName.default;
|
plugins.DefaultRootName = DefaultRootName.default;
|
||||||
plugins.Timeline = Timeline.default;
|
plugins.Timeline = Timeline.default;
|
||||||
|
plugins.ViewDatumAction = ViewDatumAction.default;
|
||||||
|
plugins.ObjectInterceptors = ObjectInterceptors.default;
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
});
|
});
|
||||||
|
@ -25,6 +25,8 @@ export default class RemoveAction {
|
|||||||
this.key = 'remove';
|
this.key = 'remove';
|
||||||
this.description = 'Remove this object from its containing object.';
|
this.description = 'Remove this object from its containing object.';
|
||||||
this.cssClass = "icon-trash";
|
this.cssClass = "icon-trash";
|
||||||
|
this.group = "action";
|
||||||
|
this.priority = 1;
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
}
|
}
|
||||||
@ -103,6 +105,16 @@ export default class RemoveAction {
|
|||||||
let parentType = parent && this.openmct.types.get(parent.type);
|
let parentType = parent && this.openmct.types.get(parent.type);
|
||||||
let child = objectPath[0];
|
let child = objectPath[0];
|
||||||
let locked = child.locked ? child.locked : parent && parent.locked;
|
let locked = child.locked ? child.locked : parent && parent.locked;
|
||||||
|
let isEditing = this.openmct.editor.isEditing();
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
let currentItemInView = this.openmct.router.path[0];
|
||||||
|
let domainObject = objectPath[0];
|
||||||
|
|
||||||
|
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction";
|
|||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return function (openmct) {
|
return function (openmct) {
|
||||||
openmct.contextMenu.registerAction(new RemoveAction(openmct));
|
openmct.actions.register(new RemoveAction(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(tab, index) in tabsList"
|
v-for="(tab, index) in tabsList"
|
||||||
:key="index"
|
:key="tab.keyString"
|
||||||
class="c-tab c-tabs-view__tab"
|
class="c-tab c-tabs-view__tab"
|
||||||
:class="{
|
:class="{
|
||||||
'is-current': isCurrent(tab)
|
'is-current': isCurrent(tab)
|
||||||
@ -29,13 +29,13 @@
|
|||||||
@click="showTab(tab, index)"
|
@click="showTab(tab, index)"
|
||||||
>
|
>
|
||||||
<div class="c-tabs-view__tab__label c-object-label"
|
<div class="c-tabs-view__tab__label c-object-label"
|
||||||
:class="{'is-missing': tab.domainObject.status === 'missing'}"
|
:class="[tab.status ? `is-status--${tab.status}` : '']"
|
||||||
>
|
>
|
||||||
<div class="c-object-label__type-icon"
|
<div class="c-object-label__type-icon"
|
||||||
:class="tab.type.definition.cssClass"
|
:class="tab.type.definition.cssClass"
|
||||||
>
|
>
|
||||||
<span class="is-missing__indicator"
|
<span class="is-status__indicator"
|
||||||
title="This item is missing"
|
:title="`This item is ${tab.status}`"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
|
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
|
||||||
@ -47,8 +47,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(tab, index) in tabsList"
|
v-for="tab in tabsList"
|
||||||
:key="index"
|
:key="tab.keyString"
|
||||||
class="c-tabs-view__object-holder"
|
class="c-tabs-view__object-holder"
|
||||||
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
|
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
|
||||||
>
|
>
|
||||||
@ -56,6 +56,7 @@
|
|||||||
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
|
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
|
||||||
class="c-tabs-view__object"
|
class="c-tabs-view__object"
|
||||||
:object="tab.domainObject"
|
:object="tab.domainObject"
|
||||||
|
:object-path="tab.objectPath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -78,7 +79,7 @@ const unknownObjectType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject', 'composition'],
|
inject: ['openmct', 'domainObject', 'composition', 'objectPath'],
|
||||||
components: {
|
components: {
|
||||||
ObjectView
|
ObjectView
|
||||||
},
|
},
|
||||||
@ -139,6 +140,10 @@ export default {
|
|||||||
this.composition.off('remove', this.removeItem);
|
this.composition.off('remove', this.removeItem);
|
||||||
this.composition.off('reorder', this.onReorder);
|
this.composition.off('reorder', this.onReorder);
|
||||||
|
|
||||||
|
this.tabsList.forEach(tab => {
|
||||||
|
tab.statusUnsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
this.clearCurrentTabIndexFromURL();
|
this.clearCurrentTabIndexFromURL();
|
||||||
|
|
||||||
@ -192,10 +197,19 @@ export default {
|
|||||||
},
|
},
|
||||||
addItem(domainObject) {
|
addItem(domainObject) {
|
||||||
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
|
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
|
||||||
|
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
|
let status = this.openmct.status.get(domainObject.identifier);
|
||||||
|
let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => {
|
||||||
|
this.updateStatus(keyString, updatedStatus);
|
||||||
|
});
|
||||||
|
let objectPath = [domainObject].concat(this.objectPath.slice());
|
||||||
let tabItem = {
|
let tabItem = {
|
||||||
domainObject,
|
domainObject,
|
||||||
type: type,
|
status,
|
||||||
key: this.openmct.objects.makeKeyString(domainObject.identifier)
|
statusUnsubscribe,
|
||||||
|
objectPath,
|
||||||
|
type,
|
||||||
|
keyString
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tabsList.push(tabItem);
|
this.tabsList.push(tabItem);
|
||||||
@ -211,10 +225,12 @@ export default {
|
|||||||
},
|
},
|
||||||
removeItem(identifier) {
|
removeItem(identifier) {
|
||||||
let pos = this.tabsList.findIndex(tab =>
|
let pos = this.tabsList.findIndex(tab =>
|
||||||
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.key === identifier.key
|
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.keyString === identifier.keyString
|
||||||
);
|
);
|
||||||
let tabToBeRemoved = this.tabsList[pos];
|
let tabToBeRemoved = this.tabsList[pos];
|
||||||
|
|
||||||
|
tabToBeRemoved.statusUnsubscribe();
|
||||||
|
|
||||||
this.tabsList.splice(pos, 1);
|
this.tabsList.splice(pos, 1);
|
||||||
|
|
||||||
if (this.isCurrent(tabToBeRemoved)) {
|
if (this.isCurrent(tabToBeRemoved)) {
|
||||||
@ -252,7 +268,7 @@ export default {
|
|||||||
this.allowDrop = false;
|
this.allowDrop = false;
|
||||||
},
|
},
|
||||||
isCurrent(tab) {
|
isCurrent(tab) {
|
||||||
return this.currentTab.key === tab.key;
|
return this.currentTab.keyString === tab.keyString;
|
||||||
},
|
},
|
||||||
updateInternalDomainObject(domainObject) {
|
updateInternalDomainObject(domainObject) {
|
||||||
this.internalDomainObject = domainObject;
|
this.internalDomainObject = domainObject;
|
||||||
@ -270,6 +286,16 @@ export default {
|
|||||||
},
|
},
|
||||||
clearCurrentTabIndexFromURL() {
|
clearCurrentTabIndexFromURL() {
|
||||||
deleteSearchParam(this.searchTabKey);
|
deleteSearchParam(this.searchTabKey);
|
||||||
|
},
|
||||||
|
updateStatus(keyString, status) {
|
||||||
|
let tabPos = this.tabsList.findIndex((tab) => {
|
||||||
|
return tab.keyString === keyString;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tabPos !== -1) {
|
||||||
|
let tab = this.tabsList[tabPos];
|
||||||
|
this.$set(tab, 'status', status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -38,7 +38,7 @@ define([
|
|||||||
canEdit: function (domainObject) {
|
canEdit: function (domainObject) {
|
||||||
return domainObject.type === 'tabs';
|
return domainObject.type === 'tabs';
|
||||||
},
|
},
|
||||||
view: function (domainObject) {
|
view: function (domainObject, objectPath) {
|
||||||
let component;
|
let component;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -56,6 +56,7 @@ define([
|
|||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject,
|
domainObject,
|
||||||
|
objectPath,
|
||||||
composition: openmct.composition.get(domainObject)
|
composition: openmct.composition.get(domainObject)
|
||||||
},
|
},
|
||||||
template: '<tabs-component :isEditing="isEditing"></tabs-component>'
|
template: '<tabs-component :isEditing="isEditing"></tabs-component>'
|
||||||
|
@ -180,7 +180,6 @@ define([
|
|||||||
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
|
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
|
||||||
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||||
this.boundedRows.add(telemetryRows);
|
this.boundedRows.add(telemetryRows);
|
||||||
this.emit('historical-rows-processed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +26,7 @@ define([], function () {
|
|||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
|
|
||||||
this.datum = createNormalizedDatum(datum, columns);
|
this.datum = createNormalizedDatum(datum, columns);
|
||||||
|
this.fullDatum = datum;
|
||||||
this.limitEvaluator = limitEvaluator;
|
this.limitEvaluator = limitEvaluator;
|
||||||
this.objectKeyString = objectKeyString;
|
this.objectKeyString = objectKeyString;
|
||||||
}
|
}
|
||||||
@ -87,7 +88,7 @@ define([], function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getContextMenuActions() {
|
getContextMenuActions() {
|
||||||
return [];
|
return ['viewDatumAction'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,15 +54,13 @@ define([
|
|||||||
view(domainObject, objectPath) {
|
view(domainObject, objectPath) {
|
||||||
let table = new TelemetryTable(domainObject, openmct);
|
let table = new TelemetryTable(domainObject, openmct);
|
||||||
let component;
|
let component;
|
||||||
|
|
||||||
let markingProp = {
|
let markingProp = {
|
||||||
enable: true,
|
enable: true,
|
||||||
useAlternateControlBar: false,
|
useAlternateControlBar: false,
|
||||||
rowName: '',
|
rowName: '',
|
||||||
rowNamePlural: ''
|
rowNamePlural: ''
|
||||||
};
|
};
|
||||||
|
const view = {
|
||||||
return {
|
|
||||||
show: function (element, editMode) {
|
show: function (element, editMode) {
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
el: element,
|
el: element,
|
||||||
@ -72,7 +70,8 @@ define([
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isEditing: editMode,
|
isEditing: editMode,
|
||||||
markingProp
|
markingProp,
|
||||||
|
view
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
@ -80,7 +79,7 @@ define([
|
|||||||
table,
|
table,
|
||||||
objectPath
|
objectPath
|
||||||
},
|
},
|
||||||
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>'
|
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onEditModeChange(editMode) {
|
onEditModeChange(editMode) {
|
||||||
@ -89,11 +88,22 @@ define([
|
|||||||
onClearData() {
|
onClearData() {
|
||||||
table.clearData();
|
table.clearData();
|
||||||
},
|
},
|
||||||
|
getViewContext() {
|
||||||
|
if (component) {
|
||||||
|
return component.$refs.tableComponent.getViewContext();
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'telemetry-table'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
destroy: function (element) {
|
destroy: function (element) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
component = undefined;
|
component = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return view;
|
||||||
},
|
},
|
||||||
priority() {
|
priority() {
|
||||||
return 1;
|
return 1;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user