diff --git a/client/luigi-client.d.ts b/client/luigi-client.d.ts index 9b472ada5c..9c2555bbe4 100644 --- a/client/luigi-client.d.ts +++ b/client/luigi-client.d.ts @@ -304,12 +304,7 @@ export declare interface LinkManager { * LuigiClient.linkManager().navigate('/settings', null, true) // preserve view * LuigiClient.linkManager().navigate('#?intent=Sales-order?id=13') // intent navigation */ - navigate: ( - path: string, - sessionId?: string, - preserveView?: boolean, - modalSettings?: ModalSettings - ) => void; + navigate: (path: string, sessionId?: string, preserveView?: boolean, modalSettings?: ModalSettings) => void; /** @lends linkManager */ /** @@ -367,10 +362,7 @@ export declare interface LinkManager { * @example * const splitViewHandle = LuigiClient.linkManager().openAsSplitView('projects/pr1/logs', {title: 'Logs', size: 40, collapsed: true}); */ - openAsSplitView: ( - path: string, - splitViewSettings?: SplitViewSettings - ) => SplitViewInstance; + openAsSplitView: (path: string, splitViewSettings?: SplitViewSettings) => SplitViewInstance; /** * Opens a view in a drawer. You can specify if the drawer has a header, if a backdrop is active in the background and configure the size of the drawer. By default the header is shown. The backdrop is not visible and has to be activated. The size of the drawer is by default set to `s` which means 25% of the micro frontend size. You can also use `l`(75%), `m`(50%) or `xs`(15.5%). Optionally, use it in combination with any of the navigation functions. @@ -397,6 +389,14 @@ export declare interface LinkManager { * LuigiClient.linkManager().withoutSync().fromClosestContext().navigate('settings'); */ withoutSync: () => this; + + /** + * Enables navigating to a new tab. + * @since NEXT_RELEASE + * @example + * LuigiClient.linkManager().newTab().navigate('/projects/xy/foobar'); + */ + newTab: () => this; } export declare interface StorageManager { @@ -471,12 +471,8 @@ export declare interface StorageManager { * @param {Lifecycle~initListenerCallback} initFn the function that is called once Luigi is initialized, receives current context and origin as parameters * @memberof Lifecycle */ -export function addInitListener( - initFn: (context: Context, origin?: string) => void -): number; -export type addInitListener = ( - initFn: (context: Context, origin?: string) => void -) => number; +export function addInitListener(initFn: (context: Context, origin?: string) => void): number; +export type addInitListener = (initFn: (context: Context, origin?: string) => void) => number; /** * Callback of the addInitListener @@ -497,12 +493,8 @@ export type removeInitListener = (id: number) => boolean; * @param {function} contextUpdatedFn the listener function called each time Luigi context changes * @memberof Lifecycle */ -export function addContextUpdateListener( - contextUpdatedFn: (context: Context) => void -): string; -export type addContextUpdateListener = ( - contextUpdatedFn: (context: Context) => void -) => string; +export function addContextUpdateListener(contextUpdatedFn: (context: Context) => void): string; +export type addContextUpdateListener = (contextUpdatedFn: (context: Context) => void) => string; /** * Removes a context update listener. diff --git a/client/src/linkManager.js b/client/src/linkManager.js index 2fea49da40..2d662e6267 100644 --- a/client/src/linkManager.js +++ b/client/src/linkManager.js @@ -26,7 +26,8 @@ export class linkManager extends LuigiClientBase { fromVirtualTreeRoot: false, fromParent: false, relative: false, - link: '' + link: '', + newTab: false }; } @@ -313,4 +314,15 @@ export class linkManager extends LuigiClientBase { this.options.withoutSync = true; return this; } + + /** + * Enables navigating to a new tab. + * @since NEXT_RELEASE + * @example + * LuigiClient.linkManager().newTab().navigate('/projects/xy/foobar'); + */ + newTab() { + this.options.newTab = true; + return this; + } } diff --git a/core/src/App.html b/core/src/App.html index 9f1ceb7562..71480f7085 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -918,8 +918,7 @@ resetMicrofrontendModalData(); const openViewInModal = async (nodepath, settings) => { - const { nodeObject } = await Navigation.extractDataFromPath(nodepath); - if (await Navigation.shouldPreventNavigation(nodeObject)) { + if (await NavigationHelpers.shouldPreventNavigationForPath(nodepath)) { return; } mfModal.displayed = true; @@ -981,8 +980,7 @@ resetMicrofrontendDrawerData(); const openViewInDrawer = async (nodepath, settings) => { - const { nodeObject } = await Navigation.extractDataFromPath(nodepath); - if (await Navigation.shouldPreventNavigation(nodeObject)) { + if (await NavigationHelpers.shouldPreventNavigationForPath(nodepath)) { return; } mfDrawer.displayed = true; @@ -1046,12 +1044,24 @@ internalUserSettingsObject.displayed = false; }; + // Open View in New Tab + + const openViewInNewTab = async nodepath => { + if (await NavigationHelpers.shouldPreventNavigationForPath(nodepath)) { + return; + } + + window.open(nodepath, '_blank'); + }; + function init(node) { const isolateAllViews = LuigiConfig.getConfigValue('navigation.defaults.isolateView'); const defaultPageErrorHandler = LuigiConfig.getConfigValue( 'navigation.defaults.pageErrorHandler' ); - const defaultRunTimeErrorHandler = LuigiConfig.getConfigValue('navigation.defaults.runTimeErrorHandler') + const defaultRunTimeErrorHandler = LuigiConfig.getConfigValue( + 'navigation.defaults.runTimeErrorHandler' + ); const config = { iframe: null, navigateOk: null, @@ -1232,7 +1242,11 @@ if ('luigi.navigation.open' === e.data.msg) { isNavigateBack = false; - if (e.data.params.modal !== undefined) { + if (e.data.params.newTab) { + let path = buildPath(e.data.params); + path = GenericHelpers.addLeadingSlash(path); + openViewInNewTab(path); + } else if (e.data.params.modal !== undefined) { let path = buildPath(e.data.params); path = GenericHelpers.addLeadingSlash(path); contentNode = node; @@ -1404,14 +1418,21 @@ e.data.data.id, e.data.data.operation, e.data.data.params - ) + ); } - if('luigi-runtime-error-handling'===e.data.msg){ + if ('luigi-runtime-error-handling' === e.data.msg) { let currentNode = iframe.luigi.currentNode; - if(currentNode && currentNode.runTimeErrorHandler && GenericHelpers.isFunction(currentNode.runTimeErrorHandler.errorFn)){ + if ( + currentNode && + currentNode.runTimeErrorHandler && + GenericHelpers.isFunction(currentNode.runTimeErrorHandler.errorFn) + ) { currentNode.runTimeErrorHandler.errorFn(e.data.errorObj, currentNode); - }else if(defaultRunTimeErrorHandler && GenericHelpers.isFunction(defaultRunTimeErrorHandler.errorFn)){ + } else if ( + defaultRunTimeErrorHandler && + GenericHelpers.isFunction(defaultRunTimeErrorHandler.errorFn) + ) { defaultRunTimeErrorHandler.errorFn(e.data.errorObj, currentNode); } } diff --git a/core/src/utilities/helpers/navigation-helpers.js b/core/src/utilities/helpers/navigation-helpers.js index 89c9d11066..dcedc8c6a0 100644 --- a/core/src/utilities/helpers/navigation-helpers.js +++ b/core/src/utilities/helpers/navigation-helpers.js @@ -307,6 +307,19 @@ class NavigationHelpersClass { return strippedNode; } + /** + * Checks if for the given node path navigation should be prevented or not + * @param {string} nodepath path to check + * @returns {boolean} navigation should be prevented or not + */ + async shouldPreventNavigationForPath(nodepath) { + const { nodeObject } = await Navigation.extractDataFromPath(nodepath); + if (await Navigation.shouldPreventNavigation(nodeObject)) { + return true; + } + return false; + } + /** * Returns a nested property value defined by a chain string * @param {*} obj the object diff --git a/core/test/utilities/helpers/navigation-helpers.spec.js b/core/test/utilities/helpers/navigation-helpers.spec.js index f77147032d..1602559910 100644 --- a/core/test/utilities/helpers/navigation-helpers.spec.js +++ b/core/test/utilities/helpers/navigation-helpers.spec.js @@ -3,9 +3,10 @@ const chai = require('chai'); const assert = chai.assert; const sinon = require('sinon'); -import { AuthHelpers, NavigationHelpers } from '../../../src/utilities/helpers'; +import { AuthHelpers, NavigationHelpers, GenericHelpers, RoutingHelpers } from '../../../src/utilities/helpers'; import { LuigiAuth, LuigiConfig } from '../../../src/core-api'; import { Routing } from '../../../src/services/routing'; +import { Navigation } from '../../../src/navigation/services/navigation'; describe('Navigation-helpers', () => { describe('isNodeAccessPermitted', () => { @@ -234,6 +235,39 @@ describe('Navigation-helpers', () => { }); }); + describe('shouldPreventNavigationForPath', () => { + afterEach(() => { + sinon.restore(); + sinon.reset(); + }); + + it('returns true when navigation should be prevented for path', async () => { + sinon.stub(Navigation, 'getNavigationPath').returns('testPreventNavigation'); + sinon.stub(RoutingHelpers, 'getLastNodeObject').returns({ + pathSegment: 'testPreventNavigation', + label: 'Prevent navigation conditionally', + onNodeActivation: () => { + return false; + } + }); + const actual = await NavigationHelpers.shouldPreventNavigationForPath('testPreventNavigation'); + assert.equal(actual, true); + }); + + it('returns false when navigation should not be prevented for path', async () => { + sinon.stub(Navigation, 'getNavigationPath').returns('testNotPreventNavigation'); + sinon.stub(RoutingHelpers, 'getLastNodeObject').returns({ + pathSegment: 'testNotPreventNavigation', + label: 'Do not prevent navigation conditionally', + onNodeActivation: () => { + return true; + } + }); + const actual = await NavigationHelpers.shouldPreventNavigationForPath('testNotPreventNavigation'); + assert.equal(actual, false); + }); + }); + describe('node title data', () => { let object; diff --git a/docs/luigi-client-api.md b/docs/luigi-client-api.md index 29e90dc76e..c0e2fc1e1e 100644 --- a/docs/luigi-client-api.md +++ b/docs/luigi-client-api.md @@ -388,6 +388,20 @@ LuigiClient.linkManager().withoutSync().fromClosestContext().navigate('settings' - **since**: 0.7.7 +#### newTab + +Enables navigating to a new tab. + +##### Examples + +```javascript +LuigiClient.linkManager().newTab().navigate('/projects/xy/foobar'); +``` + +**Meta** + +- **since**: NEXT_RELEASE + #### navigate Navigates to the given path in the application hosted by Luigi. It contains either a full absolute path or a relative path without a leading slash that uses the active route as a base. This is the standard navigation. diff --git a/test/e2e-test-application/src/app/project/project.component.html b/test/e2e-test-application/src/app/project/project.component.html index a8daa0bcf2..bcab4e5361 100644 --- a/test/e2e-test-application/src/app/project/project.component.html +++ b/test/e2e-test-application/src/app/project/project.component.html @@ -605,6 +605,25 @@