mirror of
https://github.com/nasa/openmct.git
synced 2025-04-12 13:47:55 +00:00
[Actions] New Duplicate Action (#3410)
* WIP: refactoring legacy dulicate action * WIP: debugging duplicate duplicates... * WIP: fixed duplicate duplicates issue * added unit tests * removing old legacy copyaction and renaming duplicate action * removing fdescribe * trying to see if a done callback fixes testing issues * fixed tests * testing autoflow tests on server * tweaked autoflow tests to stop failing * minor updates for new 3 dot menu Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
This commit is contained in:
parent
fd9c9aee03
commit
dff393a714
@ -22,7 +22,6 @@
|
||||
|
||||
define([
|
||||
"./src/actions/MoveAction",
|
||||
"./src/actions/CopyAction",
|
||||
"./src/actions/LinkAction",
|
||||
"./src/actions/SetPrimaryLocationAction",
|
||||
"./src/services/LocatingCreationDecorator",
|
||||
@ -37,7 +36,6 @@ define([
|
||||
"./src/services/LocationService"
|
||||
], function (
|
||||
MoveAction,
|
||||
CopyAction,
|
||||
LinkAction,
|
||||
SetPrimaryLocationAction,
|
||||
LocatingCreationDecorator,
|
||||
@ -75,24 +73,6 @@ define([
|
||||
"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",
|
||||
|
@ -1,168 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['./AbstractComposeAction', './CancelError'],
|
||||
function (AbstractComposeAction, CancelError) {
|
||||
|
||||
/**
|
||||
* The CopyAction is available from context menus and allows a user to
|
||||
* deep copy an object to another location of their choosing.
|
||||
*
|
||||
* @implements {Action}
|
||||
* @constructor
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function CopyAction(
|
||||
$log,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
) {
|
||||
this.dialog = undefined;
|
||||
this.notification = undefined;
|
||||
this.dialogService = dialogService;
|
||||
this.notificationService = notificationService;
|
||||
this.$log = $log;
|
||||
//Extend the behaviour of the Abstract Compose Action
|
||||
AbstractComposeAction.call(
|
||||
this,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
context,
|
||||
"Duplicate",
|
||||
"To a Location"
|
||||
);
|
||||
}
|
||||
|
||||
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
|
||||
|
||||
/**
|
||||
* Updates user about progress of copy. Should not be invoked by
|
||||
* client code under any circumstances.
|
||||
*
|
||||
* @private
|
||||
* @param phase
|
||||
* @param totalObjects
|
||||
* @param processed
|
||||
*/
|
||||
CopyAction.prototype.progress = function (phase, totalObjects, processed) {
|
||||
/*
|
||||
Copy has two distinct phases. In the first phase a copy plan is
|
||||
made in memory. During this phase of execution, the user is
|
||||
shown a blocking 'modal' dialog.
|
||||
|
||||
In the second phase, the copying is taking place, and the user
|
||||
is shown non-invasive banner notifications at the bottom of the screen.
|
||||
*/
|
||||
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
|
||||
this.dialog = this.dialogService.showBlockingMessage({
|
||||
title: "Preparing to copy objects",
|
||||
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
|
||||
unknownProgress: true,
|
||||
severity: "info"
|
||||
});
|
||||
} else if (phase.toLowerCase() === "copying") {
|
||||
if (this.dialog) {
|
||||
this.dialog.dismiss();
|
||||
}
|
||||
|
||||
if (!this.notification) {
|
||||
this.notification = this.notificationService
|
||||
.notify({
|
||||
title: "Copying objects",
|
||||
unknownProgress: false,
|
||||
severity: "info"
|
||||
});
|
||||
}
|
||||
|
||||
this.notification.model.progress = (processed / totalObjects) * 100;
|
||||
this.notification.model.title = ["Copied ", processed, "of ",
|
||||
totalObjects, "objects"].join(" ");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the CopyAction. The CopyAction uses the default behaviour of
|
||||
* the AbstractComposeAction, but extends it to support notification
|
||||
* updates of progress on copy.
|
||||
*/
|
||||
CopyAction.prototype.perform = function () {
|
||||
var self = this;
|
||||
|
||||
function success(domainObject) {
|
||||
var domainObjectName = domainObject.model.name;
|
||||
|
||||
self.notification.dismiss();
|
||||
self.notificationService.info(domainObjectName + " copied successfully.");
|
||||
}
|
||||
|
||||
function error(errorDetails) {
|
||||
// No need to notify user of their own cancellation
|
||||
if (errorDetails instanceof CancelError) {
|
||||
return;
|
||||
}
|
||||
|
||||
var errorDialog,
|
||||
errorMessage = {
|
||||
title: "Error copying objects.",
|
||||
severity: "error",
|
||||
hint: errorDetails.message,
|
||||
minimized: true, // want the notification to be minimized initially (don't show banner)
|
||||
options: [{
|
||||
label: "OK",
|
||||
callback: function () {
|
||||
errorDialog.dismiss();
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
self.dialog.dismiss();
|
||||
if (self.notification) {
|
||||
self.notification.dismiss(); // Clear the progress notification
|
||||
}
|
||||
|
||||
self.$log.error("Error copying objects. ", errorDetails);
|
||||
//Show a minimized notification of error for posterity
|
||||
self.notificationService.notify(errorMessage);
|
||||
//Display a blocking message
|
||||
errorDialog = self.dialogService.showBlockingMessage(errorMessage);
|
||||
|
||||
}
|
||||
|
||||
function notification(details) {
|
||||
self.progress(details.phase, details.totalObjects, details.processed);
|
||||
}
|
||||
|
||||
return AbstractComposeAction.prototype.perform.call(this)
|
||||
.then(success, error, notification);
|
||||
};
|
||||
|
||||
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
|
||||
|
||||
return CopyAction;
|
||||
}
|
||||
);
|
@ -1,243 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/actions/CopyAction',
|
||||
'../services/MockCopyService',
|
||||
'../DomainObjectFactory'
|
||||
],
|
||||
function (CopyAction, MockCopyService, domainObjectFactory) {
|
||||
|
||||
describe("Copy Action", function () {
|
||||
|
||||
var copyAction,
|
||||
policyService,
|
||||
locationService,
|
||||
locationServicePromise,
|
||||
copyService,
|
||||
context,
|
||||
selectedObject,
|
||||
selectedObjectContextCapability,
|
||||
currentParent,
|
||||
newParent,
|
||||
notificationService,
|
||||
notification,
|
||||
dialogService,
|
||||
mockDialog,
|
||||
mockLog,
|
||||
abstractComposePromise,
|
||||
domainObject = {model: {name: "mockObject"}},
|
||||
progress = {
|
||||
phase: "copying",
|
||||
totalObjects: 10,
|
||||
processed: 1
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
policyService = jasmine.createSpyObj(
|
||||
'policyService',
|
||||
['allow']
|
||||
);
|
||||
policyService.allow.and.returnValue(true);
|
||||
|
||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
||||
'selectedObjectContextCapability',
|
||||
[
|
||||
'getParent'
|
||||
]
|
||||
);
|
||||
|
||||
selectedObject = domainObjectFactory({
|
||||
name: 'selectedObject',
|
||||
model: {
|
||||
name: 'selectedObject'
|
||||
},
|
||||
capabilities: {
|
||||
context: selectedObjectContextCapability
|
||||
}
|
||||
});
|
||||
|
||||
currentParent = domainObjectFactory({
|
||||
name: 'currentParent'
|
||||
});
|
||||
|
||||
selectedObjectContextCapability
|
||||
.getParent
|
||||
.and.returnValue(currentParent);
|
||||
|
||||
newParent = domainObjectFactory({
|
||||
name: 'newParent'
|
||||
});
|
||||
|
||||
locationService = jasmine.createSpyObj(
|
||||
'locationService',
|
||||
[
|
||||
'getLocationFromUser'
|
||||
]
|
||||
);
|
||||
|
||||
locationServicePromise = jasmine.createSpyObj(
|
||||
'locationServicePromise',
|
||||
[
|
||||
'then'
|
||||
]
|
||||
);
|
||||
|
||||
abstractComposePromise = jasmine.createSpyObj(
|
||||
'abstractComposePromise',
|
||||
[
|
||||
'then'
|
||||
]
|
||||
);
|
||||
|
||||
abstractComposePromise.then.and.callFake(function (success, error, notify) {
|
||||
notify(progress);
|
||||
success(domainObject);
|
||||
});
|
||||
|
||||
locationServicePromise.then.and.callFake(function (callback) {
|
||||
callback(newParent);
|
||||
|
||||
return abstractComposePromise;
|
||||
});
|
||||
|
||||
locationService
|
||||
.getLocationFromUser
|
||||
.and.returnValue(locationServicePromise);
|
||||
|
||||
dialogService = jasmine.createSpyObj('dialogService',
|
||||
['showBlockingMessage']
|
||||
);
|
||||
|
||||
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
|
||||
dialogService.showBlockingMessage.and.returnValue(mockDialog);
|
||||
|
||||
notification = jasmine.createSpyObj('notification',
|
||||
['dismiss', 'model']
|
||||
);
|
||||
|
||||
notificationService = jasmine.createSpyObj('notificationService',
|
||||
['notify', 'info']
|
||||
);
|
||||
|
||||
notificationService.notify.and.returnValue(notification);
|
||||
|
||||
mockLog = jasmine.createSpyObj('log', ['error']);
|
||||
|
||||
copyService = new MockCopyService();
|
||||
});
|
||||
|
||||
describe("with context from context-action", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
domainObject: selectedObject
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(copyAction).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when performed it", function () {
|
||||
beforeEach(function () {
|
||||
spyOn(copyAction, 'progress').and.callThrough();
|
||||
copyAction.perform();
|
||||
});
|
||||
|
||||
it("prompts for location", function () {
|
||||
expect(locationService.getLocationFromUser)
|
||||
.toHaveBeenCalledWith(
|
||||
"Duplicate selectedObject To a Location",
|
||||
"Duplicate To",
|
||||
jasmine.any(Function),
|
||||
currentParent
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for location and handles cancellation by user", function () {
|
||||
expect(locationServicePromise.then)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("copies object to selected location", function () {
|
||||
locationServicePromise
|
||||
.then
|
||||
.calls.mostRecent()
|
||||
.args[0](newParent);
|
||||
|
||||
expect(copyService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
|
||||
it("notifies the user of progress", function () {
|
||||
expect(notificationService.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("notifies the user with name of object copied", function () {
|
||||
expect(notificationService.info)
|
||||
.toHaveBeenCalledWith("mockObject copied successfully.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with context from drag-drop", function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
selectedObject: selectedObject,
|
||||
domainObject: newParent
|
||||
};
|
||||
|
||||
copyAction = new CopyAction(
|
||||
mockLog,
|
||||
policyService,
|
||||
locationService,
|
||||
copyService,
|
||||
dialogService,
|
||||
notificationService,
|
||||
context
|
||||
);
|
||||
});
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(copyAction).toBeDefined();
|
||||
});
|
||||
|
||||
it("performs copy immediately", function () {
|
||||
copyAction.perform();
|
||||
expect(copyService.perform)
|
||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -46,6 +46,7 @@ define([
|
||||
'./api/Branding',
|
||||
'./plugins/licenses/plugin',
|
||||
'./plugins/remove/plugin',
|
||||
'./plugins/duplicate/plugin',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
@ -73,6 +74,7 @@ define([
|
||||
BrandingAPI,
|
||||
LicensesPlugin,
|
||||
RemoveActionPlugin,
|
||||
DuplicateActionPlugin,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@ -263,6 +265,7 @@ define([
|
||||
this.install(LegacyIndicatorsPlugin());
|
||||
this.install(LicensesPlugin.default());
|
||||
this.install(RemoveActionPlugin.default());
|
||||
this.install(DuplicateActionPlugin.default());
|
||||
this.install(this.plugins.FolderView());
|
||||
this.install(this.plugins.Tabs());
|
||||
this.install(ImageryPlugin.default());
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
156
src/plugins/duplicate/DuplicateAction.js
Normal file
156
src/plugins/duplicate/DuplicateAction.js
Normal file
@ -0,0 +1,156 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import DuplicateTask from './DuplicateTask';
|
||||
|
||||
export default class DuplicateAction {
|
||||
constructor(openmct) {
|
||||
this.name = 'Duplicate';
|
||||
this.key = 'duplicate';
|
||||
this.description = 'Duplicate this object.';
|
||||
this.cssClass = "icon-duplicate";
|
||||
this.group = "action";
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
async invoke(objectPath) {
|
||||
let duplicationTask = new DuplicateTask(this.openmct);
|
||||
let originalObject = objectPath[0];
|
||||
let parent = objectPath[1];
|
||||
let userInput = await this.getUserInput(originalObject, parent);
|
||||
let newParent = userInput.location;
|
||||
let inNavigationPath = this.inNavigationPath(originalObject);
|
||||
|
||||
// legacy check
|
||||
if (this.isLegacyDomainObject(newParent)) {
|
||||
newParent = await this.convertFromLegacy(newParent);
|
||||
}
|
||||
|
||||
// if editing, save
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
|
||||
// duplicate
|
||||
let newObject = await duplicationTask.duplicate(originalObject, newParent);
|
||||
this.updateNameCheck(newObject, userInput.name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async getUserInput(originalObject, parent) {
|
||||
let dialogService = this.openmct.$injector.get('dialogService');
|
||||
let dialogForm = this.getDialogForm(originalObject, parent);
|
||||
let formState = {
|
||||
name: originalObject.name
|
||||
};
|
||||
let userInput = await dialogService.getUserInput(dialogForm, formState);
|
||||
|
||||
return userInput;
|
||||
}
|
||||
|
||||
updateNameCheck(object, name) {
|
||||
if (object.name !== name) {
|
||||
this.openmct.objects.mutate(object, 'name', name);
|
||||
}
|
||||
}
|
||||
|
||||
inNavigationPath(object) {
|
||||
return this.openmct.router.path
|
||||
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
|
||||
}
|
||||
|
||||
getDialogForm(object, parent) {
|
||||
return {
|
||||
name: "Duplicate Item",
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: "name",
|
||||
control: "textfield",
|
||||
name: "Folder Name",
|
||||
pattern: "\\S+",
|
||||
required: true,
|
||||
cssClass: "l-input-lg"
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
cssClass: "grows",
|
||||
control: "locator",
|
||||
validate: this.validate(object, parent),
|
||||
key: 'location'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
validate(object, currentParent) {
|
||||
return (parentCandidate) => {
|
||||
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
|
||||
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
|
||||
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
|
||||
|
||||
if (!parentCandidate || !currentParentKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parentCandidateKeystring === objectKeystring) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.openmct.composition.checkPolicy(
|
||||
parentCandidate.useCapability('adapter'),
|
||||
object
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
isLegacyDomainObject(domainObject) {
|
||||
return domainObject.getCapability !== undefined;
|
||||
}
|
||||
|
||||
async convertFromLegacy(legacyDomainObject) {
|
||||
let objectContext = legacyDomainObject.getCapability('context');
|
||||
let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
|
||||
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
let parent = objectPath[1];
|
||||
let parentType = parent && this.openmct.types.get(parent.type);
|
||||
let child = objectPath[0];
|
||||
let locked = child.locked ? child.locked : parent && parent.locked;
|
||||
|
||||
if (locked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parentType
|
||||
&& parentType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
}
|
270
src/plugins/duplicate/DuplicateTask.js
Normal file
270
src/plugins/duplicate/DuplicateTask.js
Normal file
@ -0,0 +1,270 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
/**
|
||||
* This class encapsulates the process of duplicating/copying a domain object
|
||||
* and all of its children.
|
||||
*
|
||||
* @param {DomainObject} domainObject The object to duplicate
|
||||
* @param {DomainObject} parent The new location of the cloned object tree
|
||||
* @param {src/plugins/duplicate.DuplicateService~filter} filter
|
||||
* a function used to filter out objects from
|
||||
* the cloning process
|
||||
* @constructor
|
||||
*/
|
||||
export default class DuplicateTask {
|
||||
|
||||
constructor(openmct) {
|
||||
this.domainObject = undefined;
|
||||
this.parent = undefined;
|
||||
this.firstClone = undefined;
|
||||
this.filter = undefined;
|
||||
this.persisted = 0;
|
||||
this.clones = [];
|
||||
this.idMap = {};
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the duplicate/copy task with the objects provided in the constructor.
|
||||
* @returns {promise} Which will resolve with a clone of the object
|
||||
* once complete.
|
||||
*/
|
||||
async duplicate(domainObject, parent, filter) {
|
||||
this.domainObject = domainObject;
|
||||
this.parent = parent;
|
||||
this.filter = filter || this.isCreatable;
|
||||
|
||||
await this.buildDuplicationPlan();
|
||||
await this.persistObjects();
|
||||
await this.addClonesToParent();
|
||||
|
||||
return this.firstClone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will build a graph of an object and all of its child objects in
|
||||
* memory
|
||||
* @private
|
||||
* @param domainObject The original object to be copied
|
||||
* @param parent The parent of the original object to be copied
|
||||
* @returns {Promise} resolved with an array of clones of the models
|
||||
* of the object tree being copied. Duplicating is done in a bottom-up
|
||||
* fashion, so that the last member in the array is a clone of the model
|
||||
* object being copied. The clones are all full composed with
|
||||
* references to their own children.
|
||||
*/
|
||||
async buildDuplicationPlan() {
|
||||
let domainObjectClone = await this.duplicateObject(this.domainObject);
|
||||
if (domainObjectClone !== this.domainObject) {
|
||||
domainObjectClone.location = this.getId(this.parent);
|
||||
}
|
||||
|
||||
this.firstClone = domainObjectClone;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will persist a list of {@link objectClones}. It will persist all
|
||||
* simultaneously, irrespective of order in the list. This may
|
||||
* result in automatic request batching by the browser.
|
||||
*/
|
||||
async persistObjects() {
|
||||
let initialCount = this.clones.length;
|
||||
let dialog = this.openmct.overlays.progressDialog({
|
||||
progressPerc: 0,
|
||||
message: `Duplicating ${initialCount} files.`,
|
||||
iconClass: 'info',
|
||||
title: 'Duplicating'
|
||||
});
|
||||
let clonesDone = Promise.all(this.clones.map(clone => {
|
||||
let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
|
||||
let message = `Duplicating ${initialCount - this.persisted} files.`;
|
||||
|
||||
dialog.updateProgress(percentPersisted, message);
|
||||
|
||||
return this.openmct.objects.save(clone);
|
||||
}));
|
||||
|
||||
await clonesDone;
|
||||
dialog.dismiss();
|
||||
this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will add a list of clones to the specified parent's composition
|
||||
*/
|
||||
async addClonesToParent() {
|
||||
let parentComposition = this.openmct.composition.get(this.parent);
|
||||
await parentComposition.load();
|
||||
parentComposition.add(this.firstClone);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recursive function that will perform a bottom-up duplicate of
|
||||
* the object tree with originalObject at the root. Recurses to
|
||||
* the farthest leaf, then works its way back up again,
|
||||
* cloning objects, and composing them with their child clones
|
||||
* as it goes
|
||||
* @private
|
||||
* @returns {DomainObject} If the type of the original object allows for
|
||||
* duplication, then a duplicate of the object, otherwise the object
|
||||
* itself (to allow linking to non duplicatable objects).
|
||||
*/
|
||||
async duplicateObject(originalObject) {
|
||||
// Check if the creatable (or other passed in filter).
|
||||
if (this.filter(originalObject)) {
|
||||
// Clone original object
|
||||
let clone = this.cloneObjectModel(originalObject);
|
||||
|
||||
// Get children, if any
|
||||
let composeesCollection = this.openmct.composition.get(originalObject);
|
||||
let composees;
|
||||
|
||||
if (composeesCollection) {
|
||||
composees = await composeesCollection.load();
|
||||
}
|
||||
|
||||
// Recursively duplicate children
|
||||
return this.duplicateComposees(clone, composees);
|
||||
}
|
||||
|
||||
// Not creatable, creating a link, no need to iterate children
|
||||
return originalObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update identifiers in a cloned object model (or part of
|
||||
* a cloned object model) to reflect new identifiers after
|
||||
* duplicating.
|
||||
* @private
|
||||
*/
|
||||
rewriteIdentifiers(obj, idMap) {
|
||||
function lookupValue(value) {
|
||||
return (typeof value === 'string' && idMap[value]) || value;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((value, index) => {
|
||||
obj[index] = lookupValue(value);
|
||||
this.rewriteIdentifiers(obj[index], idMap);
|
||||
});
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
let value = obj[key];
|
||||
obj[key] = lookupValue(value);
|
||||
if (idMap[key]) {
|
||||
delete obj[key];
|
||||
obj[idMap[key]] = value;
|
||||
}
|
||||
|
||||
this.rewriteIdentifiers(value, idMap);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of objects composed by a parent, clone them, then
|
||||
* add them to the parent.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
async duplicateComposees(clonedParent, composees = []) {
|
||||
let idMap = {};
|
||||
|
||||
let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
|
||||
await previousPromise;
|
||||
let clonedComposee = await this.duplicateObject(nextComposee);
|
||||
idMap[this.getId(nextComposee)] = this.getId(clonedComposee);
|
||||
await this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
|
||||
|
||||
return;
|
||||
}, Promise.resolve());
|
||||
|
||||
await allComposeesDuplicated;
|
||||
|
||||
this.rewriteIdentifiers(clonedParent, idMap);
|
||||
this.clones.push(clonedParent);
|
||||
|
||||
return clonedParent;
|
||||
}
|
||||
|
||||
async composeChild(child, parent, setLocation) {
|
||||
const PERSIST_BOOL = false;
|
||||
let parentComposition = this.openmct.composition.get(parent);
|
||||
await parentComposition.load();
|
||||
parentComposition.add(child, PERSIST_BOOL);
|
||||
|
||||
//If a location is not specified, set it.
|
||||
if (setLocation && child.location === undefined) {
|
||||
let parentKeyString = this.getId(parent);
|
||||
child.location = parentKeyString;
|
||||
}
|
||||
}
|
||||
|
||||
getTypeDefinition(domainObject, definition) {
|
||||
let typeDefinitions = this.openmct.types.get(domainObject.type).definition;
|
||||
|
||||
return typeDefinitions[definition] || false;
|
||||
}
|
||||
|
||||
cloneObjectModel(domainObject) {
|
||||
let clone = JSON.parse(JSON.stringify(domainObject));
|
||||
let identifier = {
|
||||
key: uuid(),
|
||||
namespace: domainObject.identifier.namespace
|
||||
};
|
||||
|
||||
if (clone.modified || clone.persisted || clone.location) {
|
||||
clone.modified = undefined;
|
||||
clone.persisted = undefined;
|
||||
clone.location = undefined;
|
||||
delete clone.modified;
|
||||
delete clone.persisted;
|
||||
delete clone.location;
|
||||
}
|
||||
|
||||
if (clone.composition) {
|
||||
clone.composition = [];
|
||||
}
|
||||
|
||||
clone.identifier = identifier;
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
getId(domainObject) {
|
||||
return this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
}
|
||||
|
||||
isCreatable(domainObject) {
|
||||
return this.getTypeDefinition(domainObject, 'creatable');
|
||||
}
|
||||
}
|
28
src/plugins/duplicate/plugin.js
Normal file
28
src/plugins/duplicate/plugin.js
Normal file
@ -0,0 +1,28 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import DuplicateAction from "./DuplicateAction";
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.actions.register(new DuplicateAction(openmct));
|
||||
};
|
||||
}
|
157
src/plugins/duplicate/pluginSpec.js
Normal file
157
src/plugins/duplicate/pluginSpec.js
Normal file
@ -0,0 +1,157 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import DuplicateActionPlugin from './plugin.js';
|
||||
import DuplicateAction from './DuplicateAction.js';
|
||||
import DuplicateTask from './DuplicateTask.js';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState,
|
||||
getMockObjects
|
||||
} from 'utils/testing';
|
||||
|
||||
describe("The Duplicate Action plugin", () => {
|
||||
|
||||
let openmct;
|
||||
let duplicateTask;
|
||||
let childObject;
|
||||
let parentObject;
|
||||
let anotherParentObject;
|
||||
|
||||
// this setups up the app
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
childObject = getMockObjects({
|
||||
objectKeyStrings: ['folder'],
|
||||
overwrite: {
|
||||
folder: {
|
||||
name: "Child Folder",
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "child-folder-object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}).folder;
|
||||
parentObject = getMockObjects({
|
||||
objectKeyStrings: ['folder'],
|
||||
overwrite: {
|
||||
folder: {
|
||||
name: "Parent Folder",
|
||||
composition: [childObject.identifier]
|
||||
}
|
||||
}
|
||||
}).folder;
|
||||
anotherParentObject = getMockObjects({
|
||||
objectKeyStrings: ['folder'],
|
||||
overwrite: {
|
||||
folder: {
|
||||
name: "Another Parent Folder"
|
||||
}
|
||||
}
|
||||
}).folder;
|
||||
|
||||
let objectGet = openmct.objects.get.bind(openmct.objects);
|
||||
spyOn(openmct.objects, 'get').and.callFake((identifier) => {
|
||||
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key);
|
||||
|
||||
if (!obj) {
|
||||
// not one of the mocked objs, callthrough basically
|
||||
return objectGet(identifier);
|
||||
}
|
||||
|
||||
return Promise.resolve(obj);
|
||||
});
|
||||
|
||||
spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
|
||||
return {
|
||||
load: async () => {
|
||||
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key);
|
||||
let children = [];
|
||||
|
||||
if (obj) {
|
||||
for (let i = 0; i < obj.composition.length; i++) {
|
||||
children.push(await openmct.objects.get(obj.composition[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(children);
|
||||
},
|
||||
add: (child) => {
|
||||
domainObject.composition.push(child.identifier);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// already installed by default, but never hurts, just adds to context menu
|
||||
openmct.install(DuplicateActionPlugin());
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(DuplicateActionPlugin).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when moving an object to a new parent", () => {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
duplicateTask = new DuplicateTask(openmct);
|
||||
await duplicateTask.duplicate(parentObject, anotherParentObject);
|
||||
done();
|
||||
});
|
||||
|
||||
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
|
||||
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
|
||||
let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier);
|
||||
let duplicateObjectName = duplicatedObject.name;
|
||||
|
||||
expect(duplicateObjectName).toEqual(parentObject.name);
|
||||
});
|
||||
|
||||
it("the duplicate child object's identifier should be new", () => {
|
||||
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
|
||||
|
||||
expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a new name is provided for the duplicated object", () => {
|
||||
const NEW_NAME = 'New Name';
|
||||
|
||||
beforeEach(() => {
|
||||
duplicateTask = new DuplicateAction(openmct);
|
||||
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
|
||||
});
|
||||
|
||||
it("the name is updated", () => {
|
||||
let childName = parentObject.name;
|
||||
expect(childName).toEqual(NEW_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user