Compare commits
10 Commits
drawing-ob
...
testing/ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e67b3bbd | ||
|
|
9da750c3bb | ||
|
|
176226ddef | ||
|
|
acea18fa70 | ||
|
|
d1656f8561 | ||
|
|
87751e882c | ||
|
|
dff393a714 | ||
|
|
fd9c9aee03 | ||
|
|
59bf981fb0 | ||
|
|
4bbdac759f |
@@ -21,32 +21,24 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/actions/MoveAction",
|
||||
"./src/actions/CopyAction",
|
||||
"./src/actions/LinkAction",
|
||||
"./src/actions/SetPrimaryLocationAction",
|
||||
"./src/services/LocatingCreationDecorator",
|
||||
"./src/services/LocatingObjectDecorator",
|
||||
"./src/policies/CopyPolicy",
|
||||
"./src/policies/CrossSpacePolicy",
|
||||
"./src/policies/MovePolicy",
|
||||
"./src/capabilities/LocationCapability",
|
||||
"./src/services/MoveService",
|
||||
"./src/services/LinkService",
|
||||
"./src/services/CopyService",
|
||||
"./src/services/LocationService"
|
||||
], function (
|
||||
MoveAction,
|
||||
CopyAction,
|
||||
LinkAction,
|
||||
SetPrimaryLocationAction,
|
||||
LocatingCreationDecorator,
|
||||
LocatingObjectDecorator,
|
||||
CopyPolicy,
|
||||
CrossSpacePolicy,
|
||||
MovePolicy,
|
||||
LocationCapability,
|
||||
MoveService,
|
||||
LinkService,
|
||||
CopyService,
|
||||
LocationService
|
||||
@@ -60,39 +52,6 @@ define([
|
||||
"configuration": {},
|
||||
"extensions": {
|
||||
"actions": [
|
||||
{
|
||||
"key": "move",
|
||||
"name": "Move",
|
||||
"description": "Move object to another location.",
|
||||
"cssClass": "icon-move",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 9,
|
||||
"implementation": MoveAction,
|
||||
"depends": [
|
||||
"policyService",
|
||||
"locationService",
|
||||
"moveService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "copy",
|
||||
"name": "Duplicate",
|
||||
"description": "Duplicate object to another location.",
|
||||
"cssClass": "icon-duplicate",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 8,
|
||||
"implementation": CopyAction,
|
||||
"depends": [
|
||||
"$log",
|
||||
"policyService",
|
||||
"locationService",
|
||||
"copyService",
|
||||
"dialogService",
|
||||
"notificationService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "link",
|
||||
"name": "Create Link",
|
||||
@@ -141,10 +100,6 @@ define([
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": CopyPolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": MovePolicy
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
@@ -160,17 +115,6 @@ define([
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "moveService",
|
||||
"name": "Move Service",
|
||||
"description": "Provides a service for moving objects",
|
||||
"implementation": MoveService,
|
||||
"depends": [
|
||||
"openmct",
|
||||
"linkService",
|
||||
"$q"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "linkService",
|
||||
"name": "Link Service",
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['./AbstractComposeAction', './CancelError'],
|
||||
function (AbstractComposeAction, CancelError) {
|
||||
|
||||
/**
|
||||
* The CopyAction is available from context menus and allows a user to
|
||||
* deep copy an object to another location of their choosing.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @constructor
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function CopyAction(
|
||||
$log,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
) {
|
||||
this.dialog = undefined;
|
||||
this.notification = undefined;
|
||||
this.dialogService = dialogService;
|
||||
this.notificationService = notificationService;
|
||||
this.$log = $log;
|
||||
//Extend the behaviour of the Abstract Compose Action
|
||||
AbstractComposeAction.call(
|
||||
this,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
context,
|
||||
"Duplicate",
|
||||
"To a Location"
|
||||
);
|
||||
}
|
||||
|
||||
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
|
||||
|
||||
/**
|
||||
* Updates user about progress of copy. Should not be invoked by
|
||||
* client code under any circumstances.
|
||||
*
|
||||
* @private
|
||||
* @param phase
|
||||
* @param totalObjects
|
||||
* @param processed
|
||||
*/
|
||||
CopyAction.prototype.progress = function (phase, totalObjects, processed) {
|
||||
/*
|
||||
Copy has two distinct phases. In the first phase a copy plan is
|
||||
made in memory. During this phase of execution, the user is
|
||||
shown a blocking 'modal' dialog.
|
||||
|
||||
In the second phase, the copying is taking place, and the user
|
||||
is shown non-invasive banner notifications at the bottom of the screen.
|
||||
*/
|
||||
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
|
||||
this.dialog = this.dialogService.showBlockingMessage({
|
||||
title: "Preparing to copy objects",
|
||||
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
|
||||
unknownProgress: true,
|
||||
severity: "info"
|
||||
});
|
||||
} else if (phase.toLowerCase() === "copying") {
|
||||
if (this.dialog) {
|
||||
this.dialog.dismiss();
|
||||
}
|
||||
|
||||
if (!this.notification) {
|
||||
this.notification = this.notificationService
|
||||
.notify({
|
||||
title: "Copying objects",
|
||||
unknownProgress: false,
|
||||
severity: "info"
|
||||
});
|
||||
}
|
||||
|
||||
this.notification.model.progress = (processed / totalObjects) * 100;
|
||||
this.notification.model.title = ["Copied ", processed, "of ",
|
||||
totalObjects, "objects"].join(" ");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the CopyAction. The CopyAction uses the default behaviour of
|
||||
* the AbstractComposeAction, but extends it to support notification
|
||||
* updates of progress on copy.
|
||||
*/
|
||||
CopyAction.prototype.perform = function () {
|
||||
var self = this;
|
||||
|
||||
function success(domainObject) {
|
||||
var domainObjectName = domainObject.model.name;
|
||||
|
||||
self.notification.dismiss();
|
||||
self.notificationService.info(domainObjectName + " copied successfully.");
|
||||
}
|
||||
|
||||
function error(errorDetails) {
|
||||
// No need to notify user of their own cancellation
|
||||
if (errorDetails instanceof CancelError) {
|
||||
return;
|
||||
}
|
||||
|
||||
var errorDialog,
|
||||
errorMessage = {
|
||||
title: "Error copying objects.",
|
||||
severity: "error",
|
||||
hint: errorDetails.message,
|
||||
minimized: true, // want the notification to be minimized initially (don't show banner)
|
||||
options: [{
|
||||
label: "OK",
|
||||
callback: function () {
|
||||
errorDialog.dismiss();
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
self.dialog.dismiss();
|
||||
if (self.notification) {
|
||||
self.notification.dismiss(); // Clear the progress notification
|
||||
}
|
||||
|
||||
self.$log.error("Error copying objects. ", errorDetails);
|
||||
//Show a minimized notification of error for posterity
|
||||
self.notificationService.notify(errorMessage);
|
||||
//Display a blocking message
|
||||
errorDialog = self.dialogService.showBlockingMessage(errorMessage);
|
||||
|
||||
}
|
||||
|
||||
function notification(details) {
|
||||
self.progress(details.phase, details.totalObjects, details.processed);
|
||||
}
|
||||
|
||||
return AbstractComposeAction.prototype.perform.call(this)
|
||||
.then(success, error, notification);
|
||||
};
|
||||
|
||||
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
|
||||
|
||||
return CopyAction;
|
||||
}
|
||||
);
|
||||
@@ -1,104 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
function () {
|
||||
/**
|
||||
* MoveService provides an interface for moving objects from one
|
||||
* location to another. It also provides a method for determining if
|
||||
* an object can be copied to a specific location.
|
||||
* @constructor
|
||||
* @memberof platform/entanglement
|
||||
* @implements {platform/entanglement.AbstractComposeService}
|
||||
*/
|
||||
function MoveService(openmct, linkService) {
|
||||
this.openmct = openmct;
|
||||
this.linkService = linkService;
|
||||
}
|
||||
|
||||
MoveService.prototype.validate = function (object, parentCandidate) {
|
||||
var currentParent = object
|
||||
.getCapability('context')
|
||||
.getParent();
|
||||
|
||||
if (!parentCandidate || !parentCandidate.getId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.getId() === currentParent.getId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.getId() === object.getId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.openmct.composition.checkPolicy(
|
||||
parentCandidate.useCapability('adapter'),
|
||||
object.useCapability('adapter')
|
||||
);
|
||||
};
|
||||
|
||||
MoveService.prototype.perform = function (object, parentObject) {
|
||||
function relocate(objectInNewContext) {
|
||||
var newLocationCapability = objectInNewContext
|
||||
.getCapability('location'),
|
||||
oldLocationCapability = object
|
||||
.getCapability('location');
|
||||
|
||||
if (!newLocationCapability
|
||||
|| !oldLocationCapability) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldLocationCapability.isOriginal()) {
|
||||
return newLocationCapability.setPrimaryLocation(
|
||||
newLocationCapability
|
||||
.getContextualLocation()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.validate(object, parentObject)) {
|
||||
throw new Error(
|
||||
"Tried to move objects without validating first."
|
||||
);
|
||||
}
|
||||
|
||||
return this.linkService
|
||||
.perform(object, parentObject)
|
||||
.then(relocate)
|
||||
.then(function () {
|
||||
return object
|
||||
.getCapability('action')
|
||||
.perform('remove', true);
|
||||
});
|
||||
};
|
||||
|
||||
return MoveService;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/actions/CopyAction',
|
||||
'../services/MockCopyService',
|
||||
'../DomainObjectFactory'
|
||||
],
|
||||
function (CopyAction, MockCopyService, domainObjectFactory) {
|
||||
|
||||
describe("Copy Action", function () {
|
||||
|
||||
var copyAction,
|
||||
policyService,
|
||||
locationService,
|
||||
locationServicePromise,
|
||||
copyService,
|
||||
context,
|
||||
selectedObject,
|
||||
selectedObjectContextCapability,
|
||||
currentParent,
|
||||
newParent,
|
||||
notificationService,
|
||||
notification,
|
||||
dialogService,
|
||||
mockDialog,
|
||||
mockLog,
|
||||
abstractComposePromise,
|
||||
domainObject = {model: {name: "mockObject"}},
|
||||
progress = {
|
||||
phase: "copying",
|
||||
totalObjects: 10,
|
||||
processed: 1
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
policyService = jasmine.createSpyObj(
|
||||
'policyService',
|
||||
['allow']
|
||||
);
|
||||
policyService.allow.and.returnValue(true);
|
||||
|
||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
||||
'selectedObjectContextCapability',
|
||||
[
|
||||
'getParent'
|
||||
]
|
||||
);
|
||||
|
||||
selectedObject = domainObjectFactory({
|
||||
name: 'selectedObject',
|
||||
model: {
|
||||
name: 'selectedObject'
|
||||
},
|
||||
capabilities: {
|
||||
context: selectedObjectContextCapability
|
||||
}
|
||||
});
|
||||
|
||||
currentParent = domainObjectFactory({
|
||||
name: 'currentParent'
|
||||
});
|
||||
|
||||
selectedObjectContextCapability
|
||||
.getParent
|
||||
.and.returnValue(currentParent);
|
||||
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent'
|
||||
});
|
||||
|
||||
locationService = jasmine.createSpyObj(
|
||||
'locationService',
|
||||
[
|
||||
'getLocationFromUser'
|
||||
]
|
||||
);
|
||||
|
||||
locationServicePromise = jasmine.createSpyObj(
|
||||
'locationServicePromise',
|
||||
[
|
||||
'then'
|
||||
]
|
||||
);
|
||||
|
||||
abstractComposePromise = jasmine.createSpyObj(
|
||||
'abstractComposePromise',
|
||||
[
|
||||
'then'
|
||||
]
|
||||
);
|
||||
|
||||
abstractComposePromise.then.and.callFake(function (success, error, notify) {
|
||||
notify(progress);
|
||||
success(domainObject);
|
||||
});
|
||||
|
||||
locationServicePromise.then.and.callFake(function (callback) {
|
||||
callback(newParent);
|
||||
|
||||
return abstractComposePromise;
|
||||
});
|
||||
|
||||
locationService
|
||||
.getLocationFromUser
|
||||
.and.returnValue(locationServicePromise);
|
||||
|
||||
dialogService = jasmine.createSpyObj('dialogService',
|
||||
['showBlockingMessage']
|
||||
);
|
||||
|
||||
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
|
||||
dialogService.showBlockingMessage.and.returnValue(mockDialog);
|
||||
|
||||
notification = jasmine.createSpyObj('notification',
|
||||
['dismiss', 'model']
|
||||
);
|
||||
|
||||
notificationService = jasmine.createSpyObj('notificationService',
|
||||
['notify', 'info']
|
||||
);
|
||||
|
||||
notificationService.notify.and.returnValue(notification);
|
||||
|
||||
mockLog = jasmine.createSpyObj('log', ['error']);
|
||||
|
||||
copyService = new MockCopyService();
|
||||
});
|
||||
|
||||
describe("with context from context-action", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
domainObject: selectedObject
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(copyAction).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when performed it", function () {
|
||||
beforeEach(function () {
|
||||
spyOn(copyAction, 'progress').and.callThrough();
|
||||
copyAction.perform();
|
||||
});
|
||||
|
||||
it("prompts for location", function () {
|
||||
expect(locationService.getLocationFromUser)
|
||||
.toHaveBeenCalledWith(
|
||||
"Duplicate selectedObject To a Location",
|
||||
"Duplicate To",
|
||||
jasmine.any(Function),
|
||||
currentParent
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for location and handles cancellation by user", function () {
|
||||
expect(locationServicePromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("copies object to selected location", function () {
|
||||
locationServicePromise
|
||||
.then
|
||||
.calls.mostRecent()
|
||||
.args[0](newParent);
|
||||
|
||||
expect(copyService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
|
||||
it("notifies the user of progress", function () {
|
||||
expect(notificationService.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("notifies the user with name of object copied", function () {
|
||||
expect(notificationService.info)
|
||||
.toHaveBeenCalledWith("mockObject copied successfully.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with context from drag-drop", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
selectedObject: selectedObject,
|
||||
domainObject: newParent
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(copyAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("performs copy immediately", function () {
|
||||
copyAction.perform();
|
||||
expect(copyService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,178 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/actions/MoveAction',
|
||||
'../services/MockMoveService',
|
||||
'../DomainObjectFactory'
|
||||
],
|
||||
function (MoveAction, MockMoveService, domainObjectFactory) {
|
||||
|
||||
describe("Move Action", function () {
|
||||
|
||||
var moveAction,
|
||||
policyService,
|
||||
locationService,
|
||||
locationServicePromise,
|
||||
moveService,
|
||||
context,
|
||||
selectedObject,
|
||||
selectedObjectContextCapability,
|
||||
currentParent,
|
||||
newParent;
|
||||
|
||||
beforeEach(function () {
|
||||
policyService = jasmine.createSpyObj(
|
||||
'policyService',
|
||||
['allow']
|
||||
);
|
||||
policyService.allow.and.returnValue(true);
|
||||
|
||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
||||
'selectedObjectContextCapability',
|
||||
[
|
||||
'getParent'
|
||||
]
|
||||
);
|
||||
|
||||
selectedObject = domainObjectFactory({
|
||||
name: 'selectedObject',
|
||||
model: {
|
||||
name: 'selectedObject'
|
||||
},
|
||||
capabilities: {
|
||||
context: selectedObjectContextCapability
|
||||
}
|
||||
});
|
||||
|
||||
currentParent = domainObjectFactory({
|
||||
name: 'currentParent'
|
||||
});
|
||||
|
||||
selectedObjectContextCapability
|
||||
.getParent
|
||||
.and.returnValue(currentParent);
|
||||
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent'
|
||||
});
|
||||
|
||||
locationService = jasmine.createSpyObj(
|
||||
'locationService',
|
||||
[
|
||||
'getLocationFromUser'
|
||||
]
|
||||
);
|
||||
|
||||
locationServicePromise = jasmine.createSpyObj(
|
||||
'locationServicePromise',
|
||||
[
|
||||
'then'
|
||||
]
|
||||
);
|
||||
|
||||
locationService
|
||||
.getLocationFromUser
|
||||
.and.returnValue(locationServicePromise);
|
||||
|
||||
moveService = new MockMoveService();
|
||||
});
|
||||
|
||||
describe("with context from context-action", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
domainObject: selectedObject
|
||||
};
|
||||
|
||||
moveAction = new MoveAction(
|
||||
policyService,
|
||||
locationService,
|
||||
moveService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(moveAction).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when performed it", function () {
|
||||
beforeEach(function () {
|
||||
moveAction.perform();
|
||||
});
|
||||
|
||||
it("prompts for location", function () {
|
||||
expect(locationService.getLocationFromUser)
|
||||
.toHaveBeenCalledWith(
|
||||
"Move selectedObject To a New Location",
|
||||
"Move To",
|
||||
jasmine.any(Function),
|
||||
currentParent
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for location and handles cancellation by user", function () {
|
||||
expect(locationServicePromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("moves object to selected location", function () {
|
||||
locationServicePromise
|
||||
.then
|
||||
.calls.mostRecent()
|
||||
.args[0](newParent);
|
||||
|
||||
expect(moveService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with context from drag-drop", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
selectedObject: selectedObject,
|
||||
domainObject: newParent
|
||||
};
|
||||
|
||||
moveAction = new MoveAction(
|
||||
policyService,
|
||||
locationService,
|
||||
moveService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(moveAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("performs move immediately", function () {
|
||||
moveAction.perform();
|
||||
expect(moveService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,124 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'../../src/policies/MovePolicy',
|
||||
'../DomainObjectFactory'
|
||||
], function (MovePolicy, domainObjectFactory) {
|
||||
|
||||
describe("MovePolicy", function () {
|
||||
var testMetadata,
|
||||
testContext,
|
||||
mockDomainObject,
|
||||
mockParent,
|
||||
mockParentType,
|
||||
mockType,
|
||||
mockAction,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
var mockContextCapability =
|
||||
jasmine.createSpyObj('context', ['getParent']);
|
||||
|
||||
mockType =
|
||||
jasmine.createSpyObj('type', ['hasFeature']);
|
||||
mockParentType =
|
||||
jasmine.createSpyObj('parent-type', ['hasFeature']);
|
||||
|
||||
testMetadata = {};
|
||||
|
||||
mockDomainObject = domainObjectFactory({
|
||||
capabilities: {
|
||||
context: mockContextCapability,
|
||||
type: mockType
|
||||
}
|
||||
});
|
||||
mockParent = domainObjectFactory({
|
||||
capabilities: {
|
||||
type: mockParentType
|
||||
}
|
||||
});
|
||||
|
||||
mockContextCapability.getParent.and.returnValue(mockParent);
|
||||
|
||||
mockType.hasFeature.and.callFake(function (feature) {
|
||||
return feature === 'creation';
|
||||
});
|
||||
mockParentType.hasFeature.and.callFake(function (feature) {
|
||||
return feature === 'creation';
|
||||
});
|
||||
|
||||
mockAction = jasmine.createSpyObj('action', ['getMetadata']);
|
||||
mockAction.getMetadata.and.returnValue(testMetadata);
|
||||
|
||||
testContext = { domainObject: mockDomainObject };
|
||||
|
||||
policy = new MovePolicy();
|
||||
});
|
||||
|
||||
describe("for move actions", function () {
|
||||
beforeEach(function () {
|
||||
testMetadata.key = 'move';
|
||||
});
|
||||
|
||||
describe("when an object is non-modifiable", function () {
|
||||
beforeEach(function () {
|
||||
mockType.hasFeature.and.returnValue(false);
|
||||
});
|
||||
|
||||
it("disallows the action", function () {
|
||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a parent is non-modifiable", function () {
|
||||
beforeEach(function () {
|
||||
mockParentType.hasFeature.and.returnValue(false);
|
||||
});
|
||||
|
||||
it("disallows the action", function () {
|
||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when an object and its parent are modifiable", function () {
|
||||
it("allows the action", function () {
|
||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for other actions", function () {
|
||||
beforeEach(function () {
|
||||
testMetadata.key = 'foo';
|
||||
});
|
||||
|
||||
it("simply allows the action", function () {
|
||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
||||
mockType.hasFeature.and.returnValue(false);
|
||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
||||
mockParentType.hasFeature.and.returnValue(false);
|
||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
function () {
|
||||
|
||||
/**
|
||||
* MockMoveService provides the same interface as the moveService,
|
||||
* returning promises where it would normally do so. At it's core,
|
||||
* it is a jasmine spy object, but it also tracks the promises it
|
||||
* returns and provides shortcut methods for resolving those promises
|
||||
* synchronously.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```javascript
|
||||
* var moveService = new MockMoveService();
|
||||
*
|
||||
* // validate is a standard jasmine spy.
|
||||
* moveService.validate.and.returnValue(true);
|
||||
* var isValid = moveService.validate(object, parentCandidate);
|
||||
* expect(isValid).toBe(true);
|
||||
*
|
||||
* // perform returns promises and tracks them.
|
||||
* var whenCopied = jasmine.createSpy('whenCopied');
|
||||
* moveService.perform(object, parentObject).then(whenCopied);
|
||||
* expect(whenCopied).not.toHaveBeenCalled();
|
||||
* moveService.perform.calls.mostRecent().resolve('someArg');
|
||||
* expect(whenCopied).toHaveBeenCalledWith('someArg');
|
||||
* ```
|
||||
*/
|
||||
function MockMoveService() {
|
||||
// track most recent call of a function,
|
||||
// perform automatically returns
|
||||
var mockMoveService = jasmine.createSpyObj(
|
||||
'MockMoveService',
|
||||
[
|
||||
'validate',
|
||||
'perform'
|
||||
]
|
||||
);
|
||||
|
||||
mockMoveService.perform.and.callFake(() => {
|
||||
var performPromise,
|
||||
callExtensions,
|
||||
spy;
|
||||
|
||||
performPromise = jasmine.createSpyObj(
|
||||
'performPromise',
|
||||
['then']
|
||||
);
|
||||
|
||||
callExtensions = {
|
||||
promise: performPromise,
|
||||
resolve: function (resolveWith) {
|
||||
performPromise.then.calls.all().forEach(function (call) {
|
||||
call.args[0](resolveWith);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
spy = mockMoveService.perform;
|
||||
|
||||
Object.keys(callExtensions).forEach(function (key) {
|
||||
spy.calls.mostRecent()[key] = callExtensions[key];
|
||||
spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key];
|
||||
});
|
||||
|
||||
return performPromise;
|
||||
});
|
||||
|
||||
return mockMoveService;
|
||||
}
|
||||
|
||||
return MockMoveService;
|
||||
}
|
||||
);
|
||||
@@ -1,260 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/services/MoveService',
|
||||
'../services/MockLinkService',
|
||||
'../DomainObjectFactory',
|
||||
'../ControlledPromise'
|
||||
],
|
||||
function (
|
||||
MoveService,
|
||||
MockLinkService,
|
||||
domainObjectFactory,
|
||||
ControlledPromise
|
||||
) {
|
||||
|
||||
xdescribe("MoveService", function () {
|
||||
|
||||
var moveService,
|
||||
policyService,
|
||||
object,
|
||||
objectContextCapability,
|
||||
currentParent,
|
||||
parentCandidate,
|
||||
linkService;
|
||||
|
||||
beforeEach(function () {
|
||||
objectContextCapability = jasmine.createSpyObj(
|
||||
'objectContextCapability',
|
||||
[
|
||||
'getParent'
|
||||
]
|
||||
);
|
||||
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
id: 'a',
|
||||
capabilities: {
|
||||
context: objectContextCapability,
|
||||
type: { type: 'object' }
|
||||
}
|
||||
});
|
||||
|
||||
currentParent = domainObjectFactory({
|
||||
name: 'currentParent',
|
||||
id: 'b'
|
||||
});
|
||||
|
||||
objectContextCapability.getParent.and.returnValue(currentParent);
|
||||
|
||||
parentCandidate = domainObjectFactory({
|
||||
name: 'parentCandidate',
|
||||
model: { composition: [] },
|
||||
id: 'c',
|
||||
capabilities: {
|
||||
type: { type: 'parentCandidate' }
|
||||
}
|
||||
});
|
||||
policyService = jasmine.createSpyObj(
|
||||
'policyService',
|
||||
['allow']
|
||||
);
|
||||
linkService = new MockLinkService();
|
||||
policyService.allow.and.returnValue(true);
|
||||
moveService = new MoveService(policyService, linkService);
|
||||
});
|
||||
|
||||
describe("validate", function () {
|
||||
var validate;
|
||||
|
||||
beforeEach(function () {
|
||||
validate = function () {
|
||||
return moveService.validate(object, parentCandidate);
|
||||
};
|
||||
});
|
||||
|
||||
it("does not allow an invalid parent", function () {
|
||||
parentCandidate = undefined;
|
||||
expect(validate()).toBe(false);
|
||||
parentCandidate = {};
|
||||
expect(validate()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not allow moving to current parent", function () {
|
||||
parentCandidate.id = currentParent.id = 'xyz';
|
||||
expect(validate()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not allow moving to self", function () {
|
||||
object.id = parentCandidate.id = 'xyz';
|
||||
expect(validate()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not allow moving to the same location", function () {
|
||||
object.id = 'abc';
|
||||
parentCandidate.model.composition = ['abc'];
|
||||
expect(validate()).toBe(false);
|
||||
});
|
||||
|
||||
describe("defers to policyService", function () {
|
||||
|
||||
it("calls policy service with correct args", function () {
|
||||
validate();
|
||||
expect(policyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
||||
it("and returns false", function () {
|
||||
policyService.allow.and.returnValue(false);
|
||||
expect(validate()).toBe(false);
|
||||
});
|
||||
|
||||
it("and returns true", function () {
|
||||
policyService.allow.and.returnValue(true);
|
||||
expect(validate()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("perform", function () {
|
||||
|
||||
var actionCapability,
|
||||
locationCapability,
|
||||
locationPromise,
|
||||
newParent,
|
||||
moveResult;
|
||||
|
||||
beforeEach(function () {
|
||||
newParent = parentCandidate;
|
||||
|
||||
actionCapability = jasmine.createSpyObj(
|
||||
'actionCapability',
|
||||
['perform']
|
||||
);
|
||||
|
||||
locationCapability = jasmine.createSpyObj(
|
||||
'locationCapability',
|
||||
[
|
||||
'isOriginal',
|
||||
'setPrimaryLocation',
|
||||
'getContextualLocation'
|
||||
]
|
||||
);
|
||||
|
||||
locationPromise = new ControlledPromise();
|
||||
locationCapability.setPrimaryLocation
|
||||
.and.returnValue(locationPromise);
|
||||
|
||||
object = domainObjectFactory({
|
||||
name: 'object',
|
||||
capabilities: {
|
||||
action: actionCapability,
|
||||
location: locationCapability,
|
||||
context: objectContextCapability,
|
||||
type: { type: 'object' }
|
||||
}
|
||||
});
|
||||
moveResult = moveService.perform(object, newParent);
|
||||
});
|
||||
|
||||
it("links object to newParent", function () {
|
||||
expect(linkService.perform).toHaveBeenCalledWith(
|
||||
object,
|
||||
newParent
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a promise", function () {
|
||||
expect(moveResult.then).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("waits for result of link", function () {
|
||||
expect(linkService.perform.calls.mostRecent().promise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("throws an error when performed on invalid inputs", function () {
|
||||
function perform() {
|
||||
moveService.perform(object, newParent);
|
||||
}
|
||||
|
||||
spyOn(moveService, "validate");
|
||||
moveService.validate.and.returnValue(true);
|
||||
expect(perform).not.toThrow();
|
||||
moveService.validate.and.returnValue(false);
|
||||
expect(perform).toThrow();
|
||||
});
|
||||
|
||||
describe("when moving an original", function () {
|
||||
beforeEach(function () {
|
||||
locationCapability.getContextualLocation
|
||||
.and.returnValue('new-location');
|
||||
locationCapability.isOriginal.and.returnValue(true);
|
||||
linkService.perform.calls.mostRecent().promise.resolve();
|
||||
});
|
||||
|
||||
it("updates location", function () {
|
||||
expect(locationCapability.setPrimaryLocation)
|
||||
.toHaveBeenCalledWith('new-location');
|
||||
});
|
||||
|
||||
describe("after location update", function () {
|
||||
beforeEach(function () {
|
||||
locationPromise.resolve();
|
||||
});
|
||||
|
||||
it("removes object from parent without user warning dialog", function () {
|
||||
expect(actionCapability.perform)
|
||||
.toHaveBeenCalledWith('remove', true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("when moving a link", function () {
|
||||
beforeEach(function () {
|
||||
locationCapability.isOriginal.and.returnValue(false);
|
||||
linkService.perform.calls.mostRecent().promise.resolve();
|
||||
});
|
||||
|
||||
it("does not update location", function () {
|
||||
expect(locationCapability.setPrimaryLocation)
|
||||
.not
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes object from parent without user warning dialog", function () {
|
||||
expect(actionCapability.perform)
|
||||
.toHaveBeenCalledWith('remove', true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -46,6 +46,8 @@ define([
|
||||
'./api/Branding',
|
||||
'./plugins/licenses/plugin',
|
||||
'./plugins/remove/plugin',
|
||||
'./plugins/move/plugin',
|
||||
'./plugins/duplicate/plugin',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
@@ -73,6 +75,8 @@ define([
|
||||
BrandingAPI,
|
||||
LicensesPlugin,
|
||||
RemoveActionPlugin,
|
||||
MoveActionPlugin,
|
||||
DuplicateActionPlugin,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@@ -263,6 +267,8 @@ define([
|
||||
this.install(LegacyIndicatorsPlugin());
|
||||
this.install(LicensesPlugin.default());
|
||||
this.install(RemoveActionPlugin.default());
|
||||
this.install(MoveActionPlugin.default());
|
||||
this.install(DuplicateActionPlugin.default());
|
||||
this.install(this.plugins.FolderView());
|
||||
this.install(this.plugins.Tabs());
|
||||
this.install(ImageryPlugin.default());
|
||||
@@ -276,6 +282,7 @@ define([
|
||||
this.install(this.plugins.NotificationIndicator());
|
||||
this.install(this.plugins.NewFolderAction());
|
||||
this.install(this.plugins.ViewDatumAction());
|
||||
this.install(this.plugins.ObjectInterceptors());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
66
src/api/objects/InterceptorRegistry.js
Normal file
66
src/api/objects/InterceptorRegistry.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
export default class InterceptorRegistry {
|
||||
/**
|
||||
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
|
||||
* @interface InterceptorRegistry
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
constructor() {
|
||||
this.interceptors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface InterceptorDef
|
||||
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
|
||||
* @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
|
||||
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
|
||||
* @memberof module:openmct InterceptorRegistry#
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register a new object interceptor.
|
||||
*
|
||||
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
|
||||
* @method addInterceptor
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
addInterceptor(interceptorDef) {
|
||||
//TODO: sort by priority
|
||||
this.interceptors.push(interceptorDef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all interceptors applicable to a domain object.
|
||||
* @method getInterceptors
|
||||
* @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
getInterceptors(identifier, object) {
|
||||
return this.interceptors.filter(interceptor => {
|
||||
return typeof interceptor.appliesTo === 'function'
|
||||
&& interceptor.appliesTo(identifier, object);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
@@ -26,6 +26,7 @@ define([
|
||||
'./MutableObject',
|
||||
'./RootRegistry',
|
||||
'./RootObjectProvider',
|
||||
'./InterceptorRegistry',
|
||||
'EventEmitter'
|
||||
], function (
|
||||
_,
|
||||
@@ -33,6 +34,7 @@ define([
|
||||
MutableObject,
|
||||
RootRegistry,
|
||||
RootObjectProvider,
|
||||
InterceptorRegistry,
|
||||
EventEmitter
|
||||
) {
|
||||
|
||||
@@ -48,6 +50,7 @@ define([
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry.default();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,6 +180,10 @@ define([
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
const interceptors = this.listGetInterceptors(identifier, result);
|
||||
interceptors.forEach(interceptor => {
|
||||
result = interceptor.invoke(identifier, result);
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -312,6 +319,27 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
|
||||
* The domain object will be transformed after it is retrieved from the persistence store
|
||||
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
|
||||
*
|
||||
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
|
||||
* @method addGetInterceptor
|
||||
* @memberof module:openmct.InterceptorRegistry#
|
||||
*/
|
||||
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
|
||||
this.interceptorRegistry.addInterceptor(interceptorDef);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the interceptors for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||
};
|
||||
|
||||
/**
|
||||
* Uniquely identifies a domain object.
|
||||
*
|
||||
|
||||
@@ -63,12 +63,51 @@ describe("The Object API", () => {
|
||||
describe("The get function", () => {
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
let mockInterceptor;
|
||||
let anotherMockInterceptor;
|
||||
let notApplicableMockInterceptor;
|
||||
beforeEach(() => {
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
||||
|
||||
mockInterceptor = jasmine.createSpyObj("mock interceptor", [
|
||||
"appliesTo",
|
||||
"invoke"
|
||||
]);
|
||||
mockInterceptor.appliesTo.and.returnValue(true);
|
||||
mockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||
return Object.assign({
|
||||
changed: true
|
||||
}, object);
|
||||
});
|
||||
|
||||
anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [
|
||||
"appliesTo",
|
||||
"invoke"
|
||||
]);
|
||||
anotherMockInterceptor.appliesTo.and.returnValue(true);
|
||||
anotherMockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||
return Object.assign({
|
||||
alsoChanged: true
|
||||
}, object);
|
||||
});
|
||||
|
||||
notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [
|
||||
"appliesTo",
|
||||
"invoke"
|
||||
]);
|
||||
notApplicableMockInterceptor.appliesTo.and.returnValue(false);
|
||||
notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||
return Object.assign({
|
||||
shouldNotBeChanged: true
|
||||
}, object);
|
||||
});
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
objectAPI.addGetInterceptor(mockInterceptor);
|
||||
objectAPI.addGetInterceptor(anotherMockInterceptor);
|
||||
objectAPI.addGetInterceptor(notApplicableMockInterceptor);
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
@@ -78,6 +117,15 @@ describe("The Object API", () => {
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("applies any applicable interceptors", () => {
|
||||
expect(mockDomainObject.changed).toBeUndefined();
|
||||
objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||
expect(object.changed).toBeTrue();
|
||||
expect(object.alsoChanged).toBeTrue();
|
||||
expect(object.shouldNotBeChanged).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,342 +19,352 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
|
||||
import AutoflowTabularConstants from './AutoflowTabularConstants';
|
||||
import $ from 'zepto';
|
||||
import DOMObserver from './dom-observer';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState,
|
||||
spyOnBuiltins
|
||||
} from 'utils/testing';
|
||||
|
||||
define([
|
||||
'./AutoflowTabularPlugin',
|
||||
'./AutoflowTabularConstants',
|
||||
'../../MCT',
|
||||
'zepto',
|
||||
'./dom-observer'
|
||||
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) {
|
||||
describe("AutoflowTabularPlugin", function () {
|
||||
let testType;
|
||||
let testObject;
|
||||
let mockmct;
|
||||
describe("AutoflowTabularPlugin", () => {
|
||||
let testType;
|
||||
let testObject;
|
||||
let mockmct;
|
||||
|
||||
beforeEach(function () {
|
||||
testType = "some-type";
|
||||
testObject = { type: testType };
|
||||
mockmct = new MCT();
|
||||
spyOn(mockmct.composition, 'get');
|
||||
spyOn(mockmct.objectViews, 'addProvider');
|
||||
spyOn(mockmct.telemetry, 'getMetadata');
|
||||
spyOn(mockmct.telemetry, 'getValueFormatter');
|
||||
spyOn(mockmct.telemetry, 'limitEvaluator');
|
||||
spyOn(mockmct.telemetry, 'request');
|
||||
spyOn(mockmct.telemetry, 'subscribe');
|
||||
beforeEach(() => {
|
||||
testType = "some-type";
|
||||
testObject = { type: testType };
|
||||
mockmct = createOpenMct();
|
||||
spyOn(mockmct.composition, 'get');
|
||||
spyOn(mockmct.objectViews, 'addProvider');
|
||||
spyOn(mockmct.telemetry, 'getMetadata');
|
||||
spyOn(mockmct.telemetry, 'getValueFormatter');
|
||||
spyOn(mockmct.telemetry, 'limitEvaluator');
|
||||
spyOn(mockmct.telemetry, 'request');
|
||||
spyOn(mockmct.telemetry, 'subscribe');
|
||||
|
||||
const plugin = new AutoflowTabularPlugin({ type: testType });
|
||||
plugin(mockmct);
|
||||
const plugin = new AutoflowTabularPlugin({ type: testType });
|
||||
plugin(mockmct);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(mockmct);
|
||||
});
|
||||
|
||||
it("installs a view provider", () => {
|
||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("installs a view provider which", () => {
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider =
|
||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
||||
});
|
||||
|
||||
it("installs a view provider", function () {
|
||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
||||
it("applies its view to the type from options", () => {
|
||||
expect(provider.canView(testObject)).toBe(true);
|
||||
});
|
||||
|
||||
describe("installs a view provider which", function () {
|
||||
let provider;
|
||||
it("does not apply to other types", () => {
|
||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
provider =
|
||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
||||
});
|
||||
describe("provides a view which", () => {
|
||||
let testKeys;
|
||||
let testChildren;
|
||||
let testContainer;
|
||||
let testHistories;
|
||||
let mockComposition;
|
||||
let mockMetadata;
|
||||
let mockEvaluator;
|
||||
let mockUnsubscribes;
|
||||
let callbacks;
|
||||
let view;
|
||||
let domObserver;
|
||||
|
||||
it("applies its view to the type from options", function () {
|
||||
expect(provider.canView(testObject)).toBe(true);
|
||||
});
|
||||
function waitsForChange() {
|
||||
return new Promise(function (resolve) {
|
||||
window.requestAnimationFrame(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
it("does not apply to other types", function () {
|
||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||
});
|
||||
function emitEvent(mockEmitter, type, event) {
|
||||
mockEmitter.on.calls.all().forEach((call) => {
|
||||
if (call.args[0] === type) {
|
||||
call.args[1](event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe("provides a view which", function () {
|
||||
let testKeys;
|
||||
let testChildren;
|
||||
let testContainer;
|
||||
let testHistories;
|
||||
let mockComposition;
|
||||
let mockMetadata;
|
||||
let mockEvaluator;
|
||||
let mockUnsubscribes;
|
||||
let callbacks;
|
||||
let view;
|
||||
let domObserver;
|
||||
beforeEach((done) => {
|
||||
callbacks = {};
|
||||
|
||||
function waitsForChange() {
|
||||
return new Promise(function (resolve) {
|
||||
window.requestAnimationFrame(resolve);
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
testObject = { type: 'some-type' };
|
||||
testKeys = ['abc', 'def', 'xyz'];
|
||||
testChildren = testKeys.map((key) => {
|
||||
return {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: key
|
||||
},
|
||||
name: "Object " + key
|
||||
};
|
||||
});
|
||||
testContainer = $('<div>')[0];
|
||||
domObserver = new DOMObserver(testContainer);
|
||||
|
||||
testHistories = testKeys.reduce((histories, key, index) => {
|
||||
histories[key] = {
|
||||
key: key,
|
||||
range: index + 10,
|
||||
domain: key + index
|
||||
};
|
||||
|
||||
return histories;
|
||||
}, {});
|
||||
|
||||
mockComposition =
|
||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
||||
mockMetadata =
|
||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||
|
||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||
mockUnsubscribes = testKeys.reduce((map, key) => {
|
||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
mockmct.composition.get.and.returnValue(mockComposition);
|
||||
mockComposition.load.and.callFake(() => {
|
||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||
|
||||
return Promise.resolve(testChildren);
|
||||
});
|
||||
|
||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||
mockFormatter.format.and.callFake((datum) => {
|
||||
return datum[metadatum.hint];
|
||||
});
|
||||
|
||||
return mockFormatter;
|
||||
});
|
||||
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
|
||||
const key = obj.identifier.key;
|
||||
callbacks[key] = callback;
|
||||
|
||||
return mockUnsubscribes[key];
|
||||
});
|
||||
mockmct.telemetry.request.and.callFake((obj, request) => {
|
||||
const key = obj.identifier.key;
|
||||
|
||||
return Promise.resolve([testHistories[key]]);
|
||||
});
|
||||
mockMetadata.valuesForHints.and.callFake((hints) => {
|
||||
return [{ hint: hints[0] }];
|
||||
});
|
||||
|
||||
view = provider.view(testObject);
|
||||
view.show(testContainer);
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
domObserver.destroy();
|
||||
});
|
||||
|
||||
it("populates its container", () => {
|
||||
expect(testContainer.children.length > 0).toBe(true);
|
||||
});
|
||||
|
||||
describe("when rows have been populated", () => {
|
||||
function rowsMatch() {
|
||||
const rows = $(testContainer).find(".l-autoflow-row").length;
|
||||
|
||||
return rows === testChildren.length;
|
||||
}
|
||||
|
||||
function emitEvent(mockEmitter, type, event) {
|
||||
mockEmitter.on.calls.all().forEach(function (call) {
|
||||
if (call.args[0] === type) {
|
||||
call.args[1](event);
|
||||
}
|
||||
});
|
||||
it("shows one row per child object", () => {
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
|
||||
// it("adds rows on composition change", () => {
|
||||
// const child = {
|
||||
// identifier: {
|
||||
// namespace: "test",
|
||||
// key: "123"
|
||||
// },
|
||||
// name: "Object 123"
|
||||
// };
|
||||
// testChildren.push(child);
|
||||
// emitEvent(mockComposition, 'add', child);
|
||||
|
||||
// return domObserver.when(rowsMatch);
|
||||
// });
|
||||
|
||||
it("removes rows on composition change", () => {
|
||||
const child = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', child.identifier);
|
||||
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
});
|
||||
|
||||
it("removes subscriptions when destroyed", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||
});
|
||||
view.destroy();
|
||||
testKeys.forEach((key) => {
|
||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("provides a button to change column width", () => {
|
||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
||||
const nextWidth =
|
||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
||||
|
||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||
.toEqual(initialWidth + 'px');
|
||||
|
||||
$(testContainer).find('.change-column-width').click();
|
||||
|
||||
function widthHasChanged() {
|
||||
const width = $(testContainer).find('.l-autoflow-col').css('width');
|
||||
|
||||
return width !== initialWidth + 'px';
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
callbacks = {};
|
||||
|
||||
testObject = { type: 'some-type' };
|
||||
testKeys = ['abc', 'def', 'xyz'];
|
||||
testChildren = testKeys.map(function (key) {
|
||||
return {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: key
|
||||
},
|
||||
name: "Object " + key
|
||||
};
|
||||
return domObserver.when(widthHasChanged)
|
||||
.then(() => {
|
||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||
.toEqual(nextWidth + 'px');
|
||||
});
|
||||
testContainer = $('<div>')[0];
|
||||
domObserver = new DOMObserver(testContainer);
|
||||
});
|
||||
|
||||
testHistories = testKeys.reduce(function (histories, key, index) {
|
||||
histories[key] = {
|
||||
key: key,
|
||||
range: index + 10,
|
||||
domain: key + index
|
||||
};
|
||||
it("subscribes to all child objects", () => {
|
||||
testKeys.forEach((key) => {
|
||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
return histories;
|
||||
}, {});
|
||||
it("displays historical telemetry", () => {
|
||||
function rowTextDefined() {
|
||||
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
||||
}
|
||||
|
||||
mockComposition =
|
||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
||||
mockMetadata =
|
||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||
|
||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||
mockUnsubscribes = testKeys.reduce(function (map, key) {
|
||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
mockmct.composition.get.and.returnValue(mockComposition);
|
||||
mockComposition.load.and.callFake(function () {
|
||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||
|
||||
return Promise.resolve(testChildren);
|
||||
return domObserver.when(rowTextDefined).then(() => {
|
||||
testKeys.forEach((key, index) => {
|
||||
const datum = testHistories[key];
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||
mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) {
|
||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||
mockFormatter.format.and.callFake(function (datum) {
|
||||
return datum[metadatum.hint];
|
||||
});
|
||||
|
||||
return mockFormatter;
|
||||
});
|
||||
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||
mockmct.telemetry.subscribe.and.callFake(function (obj, callback) {
|
||||
const key = obj.identifier.key;
|
||||
callbacks[key] = callback;
|
||||
|
||||
return mockUnsubscribes[key];
|
||||
});
|
||||
mockmct.telemetry.request.and.callFake(function (obj, request) {
|
||||
const key = obj.identifier.key;
|
||||
|
||||
return Promise.resolve([testHistories[key]]);
|
||||
});
|
||||
mockMetadata.valuesForHints.and.callFake(function (hints) {
|
||||
return [{ hint: hints[0] }];
|
||||
});
|
||||
|
||||
view = provider.view(testObject);
|
||||
view.show(testContainer);
|
||||
|
||||
return waitsForChange();
|
||||
it("displays incoming telemetry", () => {
|
||||
const testData = testKeys.map((key, index) => {
|
||||
return {
|
||||
key: key,
|
||||
range: index * 100,
|
||||
domain: key + index
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
domObserver.destroy();
|
||||
testData.forEach((datum) => {
|
||||
callbacks[datum.key](datum);
|
||||
});
|
||||
|
||||
it("populates its container", function () {
|
||||
expect(testContainer.children.length > 0).toBe(true);
|
||||
return waitsForChange().then(() => {
|
||||
testData.forEach((datum, index) => {
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when rows have been populated", function () {
|
||||
function rowsMatch() {
|
||||
const rows = $(testContainer).find(".l-autoflow-row").length;
|
||||
|
||||
return rows === testChildren.length;
|
||||
}
|
||||
|
||||
it("shows one row per child object", function () {
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
|
||||
it("adds rows on composition change", function () {
|
||||
const child = {
|
||||
identifier: {
|
||||
namespace: "test",
|
||||
key: "123"
|
||||
},
|
||||
name: "Object 123"
|
||||
};
|
||||
testChildren.push(child);
|
||||
emitEvent(mockComposition, 'add', child);
|
||||
|
||||
return domObserver.when(rowsMatch);
|
||||
});
|
||||
|
||||
it("removes rows on composition change", function () {
|
||||
const child = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', child.identifier);
|
||||
|
||||
return domObserver.when(rowsMatch);
|
||||
it("updates classes for limit violations", () => {
|
||||
const testClass = "some-limit-violation";
|
||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||
testKeys.forEach((key) => {
|
||||
callbacks[key]({
|
||||
range: 'foo',
|
||||
domain: 'bar'
|
||||
});
|
||||
});
|
||||
|
||||
it("removes subscriptions when destroyed", function () {
|
||||
testKeys.forEach(function (key) {
|
||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||
});
|
||||
view.destroy();
|
||||
testKeys.forEach(function (key) {
|
||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||
return waitsForChange().then(() => {
|
||||
testKeys.forEach((datum, index) => {
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.hasClass(testClass)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("provides a button to change column width", function () {
|
||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
||||
const nextWidth =
|
||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
||||
it("automatically flows to new columns", () => {
|
||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||
const count = testKeys.length;
|
||||
const $container = $(testContainer);
|
||||
let promiseChain = Promise.resolve();
|
||||
|
||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||
.toEqual(initialWidth + 'px');
|
||||
function columnsHaveAutoflowed() {
|
||||
const itemsHeight = $container.find('.l-autoflow-items').height();
|
||||
const availableHeight = itemsHeight - sliderHeight;
|
||||
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
||||
const columns = Math.ceil(count / availableRows);
|
||||
|
||||
$(testContainer).find('.change-column-width').click();
|
||||
return $container.find('.l-autoflow-col').length === columns;
|
||||
}
|
||||
|
||||
function widthHasChanged() {
|
||||
const width = $(testContainer).find('.l-autoflow-col').css('width');
|
||||
|
||||
return width !== initialWidth + 'px';
|
||||
}
|
||||
|
||||
return domObserver.when(widthHasChanged)
|
||||
.then(function () {
|
||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||
.toEqual(nextWidth + 'px');
|
||||
});
|
||||
$container.find('.abs').css({
|
||||
position: 'absolute',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
top: '0px',
|
||||
bottom: '0px'
|
||||
});
|
||||
$container.css({ position: 'absolute' });
|
||||
|
||||
it("subscribes to all child objects", function () {
|
||||
testKeys.forEach(function (key) {
|
||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||
});
|
||||
$container.appendTo(document.body);
|
||||
|
||||
function setHeight(height) {
|
||||
$container.css('height', height + 'px');
|
||||
|
||||
return domObserver.when(columnsHaveAutoflowed);
|
||||
}
|
||||
|
||||
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||
}
|
||||
|
||||
return promiseChain.then(() => {
|
||||
$container.remove();
|
||||
});
|
||||
});
|
||||
|
||||
it("displays historical telemetry", function () {
|
||||
function rowTextDefined() {
|
||||
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
||||
}
|
||||
|
||||
return domObserver.when(rowTextDefined).then(function () {
|
||||
testKeys.forEach(function (key, index) {
|
||||
const datum = testHistories[key];
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays incoming telemetry", function () {
|
||||
const testData = testKeys.map(function (key, index) {
|
||||
return {
|
||||
key: key,
|
||||
range: index * 100,
|
||||
domain: key + index
|
||||
};
|
||||
});
|
||||
|
||||
testData.forEach(function (datum) {
|
||||
callbacks[datum.key](datum);
|
||||
});
|
||||
|
||||
return waitsForChange().then(function () {
|
||||
testData.forEach(function (datum, index) {
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.text()).toEqual(String(datum.range));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("updates classes for limit violations", function () {
|
||||
const testClass = "some-limit-violation";
|
||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||
testKeys.forEach(function (key) {
|
||||
callbacks[key]({
|
||||
range: 'foo',
|
||||
domain: 'bar'
|
||||
});
|
||||
});
|
||||
|
||||
return waitsForChange().then(function () {
|
||||
testKeys.forEach(function (datum, index) {
|
||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||
expect($cell.hasClass(testClass)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("automatically flows to new columns", function () {
|
||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||
const count = testKeys.length;
|
||||
const $container = $(testContainer);
|
||||
let promiseChain = Promise.resolve();
|
||||
|
||||
function columnsHaveAutoflowed() {
|
||||
const itemsHeight = $container.find('.l-autoflow-items').height();
|
||||
const availableHeight = itemsHeight - sliderHeight;
|
||||
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
||||
const columns = Math.ceil(count / availableRows);
|
||||
|
||||
return $container.find('.l-autoflow-col').length === columns;
|
||||
}
|
||||
|
||||
$container.find('.abs').css({
|
||||
position: 'absolute',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
top: '0px',
|
||||
bottom: '0px'
|
||||
});
|
||||
$container.css({ position: 'absolute' });
|
||||
|
||||
$container.appendTo(document.body);
|
||||
|
||||
function setHeight(height) {
|
||||
$container.css('height', height + 'px');
|
||||
|
||||
return domObserver.when(columnsHaveAutoflowed);
|
||||
}
|
||||
|
||||
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||
}
|
||||
|
||||
return promiseChain.then(function () {
|
||||
$container.remove();
|
||||
});
|
||||
});
|
||||
|
||||
it("loads composition exactly once", function () {
|
||||
const testObj = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||
testChildren.push(testObj);
|
||||
emitEvent(mockComposition, 'add', testObj);
|
||||
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||
});
|
||||
it("loads composition exactly once", () => {
|
||||
const testObj = testChildren.pop();
|
||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||
testChildren.push(testObj);
|
||||
emitEvent(mockComposition, 'add', testObj);
|
||||
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view"
|
||||
class="c-telemetry-view u-style-receiver"
|
||||
:class="[statusClass]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
|
||||
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,121 +0,0 @@
|
||||
/******************************* GRID ITEMS */
|
||||
.c-grid-item {
|
||||
// Mobile-first
|
||||
@include button($bg: $colorItemBg, $fg: $colorItemFg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: $interiorMarginLg;
|
||||
|
||||
&__type-icon {
|
||||
filter: $colorKeyFilter;
|
||||
flex: 0 0 $gridItemMobile;
|
||||
font-size: floor($gridItemMobile / 2);
|
||||
margin-right: $interiorMarginLg;
|
||||
}
|
||||
|
||||
&.is-alias {
|
||||
// Object is an alias to an original.
|
||||
[class*='__type-icon'] {
|
||||
@include isAlias();
|
||||
color: $colorIconAliasForKeyFilter;
|
||||
}
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__name {
|
||||
@include ellipsize();
|
||||
color: $colorItemFg;
|
||||
@include headerFont(1.2em);
|
||||
margin-bottom: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__metadata {
|
||||
color: $colorItemFgDetails;
|
||||
font-size: 0.9em;
|
||||
|
||||
body.mobile & {
|
||||
[class*='__item-count'] {
|
||||
&:before {
|
||||
content: ' - ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
color: $colorItemFgDetails;
|
||||
flex: 0 0 64px;
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
body.desktop & {
|
||||
$transOutMs: 300ms;
|
||||
flex-flow: column nowrap;
|
||||
transition: background $transOutMs ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $colorItemBgHov;
|
||||
transition: $transIn;
|
||||
|
||||
.c-grid-item__type-icon {
|
||||
filter: $colorKeyFilterHov;
|
||||
transform: scale(1);
|
||||
transition: $transInBounce;
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
margin: 0; // Reset from mobile
|
||||
}
|
||||
|
||||
&__controls {
|
||||
align-items: start;
|
||||
flex: 0 0 auto;
|
||||
order: 1;
|
||||
.c-info-button,
|
||||
.c-pointer-icon { display: none; }
|
||||
}
|
||||
|
||||
&__type-icon {
|
||||
flex: 1 1 auto;
|
||||
font-size: floor($gridItemDesk / 3);
|
||||
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
|
||||
order: 2;
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
transition: all $transOutMs ease-in-out;
|
||||
}
|
||||
|
||||
&__details {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
&__metadata {
|
||||
display: flex;
|
||||
|
||||
&__type {
|
||||
flex: 1 1 auto;
|
||||
@include ellipsize();
|
||||
}
|
||||
|
||||
&__item-count {
|
||||
opacity: 0.7;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='is-status'] {
|
||||
&.is-status--notebook-default {
|
||||
.is-status__indicator {
|
||||
display: block;
|
||||
|
||||
&:before {
|
||||
color: $colorFilter;
|
||||
content: $glyph-icon-notebook-page;
|
||||
font-family: symbolsfont;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='is-status--missing'],
|
||||
&[class*='is-status--suspect']{
|
||||
[class*='__type-icon'],
|
||||
[class*='__details'] {
|
||||
opacity: $opacityMissing;
|
||||
|
||||
@@ -20,40 +20,21 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['./AbstractComposeAction'],
|
||||
function (AbstractComposeAction) {
|
||||
|
||||
/**
|
||||
* The MoveAction is available from context menus and allows a user to
|
||||
* move an object to another location of their choosing.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @constructor
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function MoveAction(policyService, locationService, moveService, context) {
|
||||
AbstractComposeAction.apply(
|
||||
this,
|
||||
[policyService, locationService, moveService, context, "Move"]
|
||||
);
|
||||
}
|
||||
|
||||
MoveAction.prototype = Object.create(AbstractComposeAction.prototype);
|
||||
|
||||
MoveAction.appliesTo = function (context) {
|
||||
var applicableObject =
|
||||
context.selectedObject || context.domainObject;
|
||||
|
||||
if (applicableObject && applicableObject.model.locked) {
|
||||
return false;
|
||||
export default function MissingObjectInterceptor(openmct) {
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return identifier.key !== 'mine';
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
return {
|
||||
identifier,
|
||||
type: 'unknown',
|
||||
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
|
||||
};
|
||||
}
|
||||
|
||||
return Boolean(applicableObject
|
||||
&& applicableObject.hasCapability('context'));
|
||||
};
|
||||
|
||||
return MoveAction;
|
||||
}
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -20,44 +20,24 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
export default function MyItemsInterceptor(openmct) {
|
||||
|
||||
/**
|
||||
* Disallow moves when either the parent or the child are not
|
||||
* modifiable by users.
|
||||
* @constructor
|
||||
* @implements {Policy}
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function MovePolicy() {
|
||||
}
|
||||
openmct.objects.addGetInterceptor({
|
||||
appliesTo: (identifier, domainObject) => {
|
||||
return identifier.key === 'mine';
|
||||
},
|
||||
invoke: (identifier, object) => {
|
||||
if (object === undefined) {
|
||||
return {
|
||||
identifier,
|
||||
"name": "My Items",
|
||||
"type": "folder",
|
||||
"composition": [],
|
||||
"location": "ROOT"
|
||||
};
|
||||
}
|
||||
|
||||
function parentOf(domainObject) {
|
||||
var context = domainObject.getCapability('context');
|
||||
|
||||
return context && context.getParent();
|
||||
}
|
||||
|
||||
function allowMutation(domainObject) {
|
||||
var type = domainObject && domainObject.getCapability('type');
|
||||
|
||||
return Boolean(type && type.hasFeature('creation'));
|
||||
}
|
||||
|
||||
function selectedObject(context) {
|
||||
return context.selectedObject || context.domainObject;
|
||||
}
|
||||
|
||||
MovePolicy.prototype.allow = function (action, context) {
|
||||
var key = action.getMetadata().key;
|
||||
|
||||
if (key === 'move') {
|
||||
return allowMutation(selectedObject(context))
|
||||
&& allowMutation(parentOf(selectedObject(context)));
|
||||
return object;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return MovePolicy;
|
||||
});
|
||||
});
|
||||
}
|
||||
9
src/plugins/interceptors/plugin.js
Normal file
9
src/plugins/interceptors/plugin.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import missingObjectInterceptor from "./missingObjectInterceptor";
|
||||
import myItemsInterceptor from "./myItemsInterceptor";
|
||||
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
myItemsInterceptor(openmct);
|
||||
missingObjectInterceptor(openmct);
|
||||
};
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -118,7 +118,7 @@ export default {
|
||||
painterroInstance.show(this.embed.snapshot.src);
|
||||
},
|
||||
changeLocation() {
|
||||
const link = this.embed.historicLink;
|
||||
const hash = this.embed.historicLink;
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
||||
@@ -143,6 +143,7 @@ export default {
|
||||
this.openmct.notifications.alert(message);
|
||||
}
|
||||
|
||||
const link = `${location.host}${location.pathname}${hash}`;
|
||||
const url = new URL(link);
|
||||
window.location.href = url.hash;
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<script>
|
||||
import Snapshot from '../snapshot';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||
|
||||
export default {
|
||||
@@ -49,6 +49,8 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
validateNotebookStorageObject();
|
||||
|
||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||
this.setDefaultNotebookStatus();
|
||||
},
|
||||
|
||||
@@ -67,3 +67,24 @@ export function setDefaultNotebookPage(page) {
|
||||
notebookStorage.page = page;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
export function validateNotebookStorageObject() {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
|
||||
let valid = false;
|
||||
if (notebookStorage) {
|
||||
Object.entries(notebookStorage).forEach(([key, value]) => {
|
||||
const validKey = key !== undefined && key !== null;
|
||||
const validValue = value !== undefined && value !== null;
|
||||
valid = validKey && validValue;
|
||||
});
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
return notebookStorage;
|
||||
}
|
||||
|
||||
console.warn('Invalid Notebook object, clearing default notebook storage');
|
||||
|
||||
clearDefaultNotebook();
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ export default class CouchObjectProvider {
|
||||
}
|
||||
|
||||
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
||||
if (!this.objectQueue[key].pending) {
|
||||
//Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates.
|
||||
if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) {
|
||||
this.objectQueue[key].updateRevision(response[REV]);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,8 @@ define([
|
||||
'./persistence/couch/plugin',
|
||||
'./defaultRootName/plugin',
|
||||
'./timeline/plugin',
|
||||
'./viewDatumAction/plugin'
|
||||
'./viewDatumAction/plugin',
|
||||
'./interceptors/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -99,7 +100,8 @@ define([
|
||||
CouchDBPlugin,
|
||||
DefaultRootName,
|
||||
Timeline,
|
||||
ViewDatumAction
|
||||
ViewDatumAction,
|
||||
ObjectInterceptors
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@@ -194,6 +196,7 @@ define([
|
||||
plugins.DefaultRootName = DefaultRootName.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
plugins.ViewDatumAction = ViewDatumAction.default;
|
||||
plugins.ObjectInterceptors = ObjectInterceptors.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
110
src/plugins/tabs/pluginSpec.js
Normal file
110
src/plugins/tabs/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 {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
fdescribe("the tabs plugin", () => {
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("defines a tabs object that is creatable", () => {
|
||||
const tabsType = openmct.types.get('tabs');
|
||||
|
||||
expect(tabsType.definition.creatable).toBe(true);
|
||||
});
|
||||
|
||||
it("defines a form to configure the tabs object", () => {
|
||||
const tabsType = openmct.types.get('tabs');
|
||||
|
||||
expect(tabsType.definition.form).toBeDefined();
|
||||
});
|
||||
|
||||
it("provides a tabs view", () => {
|
||||
const domainObject = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'mock-tabs-object'
|
||||
},
|
||||
type: 'tabs'
|
||||
};
|
||||
const applicableViews = openmct.objectViews.get(domainObject);
|
||||
const tabsView = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
|
||||
|
||||
expect(tabsView).toBeDefined();
|
||||
});
|
||||
|
||||
describe("the tabs view", () => {
|
||||
it("displays instructional text instead of tabs if no tabs exist", () => {
|
||||
const domainObject = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'mock-tabs-object'
|
||||
},
|
||||
type: 'tabs'
|
||||
};
|
||||
const element = document.createElement('div');
|
||||
const child = document.createElement('div');
|
||||
element.appendChild(child);
|
||||
|
||||
const applicableViews = openmct.objectViews.get(domainObject);
|
||||
const tabsViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs');
|
||||
const tabsView = tabsViewProvider.view(domainObject, [domainObject]);
|
||||
tabsView.show(child, true);
|
||||
|
||||
const noTabsElement = getNoTabsElement(element);
|
||||
|
||||
expect(noTabsElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it("creates a tab for each object dropped into the drop zone", () => {
|
||||
|
||||
});
|
||||
|
||||
it("creates a tab level for objects dropped into current tab view", () => {
|
||||
|
||||
});
|
||||
|
||||
it("removes a tab and tab contents", () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function getNoTabsElement(parent) {
|
||||
return parent.querySelector('.c-tabs-view__empty-message');
|
||||
}
|
||||
|
||||
function getTabElements(parent) {
|
||||
return parent.querySelectorAll('.c-tabs-view__tab');
|
||||
}
|
||||
});
|
||||
@@ -126,7 +126,7 @@ button {
|
||||
|
||||
.c-icon-button {
|
||||
[class*='label'] {
|
||||
opacity: 0.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&--mixed {
|
||||
@@ -468,9 +468,6 @@ select {
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
//+ * {
|
||||
// margin-top: $interiorMarginSm;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,8 +497,8 @@ select {
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
&:not([class]):before {
|
||||
content: ''; // Add this element so that menu items without an icon still indent properly
|
||||
&:not([class*='icon']):before {
|
||||
content: ''; // Enable :before so that menu items without an icon still indent properly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin isStatus($absPos: false) {
|
||||
@mixin isStatus($absPos: false, $glyph: '', $color: $colorBodyFg) {
|
||||
// Supports CSS classing as follows:
|
||||
// is-status--missing, is-status--suspect, etc.
|
||||
// Common styles to be applied to tree items, object labels, grid and list item views
|
||||
@@ -143,6 +143,11 @@
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:before {
|
||||
color: $color;
|
||||
content: $glyph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,20 +205,10 @@ tr {
|
||||
}
|
||||
|
||||
&--missing {
|
||||
@include isStatus();
|
||||
|
||||
.is-status__indicator:before {
|
||||
color: $colorAlert;
|
||||
content: $glyph-icon-alert-triangle;
|
||||
}
|
||||
@include isStatus($glyph: $glyph-icon-alert-triangle, $color: $colorAlert);
|
||||
}
|
||||
|
||||
&--suspect {
|
||||
@include isStatus();
|
||||
|
||||
.is-status__indicator:before {
|
||||
color: $colorWarningLo;
|
||||
content: $glyph-icon-alert-rect;
|
||||
}
|
||||
@include isStatus($glyph: $glyph-icon-alert-rect, $color: $colorWarningLo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,14 +217,12 @@
|
||||
.is-status--notebook-default {
|
||||
&:after {
|
||||
color: $colorFilter;
|
||||
content: $glyph-icon-notebook-page;
|
||||
display: block;
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.9em;
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
|
||||
&.c-list__item:after {
|
||||
content: $glyph-icon-notebook-page;
|
||||
flex: 1 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $interiorMarginSm;
|
||||
overflow: hidden;
|
||||
padding: 3px;
|
||||
|
||||
.c-object-label {
|
||||
@@ -35,6 +36,7 @@
|
||||
/*************************** FRAME CONTROLS */
|
||||
&__frame-controls {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&__btns,
|
||||
&__more {
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
&[class*='is-status--missing'],
|
||||
&[class*='is-status--suspect'] {
|
||||
&.is-status--missing,
|
||||
&.is-status--suspect {
|
||||
[class*='__type-icon'] {
|
||||
&:before,
|
||||
&:after {
|
||||
@@ -38,6 +38,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-status--notebook-default {
|
||||
&:after {
|
||||
content: $glyph-icon-notebook-page;
|
||||
display: block;
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-tree .c-object-label {
|
||||
|
||||
@@ -97,9 +97,12 @@ export default {
|
||||
} else {
|
||||
this.multiSelect = false;
|
||||
this.domainObject = selection[0][0].context.item;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.status = this.openmct.status.get(this.keyString);
|
||||
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
|
||||
|
||||
if (this.domainObject) {
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.status = this.openmct.status.get(this.keyString);
|
||||
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetDomainObject() {
|
||||
|
||||
Reference in New Issue
Block a user