From 1ced47fc2c55f14b2fd338095b34c4dd10df9900 Mon Sep 17 00:00:00 2001 From: Victor Woeltjen Date: Thu, 19 May 2016 16:26:30 -0700 Subject: [PATCH] [Navigation] Prevent navigation to orphan objects This is particularly useful when a persistence failure has caused a created object not to be added to its parent container. #765 --- platform/commonUI/browse/bundle.js | 10 + .../src/navigation/OrphanNavigationHandler.js | 75 ++++++++ .../navigation/OrphanNavigationHandlerSpec.js | 180 ++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 platform/commonUI/browse/src/navigation/OrphanNavigationHandler.js create mode 100644 platform/commonUI/browse/test/navigation/OrphanNavigationHandlerSpec.js diff --git a/platform/commonUI/browse/bundle.js b/platform/commonUI/browse/bundle.js index 73b3f44de7..b7cf2dca71 100644 --- a/platform/commonUI/browse/bundle.js +++ b/platform/commonUI/browse/bundle.js @@ -30,6 +30,7 @@ define([ "./src/navigation/NavigationService", "./src/creation/CreationPolicy", "./src/navigation/NavigateAction", + "./src/navigation/OrphanNavigationHandler", "./src/windowing/NewTabAction", "./src/windowing/FullscreenAction", "./src/creation/CreateActionProvider", @@ -59,6 +60,7 @@ define([ NavigationService, CreationPolicy, NavigateAction, + OrphanNavigationHandler, NewTabAction, FullscreenAction, CreateActionProvider, @@ -346,6 +348,14 @@ define([ "$rootScope", "$document" ] + }, + { + "implementation": OrphanNavigationHandler, + "depends": [ + "throttle", + "topic", + "navigationService" + ] } ], "licenses": [ diff --git a/platform/commonUI/browse/src/navigation/OrphanNavigationHandler.js b/platform/commonUI/browse/src/navigation/OrphanNavigationHandler.js new file mode 100644 index 0000000000..00b3182e42 --- /dev/null +++ b/platform/commonUI/browse/src/navigation/OrphanNavigationHandler.js @@ -0,0 +1,75 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web 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 Web 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 () { + + /** + * Navigates away from orphan objects whenever they are detected. + * + * An orphan object is an object whose apparent parent does not + * actually contain it. This may occur in certain circumstances, such + * as when persistence succeeds for a newly-created object but fails + * for its parent. + * + * @param throttle the `throttle` service + * @param topic the `topic` service + * @param navigationService the `navigationService` + * @constructor + */ + function OrphanNavigationHandler(throttle, topic, navigationService) { + var throttledCheckNavigation; + + function getParent(domainObject) { + var context = domainObject.getCapability('context'); + return context.getParent(); + } + + function isOrphan(domainObject) { + var parent = getParent(domainObject), + composition = parent.getModel().composition, + id = domainObject.getId(); + return !composition || (composition.indexOf(id) === -1); + } + + function navigateToParent(domainObject) { + var parent = getParent(domainObject); + return parent.getCapability('action').perform('navigate'); + } + + function checkNavigation() { + var navigatedObject = navigationService.getNavigation(); + if (navigatedObject.hasCapability('context') && + isOrphan(navigatedObject)) { + if (!navigatedObject.getCapability('editor').isEditContextRoot()) { + navigateToParent(navigatedObject); + } + } + } + + throttledCheckNavigation = throttle(checkNavigation); + + navigationService.addListener(throttledCheckNavigation); + topic('mutation').listen(throttledCheckNavigation); + } + + return OrphanNavigationHandler; +}); diff --git a/platform/commonUI/browse/test/navigation/OrphanNavigationHandlerSpec.js b/platform/commonUI/browse/test/navigation/OrphanNavigationHandlerSpec.js new file mode 100644 index 0000000000..4f71feedbd --- /dev/null +++ b/platform/commonUI/browse/test/navigation/OrphanNavigationHandlerSpec.js @@ -0,0 +1,180 @@ +/***************************************************************************** +* Open MCT Web, Copyright (c) 2014-2015, United States Government +* as represented by the Administrator of the National Aeronautics and Space +* Administration. All rights reserved. +* +* Open MCT Web 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 Web 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/navigation/OrphanNavigationHandler' +], function (OrphanNavigationHandler) { + describe("OrphanNavigationHandler", function () { + var mockTopic, + mockThrottle, + mockMutationTopic, + mockNavigationService, + mockDomainObject, + mockParentObject, + mockContext, + mockActionCapability, + mockEditor, + testParentModel, + testId, + mockThrottledFns; + + beforeEach(function () { + testId = 'some-identifier'; + + mockThrottledFns = []; + testParentModel = {}; + + mockTopic = jasmine.createSpy('topic'); + mockThrottle = jasmine.createSpy('throttle'); + mockNavigationService = jasmine.createSpyObj('navigationService', [ + 'getNavigation', + 'addListener' + ]); + mockMutationTopic = jasmine.createSpyObj('mutationTopic', [ + 'listen' + ]); + mockDomainObject = jasmine.createSpyObj('domainObject', [ + 'getId', + 'getCapability', + 'getModel', + 'hasCapability' + ]); + mockParentObject = jasmine.createSpyObj('domainObject', [ + 'getId', + 'getCapability', + 'getModel', + 'hasCapability' + ]); + mockContext = jasmine.createSpyObj('context', ['getParent']); + mockActionCapability = jasmine.createSpyObj('action', ['perform']); + mockEditor = jasmine.createSpyObj('editor', ['isEditContextRoot']); + + mockThrottle.andCallFake(function (fn) { + var mockThrottledFn = + jasmine.createSpy('throttled-' + mockThrottledFns.length); + mockThrottledFn.andCallFake(fn); + mockThrottledFns.push(mockThrottledFn); + return mockThrottledFn; + }); + mockTopic.andCallFake(function (k) { + return k === 'mutation' && mockMutationTopic; + }); + mockDomainObject.getId.andReturn(testId); + mockDomainObject.getCapability.andCallFake(function (c) { + return { + context: mockContext, + editor: mockEditor + }[c]; + }); + mockDomainObject.hasCapability.andCallFake(function (c) { + return !!mockDomainObject.getCapability(c); + }); + mockParentObject.getModel.andReturn(testParentModel); + mockParentObject.getCapability.andCallFake(function (c) { + return { + action: mockActionCapability + }[c]; + }); + mockContext.getParent.andReturn(mockParentObject); + mockNavigationService.getNavigation.andReturn(mockDomainObject); + mockEditor.isEditContextRoot.andReturn(false); + + return new OrphanNavigationHandler( + mockThrottle, + mockTopic, + mockNavigationService + ); + }); + + + it("listens for mutation with a throttled function", function () { + expect(mockMutationTopic.listen) + .toHaveBeenCalledWith(jasmine.any(Function)); + expect(mockThrottledFns.indexOf( + mockMutationTopic.listen.mostRecentCall.args[0] + )).not.toEqual(-1); + }); + + it("listens for navigation changes with a throttled function", function () { + expect(mockNavigationService.addListener) + .toHaveBeenCalledWith(jasmine.any(Function)); + expect(mockThrottledFns.indexOf( + mockNavigationService.addListener.mostRecentCall.args[0] + )).not.toEqual(-1); + }); + + [false, true].forEach(function (isOrphan) { + var prefix = isOrphan ? "" : "non-"; + describe("for " + prefix + "orphan objects", function () { + beforeEach(function () { + testParentModel.composition = isOrphan ? [] : [testId]; + }); + + [false, true].forEach(function (isEditRoot) { + var caseName = isEditRoot ? + "that are being edited" : "that are not being edited"; + + function itNavigatesAsExpected() { + if (isOrphan && !isEditRoot) { + it("navigates to the parent", function () { + expect(mockActionCapability.perform) + .toHaveBeenCalledWith('navigate'); + }); + } else { + it("does nothing", function () { + expect(mockActionCapability.perform) + .not.toHaveBeenCalled(); + }); + } + } + + describe(caseName, function () { + beforeEach(function () { + mockEditor.isEditContextRoot.andReturn(isEditRoot); + }); + + describe("when navigation changes", function () { + beforeEach(function () { + mockNavigationService.addListener.mostRecentCall + .args[0](mockDomainObject); + }); + + itNavigatesAsExpected(); + }); + + describe("when mutation occurs", function () { + beforeEach(function () { + mockMutationTopic.listen.mostRecentCall + .args[0](mockParentObject); + }); + + itNavigatesAsExpected(); + }); + + }); + }); + }); + }); + + }); +}); +