diff --git a/platform/entanglement/bundle.js b/platform/entanglement/bundle.js
index 9715ba20c0..23da011594 100644
--- a/platform/entanglement/bundle.js
+++ b/platform/entanglement/bundle.js
@@ -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",
diff --git a/platform/entanglement/src/actions/CopyAction.js b/platform/entanglement/src/actions/CopyAction.js
deleted file mode 100644
index 2cd26826f5..0000000000
--- a/platform/entanglement/src/actions/CopyAction.js
+++ /dev/null
@@ -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;
- }
-);
diff --git a/platform/entanglement/test/actions/CopyActionSpec.js b/platform/entanglement/test/actions/CopyActionSpec.js
deleted file mode 100644
index 8556230f15..0000000000
--- a/platform/entanglement/test/actions/CopyActionSpec.js
+++ /dev/null
@@ -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);
- });
- });
- });
- }
-);
diff --git a/src/MCT.js b/src/MCT.js
index e3ed1ab945..0e07aa8d5c 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -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());
diff --git a/src/plugins/autoflow/AutoflowTabularPluginSpec.js b/src/plugins/autoflow/AutoflowTabularPluginSpec.js
index eb7aea1e1f..1cde374190 100644
--- a/src/plugins/autoflow/AutoflowTabularPluginSpec.js
+++ b/src/plugins/autoflow/AutoflowTabularPluginSpec.js
@@ -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 = $('
')[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 = $('
')[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);
});
});
});
diff --git a/src/plugins/duplicate/DuplicateAction.js b/src/plugins/duplicate/DuplicateAction.js
new file mode 100644
index 0000000000..e398b6c041
--- /dev/null
+++ b/src/plugins/duplicate/DuplicateAction.js
@@ -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);
+ }
+}
diff --git a/src/plugins/duplicate/DuplicateTask.js b/src/plugins/duplicate/DuplicateTask.js
new file mode 100644
index 0000000000..678a8fc5c1
--- /dev/null
+++ b/src/plugins/duplicate/DuplicateTask.js
@@ -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');
+ }
+}
diff --git a/src/plugins/duplicate/plugin.js b/src/plugins/duplicate/plugin.js
new file mode 100644
index 0000000000..0f07cc579a
--- /dev/null
+++ b/src/plugins/duplicate/plugin.js
@@ -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));
+ };
+}
diff --git a/src/plugins/duplicate/pluginSpec.js b/src/plugins/duplicate/pluginSpec.js
new file mode 100644
index 0000000000..07af1bb78f
--- /dev/null
+++ b/src/plugins/duplicate/pluginSpec.js
@@ -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);
+ });
+ });
+
+});