From acea18fa70f06ca34e832abae49146330cd032f8 Mon Sep 17 00:00:00 2001 From: Jamie V Date: Tue, 24 Nov 2020 14:37:28 -0800 Subject: [PATCH] Move action (#3356) * WIP: added new move action plugin, added to default plugins in mct.js * WIP: removed old move action and references, added new root action, working, needs tess * added tests for move action * removing focused tests * WIP * using composition collection now, optimized some calls * removed test for removed function * minor spec change, format only * updated for new action registration and 3 dot * removing comments Co-authored-by: Shefali Joshi --- platform/entanglement/bundle.js | 36 --- .../entanglement/src/actions/MoveAction.js | 59 ---- .../entanglement/src/services/MoveService.js | 104 ------- .../test/actions/MoveActionSpec.js | 178 ------------ .../test/policies/MovePolicySpec.js | 124 --------- .../test/services/MockMoveService.js | 96 ------- .../test/services/MoveServiceSpec.js | 260 ------------------ src/MCT.js | 3 + src/plugins/move/MoveAction.js | 166 +++++++++++ .../plugins/move/plugin.js | 45 +-- src/plugins/move/pluginSpec.js | 110 ++++++++ 11 files changed, 284 insertions(+), 897 deletions(-) delete mode 100644 platform/entanglement/src/actions/MoveAction.js delete mode 100644 platform/entanglement/src/services/MoveService.js delete mode 100644 platform/entanglement/test/actions/MoveActionSpec.js delete mode 100644 platform/entanglement/test/policies/MovePolicySpec.js delete mode 100644 platform/entanglement/test/services/MockMoveService.js delete mode 100644 platform/entanglement/test/services/MoveServiceSpec.js create mode 100644 src/plugins/move/MoveAction.js rename platform/entanglement/src/policies/MovePolicy.js => src/plugins/move/plugin.js (53%) create mode 100644 src/plugins/move/pluginSpec.js diff --git a/platform/entanglement/bundle.js b/platform/entanglement/bundle.js index 23da011594..b590d9a7c4 100644 --- a/platform/entanglement/bundle.js +++ b/platform/entanglement/bundle.js @@ -21,30 +21,24 @@ *****************************************************************************/ define([ - "./src/actions/MoveAction", "./src/actions/LinkAction", "./src/actions/SetPrimaryLocationAction", "./src/services/LocatingCreationDecorator", "./src/services/LocatingObjectDecorator", "./src/policies/CopyPolicy", "./src/policies/CrossSpacePolicy", - "./src/policies/MovePolicy", "./src/capabilities/LocationCapability", - "./src/services/MoveService", "./src/services/LinkService", "./src/services/CopyService", "./src/services/LocationService" ], function ( - MoveAction, LinkAction, SetPrimaryLocationAction, LocatingCreationDecorator, LocatingObjectDecorator, CopyPolicy, CrossSpacePolicy, - MovePolicy, LocationCapability, - MoveService, LinkService, CopyService, LocationService @@ -58,21 +52,6 @@ define([ "configuration": {}, "extensions": { "actions": [ - { - "key": "move", - "name": "Move", - "description": "Move object to another location.", - "cssClass": "icon-move", - "category": "contextual", - "group": "action", - "priority": 9, - "implementation": MoveAction, - "depends": [ - "policyService", - "locationService", - "moveService" - ] - }, { "key": "link", "name": "Create Link", @@ -121,10 +100,6 @@ define([ { "category": "action", "implementation": CopyPolicy - }, - { - "category": "action", - "implementation": MovePolicy } ], "capabilities": [ @@ -140,17 +115,6 @@ define([ } ], "services": [ - { - "key": "moveService", - "name": "Move Service", - "description": "Provides a service for moving objects", - "implementation": MoveService, - "depends": [ - "openmct", - "linkService", - "$q" - ] - }, { "key": "linkService", "name": "Link Service", diff --git a/platform/entanglement/src/actions/MoveAction.js b/platform/entanglement/src/actions/MoveAction.js deleted file mode 100644 index 1147d9c116..0000000000 --- a/platform/entanglement/src/actions/MoveAction.js +++ /dev/null @@ -1,59 +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'], - function (AbstractComposeAction) { - - /** - * The MoveAction is available from context menus and allows a user to - * move an object to another location of their choosing. - * - * @implements {Action} - * @constructor - * @memberof platform/entanglement - */ - function MoveAction(policyService, locationService, moveService, context) { - AbstractComposeAction.apply( - this, - [policyService, locationService, moveService, context, "Move"] - ); - } - - MoveAction.prototype = Object.create(AbstractComposeAction.prototype); - - MoveAction.appliesTo = function (context) { - var applicableObject = - context.selectedObject || context.domainObject; - - if (applicableObject && applicableObject.model.locked) { - return false; - } - - return Boolean(applicableObject - && applicableObject.hasCapability('context')); - }; - - return MoveAction; - } -); - diff --git a/platform/entanglement/src/services/MoveService.js b/platform/entanglement/src/services/MoveService.js deleted file mode 100644 index da6e3161ac..0000000000 --- a/platform/entanglement/src/services/MoveService.js +++ /dev/null @@ -1,104 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2020, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - function () { - /** - * MoveService provides an interface for moving objects from one - * location to another. It also provides a method for determining if - * an object can be copied to a specific location. - * @constructor - * @memberof platform/entanglement - * @implements {platform/entanglement.AbstractComposeService} - */ - function MoveService(openmct, linkService) { - this.openmct = openmct; - this.linkService = linkService; - } - - MoveService.prototype.validate = function (object, parentCandidate) { - var currentParent = object - .getCapability('context') - .getParent(); - - if (!parentCandidate || !parentCandidate.getId) { - return false; - } - - if (parentCandidate.getId() === currentParent.getId()) { - return false; - } - - if (parentCandidate.getId() === object.getId()) { - return false; - } - - if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) { - return false; - } - - return this.openmct.composition.checkPolicy( - parentCandidate.useCapability('adapter'), - object.useCapability('adapter') - ); - }; - - MoveService.prototype.perform = function (object, parentObject) { - function relocate(objectInNewContext) { - var newLocationCapability = objectInNewContext - .getCapability('location'), - oldLocationCapability = object - .getCapability('location'); - - if (!newLocationCapability - || !oldLocationCapability) { - return; - } - - if (oldLocationCapability.isOriginal()) { - return newLocationCapability.setPrimaryLocation( - newLocationCapability - .getContextualLocation() - ); - } - } - - if (!this.validate(object, parentObject)) { - throw new Error( - "Tried to move objects without validating first." - ); - } - - return this.linkService - .perform(object, parentObject) - .then(relocate) - .then(function () { - return object - .getCapability('action') - .perform('remove', true); - }); - }; - - return MoveService; - } -); - diff --git a/platform/entanglement/test/actions/MoveActionSpec.js b/platform/entanglement/test/actions/MoveActionSpec.js deleted file mode 100644 index 10c4d66660..0000000000 --- a/platform/entanglement/test/actions/MoveActionSpec.js +++ /dev/null @@ -1,178 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2020, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - '../../src/actions/MoveAction', - '../services/MockMoveService', - '../DomainObjectFactory' - ], - function (MoveAction, MockMoveService, domainObjectFactory) { - - describe("Move Action", function () { - - var moveAction, - policyService, - locationService, - locationServicePromise, - moveService, - context, - selectedObject, - selectedObjectContextCapability, - currentParent, - newParent; - - beforeEach(function () { - policyService = jasmine.createSpyObj( - 'policyService', - ['allow'] - ); - policyService.allow.and.returnValue(true); - - selectedObjectContextCapability = jasmine.createSpyObj( - 'selectedObjectContextCapability', - [ - 'getParent' - ] - ); - - selectedObject = domainObjectFactory({ - name: 'selectedObject', - model: { - name: 'selectedObject' - }, - capabilities: { - context: selectedObjectContextCapability - } - }); - - currentParent = domainObjectFactory({ - name: 'currentParent' - }); - - selectedObjectContextCapability - .getParent - .and.returnValue(currentParent); - - newParent = domainObjectFactory({ - name: 'newParent' - }); - - locationService = jasmine.createSpyObj( - 'locationService', - [ - 'getLocationFromUser' - ] - ); - - locationServicePromise = jasmine.createSpyObj( - 'locationServicePromise', - [ - 'then' - ] - ); - - locationService - .getLocationFromUser - .and.returnValue(locationServicePromise); - - moveService = new MockMoveService(); - }); - - describe("with context from context-action", function () { - beforeEach(function () { - context = { - domainObject: selectedObject - }; - - moveAction = new MoveAction( - policyService, - locationService, - moveService, - context - ); - }); - - it("initializes happily", function () { - expect(moveAction).toBeDefined(); - }); - - describe("when performed it", function () { - beforeEach(function () { - moveAction.perform(); - }); - - it("prompts for location", function () { - expect(locationService.getLocationFromUser) - .toHaveBeenCalledWith( - "Move selectedObject To a New Location", - "Move To", - jasmine.any(Function), - currentParent - ); - }); - - it("waits for location and handles cancellation by user", function () { - expect(locationServicePromise.then) - .toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function)); - }); - - it("moves object to selected location", function () { - locationServicePromise - .then - .calls.mostRecent() - .args[0](newParent); - - expect(moveService.perform) - .toHaveBeenCalledWith(selectedObject, newParent); - }); - }); - }); - - describe("with context from drag-drop", function () { - beforeEach(function () { - context = { - selectedObject: selectedObject, - domainObject: newParent - }; - - moveAction = new MoveAction( - policyService, - locationService, - moveService, - context - ); - }); - - it("initializes happily", function () { - expect(moveAction).toBeDefined(); - }); - - it("performs move immediately", function () { - moveAction.perform(); - expect(moveService.perform) - .toHaveBeenCalledWith(selectedObject, newParent); - }); - }); - }); - } -); diff --git a/platform/entanglement/test/policies/MovePolicySpec.js b/platform/entanglement/test/policies/MovePolicySpec.js deleted file mode 100644 index 26750476df..0000000000 --- a/platform/entanglement/test/policies/MovePolicySpec.js +++ /dev/null @@ -1,124 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2020, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define([ - '../../src/policies/MovePolicy', - '../DomainObjectFactory' -], function (MovePolicy, domainObjectFactory) { - - describe("MovePolicy", function () { - var testMetadata, - testContext, - mockDomainObject, - mockParent, - mockParentType, - mockType, - mockAction, - policy; - - beforeEach(function () { - var mockContextCapability = - jasmine.createSpyObj('context', ['getParent']); - - mockType = - jasmine.createSpyObj('type', ['hasFeature']); - mockParentType = - jasmine.createSpyObj('parent-type', ['hasFeature']); - - testMetadata = {}; - - mockDomainObject = domainObjectFactory({ - capabilities: { - context: mockContextCapability, - type: mockType - } - }); - mockParent = domainObjectFactory({ - capabilities: { - type: mockParentType - } - }); - - mockContextCapability.getParent.and.returnValue(mockParent); - - mockType.hasFeature.and.callFake(function (feature) { - return feature === 'creation'; - }); - mockParentType.hasFeature.and.callFake(function (feature) { - return feature === 'creation'; - }); - - mockAction = jasmine.createSpyObj('action', ['getMetadata']); - mockAction.getMetadata.and.returnValue(testMetadata); - - testContext = { domainObject: mockDomainObject }; - - policy = new MovePolicy(); - }); - - describe("for move actions", function () { - beforeEach(function () { - testMetadata.key = 'move'; - }); - - describe("when an object is non-modifiable", function () { - beforeEach(function () { - mockType.hasFeature.and.returnValue(false); - }); - - it("disallows the action", function () { - expect(policy.allow(mockAction, testContext)).toBe(false); - }); - }); - - describe("when a parent is non-modifiable", function () { - beforeEach(function () { - mockParentType.hasFeature.and.returnValue(false); - }); - - it("disallows the action", function () { - expect(policy.allow(mockAction, testContext)).toBe(false); - }); - }); - - describe("when an object and its parent are modifiable", function () { - it("allows the action", function () { - expect(policy.allow(mockAction, testContext)).toBe(true); - }); - }); - }); - - describe("for other actions", function () { - beforeEach(function () { - testMetadata.key = 'foo'; - }); - - it("simply allows the action", function () { - expect(policy.allow(mockAction, testContext)).toBe(true); - mockType.hasFeature.and.returnValue(false); - expect(policy.allow(mockAction, testContext)).toBe(true); - mockParentType.hasFeature.and.returnValue(false); - expect(policy.allow(mockAction, testContext)).toBe(true); - }); - }); - }); -}); diff --git a/platform/entanglement/test/services/MockMoveService.js b/platform/entanglement/test/services/MockMoveService.js deleted file mode 100644 index 6d75d29246..0000000000 --- a/platform/entanglement/test/services/MockMoveService.js +++ /dev/null @@ -1,96 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2020, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - function () { - - /** - * MockMoveService provides the same interface as the moveService, - * returning promises where it would normally do so. At it's core, - * it is a jasmine spy object, but it also tracks the promises it - * returns and provides shortcut methods for resolving those promises - * synchronously. - * - * Usage: - * - * ```javascript - * var moveService = new MockMoveService(); - * - * // validate is a standard jasmine spy. - * moveService.validate.and.returnValue(true); - * var isValid = moveService.validate(object, parentCandidate); - * expect(isValid).toBe(true); - * - * // perform returns promises and tracks them. - * var whenCopied = jasmine.createSpy('whenCopied'); - * moveService.perform(object, parentObject).then(whenCopied); - * expect(whenCopied).not.toHaveBeenCalled(); - * moveService.perform.calls.mostRecent().resolve('someArg'); - * expect(whenCopied).toHaveBeenCalledWith('someArg'); - * ``` - */ - function MockMoveService() { - // track most recent call of a function, - // perform automatically returns - var mockMoveService = jasmine.createSpyObj( - 'MockMoveService', - [ - 'validate', - 'perform' - ] - ); - - mockMoveService.perform.and.callFake(() => { - var performPromise, - callExtensions, - spy; - - performPromise = jasmine.createSpyObj( - 'performPromise', - ['then'] - ); - - callExtensions = { - promise: performPromise, - resolve: function (resolveWith) { - performPromise.then.calls.all().forEach(function (call) { - call.args[0](resolveWith); - }); - } - }; - - spy = mockMoveService.perform; - - Object.keys(callExtensions).forEach(function (key) { - spy.calls.mostRecent()[key] = callExtensions[key]; - spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key]; - }); - - return performPromise; - }); - - return mockMoveService; - } - - return MockMoveService; - } -); diff --git a/platform/entanglement/test/services/MoveServiceSpec.js b/platform/entanglement/test/services/MoveServiceSpec.js deleted file mode 100644 index 53d893345f..0000000000 --- a/platform/entanglement/test/services/MoveServiceSpec.js +++ /dev/null @@ -1,260 +0,0 @@ -/***************************************************************************** - * Open MCT, Copyright (c) 2014-2020, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT is licensed under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - * - * Open MCT includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ - -define( - [ - '../../src/services/MoveService', - '../services/MockLinkService', - '../DomainObjectFactory', - '../ControlledPromise' - ], - function ( - MoveService, - MockLinkService, - domainObjectFactory, - ControlledPromise - ) { - - xdescribe("MoveService", function () { - - var moveService, - policyService, - object, - objectContextCapability, - currentParent, - parentCandidate, - linkService; - - beforeEach(function () { - objectContextCapability = jasmine.createSpyObj( - 'objectContextCapability', - [ - 'getParent' - ] - ); - - object = domainObjectFactory({ - name: 'object', - id: 'a', - capabilities: { - context: objectContextCapability, - type: { type: 'object' } - } - }); - - currentParent = domainObjectFactory({ - name: 'currentParent', - id: 'b' - }); - - objectContextCapability.getParent.and.returnValue(currentParent); - - parentCandidate = domainObjectFactory({ - name: 'parentCandidate', - model: { composition: [] }, - id: 'c', - capabilities: { - type: { type: 'parentCandidate' } - } - }); - policyService = jasmine.createSpyObj( - 'policyService', - ['allow'] - ); - linkService = new MockLinkService(); - policyService.allow.and.returnValue(true); - moveService = new MoveService(policyService, linkService); - }); - - describe("validate", function () { - var validate; - - beforeEach(function () { - validate = function () { - return moveService.validate(object, parentCandidate); - }; - }); - - it("does not allow an invalid parent", function () { - parentCandidate = undefined; - expect(validate()).toBe(false); - parentCandidate = {}; - expect(validate()).toBe(false); - }); - - it("does not allow moving to current parent", function () { - parentCandidate.id = currentParent.id = 'xyz'; - expect(validate()).toBe(false); - }); - - it("does not allow moving to self", function () { - object.id = parentCandidate.id = 'xyz'; - expect(validate()).toBe(false); - }); - - it("does not allow moving to the same location", function () { - object.id = 'abc'; - parentCandidate.model.composition = ['abc']; - expect(validate()).toBe(false); - }); - - describe("defers to policyService", function () { - - it("calls policy service with correct args", function () { - validate(); - expect(policyService.allow).toHaveBeenCalledWith( - "composition", - parentCandidate, - object - ); - }); - - it("and returns false", function () { - policyService.allow.and.returnValue(false); - expect(validate()).toBe(false); - }); - - it("and returns true", function () { - policyService.allow.and.returnValue(true); - expect(validate()).toBe(true); - }); - }); - }); - - describe("perform", function () { - - var actionCapability, - locationCapability, - locationPromise, - newParent, - moveResult; - - beforeEach(function () { - newParent = parentCandidate; - - actionCapability = jasmine.createSpyObj( - 'actionCapability', - ['perform'] - ); - - locationCapability = jasmine.createSpyObj( - 'locationCapability', - [ - 'isOriginal', - 'setPrimaryLocation', - 'getContextualLocation' - ] - ); - - locationPromise = new ControlledPromise(); - locationCapability.setPrimaryLocation - .and.returnValue(locationPromise); - - object = domainObjectFactory({ - name: 'object', - capabilities: { - action: actionCapability, - location: locationCapability, - context: objectContextCapability, - type: { type: 'object' } - } - }); - moveResult = moveService.perform(object, newParent); - }); - - it("links object to newParent", function () { - expect(linkService.perform).toHaveBeenCalledWith( - object, - newParent - ); - }); - - it("returns a promise", function () { - expect(moveResult.then).toEqual(jasmine.any(Function)); - }); - - it("waits for result of link", function () { - expect(linkService.perform.calls.mostRecent().promise.then) - .toHaveBeenCalledWith(jasmine.any(Function)); - }); - - it("throws an error when performed on invalid inputs", function () { - function perform() { - moveService.perform(object, newParent); - } - - spyOn(moveService, "validate"); - moveService.validate.and.returnValue(true); - expect(perform).not.toThrow(); - moveService.validate.and.returnValue(false); - expect(perform).toThrow(); - }); - - describe("when moving an original", function () { - beforeEach(function () { - locationCapability.getContextualLocation - .and.returnValue('new-location'); - locationCapability.isOriginal.and.returnValue(true); - linkService.perform.calls.mostRecent().promise.resolve(); - }); - - it("updates location", function () { - expect(locationCapability.setPrimaryLocation) - .toHaveBeenCalledWith('new-location'); - }); - - describe("after location update", function () { - beforeEach(function () { - locationPromise.resolve(); - }); - - it("removes object from parent without user warning dialog", function () { - expect(actionCapability.perform) - .toHaveBeenCalledWith('remove', true); - }); - - }); - - }); - - describe("when moving a link", function () { - beforeEach(function () { - locationCapability.isOriginal.and.returnValue(false); - linkService.perform.calls.mostRecent().promise.resolve(); - }); - - it("does not update location", function () { - expect(locationCapability.setPrimaryLocation) - .not - .toHaveBeenCalled(); - }); - - it("removes object from parent without user warning dialog", function () { - expect(actionCapability.perform) - .toHaveBeenCalledWith('remove', true); - }); - }); - - }); - }); - } -); diff --git a/src/MCT.js b/src/MCT.js index 0e07aa8d5c..a5d6ce88b1 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -46,6 +46,7 @@ define([ './api/Branding', './plugins/licenses/plugin', './plugins/remove/plugin', + './plugins/move/plugin', './plugins/duplicate/plugin', 'vue' ], function ( @@ -74,6 +75,7 @@ define([ BrandingAPI, LicensesPlugin, RemoveActionPlugin, + MoveActionPlugin, DuplicateActionPlugin, Vue ) { @@ -265,6 +267,7 @@ define([ this.install(LegacyIndicatorsPlugin()); this.install(LicensesPlugin.default()); this.install(RemoveActionPlugin.default()); + this.install(MoveActionPlugin.default()); this.install(DuplicateActionPlugin.default()); this.install(this.plugins.FolderView()); this.install(this.plugins.Tabs()); diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js new file mode 100644 index 0000000000..2a46053074 --- /dev/null +++ b/src/plugins/move/MoveAction.js @@ -0,0 +1,166 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +export default class MoveAction { + constructor(openmct) { + this.name = 'Move'; + this.key = 'move'; + this.description = 'Move this object from its containing object to another object.'; + this.cssClass = "icon-move"; + this.group = "action"; + this.priority = 7; + + this.openmct = openmct; + } + + async invoke(objectPath) { + let object = objectPath[0]; + let inNavigationPath = this.inNavigationPath(object); + let oldParent = objectPath[1]; + let dialogService = this.openmct.$injector.get('dialogService'); + let dialogForm = this.getDialogForm(object, oldParent); + let userInput = await dialogService.getUserInput(dialogForm, { name: object.name }); + + // if we need to update name + if (object.name !== userInput.name) { + this.openmct.objects.mutate(object, 'name', userInput.name); + } + + let parentContext = userInput.location.getCapability('context'); + let newParent = await this.openmct.objects.get(parentContext.domainObject.id); + + if (inNavigationPath && this.openmct.editor.isEditing()) { + this.openmct.editor.save(); + } + + this.addToNewParent(object, newParent); + this.removeFromOldParent(oldParent, object); + + if (inNavigationPath) { + let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier); + let root = await this.openmct.objects.getRoot(); + let rootChildCount = root.composition.length; + + // if not multiple root children, remove root from path + if (rootChildCount < 2) { + newObjectPath.pop(); // remove ROOT + } + + this.navigateTo(newObjectPath); + } + } + + inNavigationPath(object) { + return this.openmct.router.path + .some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier)); + } + + navigateTo(objectPath) { + let urlPath = objectPath.reverse() + .map(object => this.openmct.objects.makeKeyString(object.identifier)) + .join("/"); + + window.location.href = '#/browse/' + urlPath; + } + + addToNewParent(child, newParent) { + let newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier); + let compositionCollection = this.openmct.composition.get(newParent); + + this.openmct.objects.mutate(child, 'location', newParentKeyString); + compositionCollection.add(child); + } + + removeFromOldParent(parent, child) { + let compositionCollection = this.openmct.composition.get(parent); + + compositionCollection.remove(child); + } + + getDialogForm(object, parent) { + return { + name: "Move Item", + sections: [ + { + rows: [ + { + key: "name", + control: "textfield", + name: "Folder Name", + pattern: "\\S+", + required: true, + cssClass: "l-input-lg" + }, + { + name: "location", + control: "locator", + validate: this.validate(object, parent), + key: 'location' + } + ] + } + ] + }; + } + + validate(object, currentParent) { + return (parentCandidate) => { + let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier); + let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId()); + let objectKeystring = this.openmct.objects.makeKeyString(object.identifier); + + if (!parentCandidateKeystring || !currentParentKeystring) { + return false; + } + + if (parentCandidateKeystring === currentParentKeystring) { + return false; + } + + if (parentCandidateKeystring === objectKeystring) { + return false; + } + + if (parentCandidate.getModel().composition.indexOf(objectKeystring) !== -1) { + return false; + } + + return this.openmct.composition.checkPolicy( + parentCandidate.useCapability('adapter'), + object + ); + }; + } + + appliesTo(objectPath) { + let parent = objectPath[1]; + let parentType = parent && this.openmct.types.get(parent.type); + let child = objectPath[0]; + + if (child.locked || (parent && parent.locked)) { + return false; + } + + return parentType + && parentType.definition.creatable + && Array.isArray(parent.composition); + } +} diff --git a/platform/entanglement/src/policies/MovePolicy.js b/src/plugins/move/plugin.js similarity index 53% rename from platform/entanglement/src/policies/MovePolicy.js rename to src/plugins/move/plugin.js index 87d6d5e96d..b7757229d9 100644 --- a/platform/entanglement/src/policies/MovePolicy.js +++ b/src/plugins/move/plugin.js @@ -19,45 +19,10 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ +import MoveAction from "./MoveAction"; -define([], function () { - - /** - * Disallow moves when either the parent or the child are not - * modifiable by users. - * @constructor - * @implements {Policy} - * @memberof platform/entanglement - */ - function MovePolicy() { - } - - function parentOf(domainObject) { - var context = domainObject.getCapability('context'); - - return context && context.getParent(); - } - - function allowMutation(domainObject) { - var type = domainObject && domainObject.getCapability('type'); - - return Boolean(type && type.hasFeature('creation')); - } - - function selectedObject(context) { - return context.selectedObject || context.domainObject; - } - - MovePolicy.prototype.allow = function (action, context) { - var key = action.getMetadata().key; - - if (key === 'move') { - return allowMutation(selectedObject(context)) - && allowMutation(parentOf(selectedObject(context))); - } - - return true; +export default function () { + return function (openmct) { + openmct.actions.register(new MoveAction(openmct)); }; - - return MovePolicy; -}); +} diff --git a/src/plugins/move/pluginSpec.js b/src/plugins/move/pluginSpec.js new file mode 100644 index 0000000000..29798e031b --- /dev/null +++ b/src/plugins/move/pluginSpec.js @@ -0,0 +1,110 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2020, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import MoveActionPlugin from './plugin.js'; +import MoveAction from './MoveAction.js'; +import { + createOpenMct, + resetApplicationState, + getMockObjects +} from 'utils/testing'; + +describe("The Move Action plugin", () => { + + let openmct; + let moveAction; + let childObject; + let parentObject; + let anotherParentObject; + + // this setups up the app + beforeEach((done) => { + const appHolder = document.createElement('div'); + appHolder.style.width = '640px'; + appHolder.style.height = '480px'; + + openmct = createOpenMct(); + + childObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: "Child Folder", + identifier: { + namespace: "", + key: "child-folder-object" + } + } + } + }).folder; + parentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: "Parent Folder", + composition: [childObject.identifier] + } + } + }).folder; + anotherParentObject = getMockObjects({ + objectKeyStrings: ['folder'], + overwrite: { + folder: { + name: "Another Parent Folder" + } + } + }).folder; + + // already installed by default, but never hurts, just adds to context menu + openmct.install(MoveActionPlugin()); + + openmct.on('start', done); + openmct.startHeadless(appHolder); + }); + + afterEach(() => { + resetApplicationState(openmct); + }); + + it("should be defined", () => { + expect(MoveActionPlugin).toBeDefined(); + }); + + describe("when moving an object to a new parent and removing from the old parent", () => { + + beforeEach(() => { + moveAction = new MoveAction(openmct); + moveAction.addToNewParent(childObject, anotherParentObject); + moveAction.removeFromOldParent(parentObject, childObject); + }); + + it("the child object's identifier should be in the new parent's composition", () => { + let newParentChild = anotherParentObject.composition[0]; + expect(newParentChild).toEqual(childObject.identifier); + }); + + it("the child object's identifier should be removed from the old parent's composition", () => { + let oldParentComposition = parentObject.composition; + expect(oldParentComposition.length).toEqual(0); + }); + }); + +});