mirror of
https://github.com/nasa/openmct.git
synced 2025-06-05 17:01:41 +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([
|
define([
|
||||||
"./src/actions/MoveAction",
|
"./src/actions/MoveAction",
|
||||||
"./src/actions/CopyAction",
|
|
||||||
"./src/actions/LinkAction",
|
"./src/actions/LinkAction",
|
||||||
"./src/actions/SetPrimaryLocationAction",
|
"./src/actions/SetPrimaryLocationAction",
|
||||||
"./src/services/LocatingCreationDecorator",
|
"./src/services/LocatingCreationDecorator",
|
||||||
@ -37,7 +36,6 @@ define([
|
|||||||
"./src/services/LocationService"
|
"./src/services/LocationService"
|
||||||
], function (
|
], function (
|
||||||
MoveAction,
|
MoveAction,
|
||||||
CopyAction,
|
|
||||||
LinkAction,
|
LinkAction,
|
||||||
SetPrimaryLocationAction,
|
SetPrimaryLocationAction,
|
||||||
LocatingCreationDecorator,
|
LocatingCreationDecorator,
|
||||||
@ -75,24 +73,6 @@ define([
|
|||||||
"moveService"
|
"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",
|
"key": "link",
|
||||||
"name": "Create 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',
|
'./api/Branding',
|
||||||
'./plugins/licenses/plugin',
|
'./plugins/licenses/plugin',
|
||||||
'./plugins/remove/plugin',
|
'./plugins/remove/plugin',
|
||||||
|
'./plugins/duplicate/plugin',
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -73,6 +74,7 @@ define([
|
|||||||
BrandingAPI,
|
BrandingAPI,
|
||||||
LicensesPlugin,
|
LicensesPlugin,
|
||||||
RemoveActionPlugin,
|
RemoveActionPlugin,
|
||||||
|
DuplicateActionPlugin,
|
||||||
Vue
|
Vue
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
@ -263,6 +265,7 @@ define([
|
|||||||
this.install(LegacyIndicatorsPlugin());
|
this.install(LegacyIndicatorsPlugin());
|
||||||
this.install(LicensesPlugin.default());
|
this.install(LicensesPlugin.default());
|
||||||
this.install(RemoveActionPlugin.default());
|
this.install(RemoveActionPlugin.default());
|
||||||
|
this.install(DuplicateActionPlugin.default());
|
||||||
this.install(this.plugins.FolderView());
|
this.install(this.plugins.FolderView());
|
||||||
this.install(this.plugins.Tabs());
|
this.install(this.plugins.Tabs());
|
||||||
this.install(ImageryPlugin.default());
|
this.install(ImageryPlugin.default());
|
||||||
|
@ -19,342 +19,352 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
|
||||||
|
import AutoflowTabularConstants from './AutoflowTabularConstants';
|
||||||
|
import $ from 'zepto';
|
||||||
|
import DOMObserver from './dom-observer';
|
||||||
|
import {
|
||||||
|
createOpenMct,
|
||||||
|
resetApplicationState,
|
||||||
|
spyOnBuiltins
|
||||||
|
} from 'utils/testing';
|
||||||
|
|
||||||
define([
|
describe("AutoflowTabularPlugin", () => {
|
||||||
'./AutoflowTabularPlugin',
|
let testType;
|
||||||
'./AutoflowTabularConstants',
|
let testObject;
|
||||||
'../../MCT',
|
let mockmct;
|
||||||
'zepto',
|
|
||||||
'./dom-observer'
|
|
||||||
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) {
|
|
||||||
describe("AutoflowTabularPlugin", function () {
|
|
||||||
let testType;
|
|
||||||
let testObject;
|
|
||||||
let mockmct;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
testType = "some-type";
|
testType = "some-type";
|
||||||
testObject = { type: testType };
|
testObject = { type: testType };
|
||||||
mockmct = new MCT();
|
mockmct = createOpenMct();
|
||||||
spyOn(mockmct.composition, 'get');
|
spyOn(mockmct.composition, 'get');
|
||||||
spyOn(mockmct.objectViews, 'addProvider');
|
spyOn(mockmct.objectViews, 'addProvider');
|
||||||
spyOn(mockmct.telemetry, 'getMetadata');
|
spyOn(mockmct.telemetry, 'getMetadata');
|
||||||
spyOn(mockmct.telemetry, 'getValueFormatter');
|
spyOn(mockmct.telemetry, 'getValueFormatter');
|
||||||
spyOn(mockmct.telemetry, 'limitEvaluator');
|
spyOn(mockmct.telemetry, 'limitEvaluator');
|
||||||
spyOn(mockmct.telemetry, 'request');
|
spyOn(mockmct.telemetry, 'request');
|
||||||
spyOn(mockmct.telemetry, 'subscribe');
|
spyOn(mockmct.telemetry, 'subscribe');
|
||||||
|
|
||||||
const plugin = new AutoflowTabularPlugin({ type: testType });
|
const plugin = new AutoflowTabularPlugin({ type: testType });
|
||||||
plugin(mockmct);
|
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 () {
|
it("applies its view to the type from options", () => {
|
||||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
expect(provider.canView(testObject)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("installs a view provider which", function () {
|
it("does not apply to other types", () => {
|
||||||
let provider;
|
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
describe("provides a view which", () => {
|
||||||
provider =
|
let testKeys;
|
||||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
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 () {
|
function waitsForChange() {
|
||||||
expect(provider.canView(testObject)).toBe(true);
|
return new Promise(function (resolve) {
|
||||||
});
|
window.requestAnimationFrame(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("does not apply to other types", function () {
|
function emitEvent(mockEmitter, type, event) {
|
||||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
mockEmitter.on.calls.all().forEach((call) => {
|
||||||
});
|
if (call.args[0] === type) {
|
||||||
|
call.args[1](event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("provides a view which", function () {
|
beforeEach((done) => {
|
||||||
let testKeys;
|
callbacks = {};
|
||||||
let testChildren;
|
|
||||||
let testContainer;
|
|
||||||
let testHistories;
|
|
||||||
let mockComposition;
|
|
||||||
let mockMetadata;
|
|
||||||
let mockEvaluator;
|
|
||||||
let mockUnsubscribes;
|
|
||||||
let callbacks;
|
|
||||||
let view;
|
|
||||||
let domObserver;
|
|
||||||
|
|
||||||
function waitsForChange() {
|
spyOnBuiltins(['requestAnimationFrame']);
|
||||||
return new Promise(function (resolve) {
|
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||||
window.requestAnimationFrame(resolve);
|
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) {
|
it("shows one row per child object", () => {
|
||||||
mockEmitter.on.calls.all().forEach(function (call) {
|
return domObserver.when(rowsMatch);
|
||||||
if (call.args[0] === type) {
|
});
|
||||||
call.args[1](event);
|
|
||||||
}
|
// 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 () {
|
return domObserver.when(widthHasChanged)
|
||||||
callbacks = {};
|
.then(() => {
|
||||||
|
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||||
testObject = { type: 'some-type' };
|
.toEqual(nextWidth + 'px');
|
||||||
testKeys = ['abc', 'def', 'xyz'];
|
|
||||||
testChildren = testKeys.map(function (key) {
|
|
||||||
return {
|
|
||||||
identifier: {
|
|
||||||
namespace: "test",
|
|
||||||
key: key
|
|
||||||
},
|
|
||||||
name: "Object " + key
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
testContainer = $('<div>')[0];
|
});
|
||||||
domObserver = new DOMObserver(testContainer);
|
|
||||||
|
|
||||||
testHistories = testKeys.reduce(function (histories, key, index) {
|
it("subscribes to all child objects", () => {
|
||||||
histories[key] = {
|
testKeys.forEach((key) => {
|
||||||
key: key,
|
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||||
range: index + 10,
|
});
|
||||||
domain: key + index
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return histories;
|
it("displays historical telemetry", () => {
|
||||||
}, {});
|
function rowTextDefined() {
|
||||||
|
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
mockComposition =
|
return domObserver.when(rowTextDefined).then(() => {
|
||||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
testKeys.forEach((key, index) => {
|
||||||
mockMetadata =
|
const datum = testHistories[key];
|
||||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
|
expect($cell.text()).toEqual(String(datum.range));
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
it("displays incoming telemetry", () => {
|
||||||
mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) {
|
const testData = testKeys.map((key, index) => {
|
||||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
return {
|
||||||
mockFormatter.format.and.callFake(function (datum) {
|
key: key,
|
||||||
return datum[metadatum.hint];
|
range: index * 100,
|
||||||
});
|
domain: key + index
|
||||||
|
};
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
testData.forEach((datum) => {
|
||||||
domObserver.destroy();
|
callbacks[datum.key](datum);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("populates its container", function () {
|
return waitsForChange().then(() => {
|
||||||
expect(testContainer.children.length > 0).toBe(true);
|
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 () {
|
it("updates classes for limit violations", () => {
|
||||||
function rowsMatch() {
|
const testClass = "some-limit-violation";
|
||||||
const rows = $(testContainer).find(".l-autoflow-row").length;
|
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||||
|
testKeys.forEach((key) => {
|
||||||
return rows === testChildren.length;
|
callbacks[key]({
|
||||||
}
|
range: 'foo',
|
||||||
|
domain: 'bar'
|
||||||
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("removes subscriptions when destroyed", function () {
|
return waitsForChange().then(() => {
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((datum, index) => {
|
||||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
});
|
expect($cell.hasClass(testClass)).toBe(true);
|
||||||
view.destroy();
|
|
||||||
testKeys.forEach(function (key) {
|
|
||||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("provides a button to change column width", function () {
|
it("automatically flows to new columns", () => {
|
||||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||||
const nextWidth =
|
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
const count = testKeys.length;
|
||||||
|
const $container = $(testContainer);
|
||||||
|
let promiseChain = Promise.resolve();
|
||||||
|
|
||||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
function columnsHaveAutoflowed() {
|
||||||
.toEqual(initialWidth + 'px');
|
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() {
|
$container.find('.abs').css({
|
||||||
const width = $(testContainer).find('.l-autoflow-col').css('width');
|
position: 'absolute',
|
||||||
|
left: '0px',
|
||||||
return width !== initialWidth + 'px';
|
right: '0px',
|
||||||
}
|
top: '0px',
|
||||||
|
bottom: '0px'
|
||||||
return domObserver.when(widthHasChanged)
|
|
||||||
.then(function () {
|
|
||||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
|
||||||
.toEqual(nextWidth + 'px');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
$container.css({ position: 'absolute' });
|
||||||
|
|
||||||
it("subscribes to all child objects", function () {
|
$container.appendTo(document.body);
|
||||||
testKeys.forEach(function (key) {
|
|
||||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
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 () {
|
it("loads composition exactly once", () => {
|
||||||
function rowTextDefined() {
|
const testObj = testChildren.pop();
|
||||||
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||||
}
|
testChildren.push(testObj);
|
||||||
|
emitEvent(mockComposition, 'add', testObj);
|
||||||
return domObserver.when(rowTextDefined).then(function () {
|
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
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