diff --git a/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js b/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js index c0decdc993..6c8cc4fc07 100644 --- a/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js +++ b/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js @@ -120,6 +120,11 @@ describe('Luigi client features', () => { { path: '/projects/pr2/', successExpected: true }, // existent absolute path without '/' at the end { path: '/projects/pr2', successExpected: true }, + // existent path with two dynamic pathSegments + { + path: '/projects/pr1/users/groups/avengers/settings/dynamic-two', + successExpected: true + }, // existent relative path { path: 'developers', successExpected: true } ].map(data => { diff --git a/core/examples/luigi-sample-angular/src/app/app-routing.module.ts b/core/examples/luigi-sample-angular/src/app/app-routing.module.ts index 2d1498cb3c..75e73c5e63 100644 --- a/core/examples/luigi-sample-angular/src/app/app-routing.module.ts +++ b/core/examples/luigi-sample-angular/src/app/app-routing.module.ts @@ -36,6 +36,10 @@ const routes: Routes = [ path: 'projects/:projectId/users/groups/:groupId/settings', component: GroupSettingsComponent }, + { + path: 'projects/:projectId/users/groups/:groupId/settings/:dynamicValue', + component: DynamicComponent + }, { path: 'projects/:projectId/developers', component: DevelopersComponent }, { path: 'projects/:projectId/settings', component: SettingsComponent }, { diff --git a/core/examples/luigi-sample-angular/src/assets/extendedConfiguration.js b/core/examples/luigi-sample-angular/src/assets/extendedConfiguration.js index 72f9bc943f..24d0fd21cb 100644 --- a/core/examples/luigi-sample-angular/src/assets/extendedConfiguration.js +++ b/core/examples/luigi-sample-angular/src/assets/extendedConfiguration.js @@ -109,11 +109,25 @@ var projectDetailNavProviderFn = function(context) { { label: 'Group Settings', pathSegment: 'settings', + keepSelectedForChildren: true, icon: 'user-settings', viewUrl: '/sampleapp.html#/projects/' + projectId + - '/users/groups/:group/settings' + '/users/groups/:group/settings', + children: [ + { + label: 'Multi Path Params', + pathSegment: ':dynamic', + viewUrl: + '/sampleapp.html#/projects/' + + projectId + + '/users/groups/:group/settings/:dynamic', + context: { + label: ':dynamic' + } + } + ] } ] } diff --git a/core/src/App.html b/core/src/App.html index b20003319f..1a35f772e8 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -35,6 +35,7 @@ import ConfirmationModal from './ConfirmationModal.html'; import { LuigiConfig } from './services/config.js'; import * as Routing from './services/routing.js'; + import * as Navigation from './navigation/services/navigation.js' import * as Iframe from './services/iframe'; import * as RoutingHelpers from './utilities/helpers/routing-helpers'; import * as GenericHelpers from './utilities/helpers/generic-helpers'; @@ -289,23 +290,22 @@ if ('luigi.navigation.pathExists' === e.data.msg) { const data = e.data.data; const path = buildPath(this, data); - const matchedPath = await Routing.matchPath(path); + const pathData = await Navigation.getNavigationPath( + LuigiConfig.getConfigValueAsync('navigation.nodes'), path + ); - let normalizedPath = - (!path.startsWith('/') ? '/' : '') + - (path.endsWith('/') ? path.slice(0, -1) : path); - const pathExists = matchedPath === normalizedPath; config.iframe.contentWindow.postMessage( { msg: 'luigi.navigation.pathExists.answer', data: { correlationId: data.id, - pathExists + pathExists: pathData.isExistingRoute } }, '*' ); } + if ('luigi.set-page-dirty' === e.data.msg) { this.set({ unsavedChanges: { @@ -331,9 +331,7 @@ }; }, handleNavClick(node) { - this.getUnsavedChangesModalPromise().then(() => { - Routing.handleRouteClick(node); - }); + this.getUnsavedChangesModalPromise().then(() => { Routing.handleRouteClick(node, this.get())}); }, handleModalResult(result) { const promise = this.get().confirmationModal.promise; diff --git a/core/src/navigation/services/navigation.js b/core/src/navigation/services/navigation.js index 2c86539296..94379675d2 100644 --- a/core/src/navigation/services/navigation.js +++ b/core/src/navigation/services/navigation.js @@ -3,13 +3,18 @@ import * as NavigationHelpers from '../../utilities/helpers/navigation-helpers'; import * as AsyncHelpers from '../../utilities/helpers/async-helpers'; import * as GenericHelpers from '../../utilities/helpers/generic-helpers'; +import * as RoutingHelpers from '../../utilities/helpers/routing-helpers'; +import { LuigiConfig } from '../../services/config.js'; -export const getNavigationPath = async (rootNavProviderPromise, activePath) => { - if (!rootNavProviderPromise) { - console.error('No navigation nodes provided in the configuration.'); - return [{}]; - } +export const getNavigationPath = async (rootNavProviderPromise, path = '') => { try { + const activePath = GenericHelpers.getTrimmedUrl(path); + + if (!rootNavProviderPromise) { + console.error('No navigation nodes provided in the configuration.'); + return [{}]; + } + let rootNode; const topNavNodes = await rootNavProviderPromise; if (GenericHelpers.isObject(topNavNodes)) { @@ -24,13 +29,32 @@ export const getNavigationPath = async (rootNavProviderPromise, activePath) => { rootNode = { children: topNavNodes }; } await getChildren(rootNode); // keep it, mutates and filters children - const nodeNamesInCurrentPath = (activePath || '').split('/'); - return buildNode( + const nodeNamesInCurrentPath = activePath.split('/'); + const navObj = await buildNode( nodeNamesInCurrentPath, [rootNode], rootNode.children, rootNode.context || {} ); + + const navPathSegments = navObj.navigationPath + .filter(x => x.pathSegment) + .map(x => x.pathSegment); + + navObj.isExistingRoute = + !activePath || nodeNamesInCurrentPath.length === navPathSegments.length; + + const pathSegments = activePath.split('/'); + navObj.matchedPath = pathSegments + .filter((segment, index) => { + return ( + (navPathSegments[index] && navPathSegments[index].startsWith(':')) || + navPathSegments[index] === segment + ); + }) + .join('/'); + + return navObj; } catch (err) { console.error('Failed to load top navigation nodes.', err); } @@ -83,21 +107,23 @@ const buildNode = async ( nodeNamesInCurrentPath, nodesInCurrentPath, childrenOfCurrentNode, - context + context, + pathParams = {} ) => { if (!context.parentNavigationContexts) { context.parentNavigationContexts = []; } let result = { navigationPath: nodesInCurrentPath, - context: context + context: context, + pathParams: pathParams }; if ( nodeNamesInCurrentPath.length > 0 && childrenOfCurrentNode && childrenOfCurrentNode.length > 0 ) { - const urlPathElement = nodeNamesInCurrentPath.splice(0, 1)[0]; + const urlPathElement = nodeNamesInCurrentPath[0]; const node = findMatchingNode(urlPathElement, childrenOfCurrentNode); if (node) { nodesInCurrentPath.push(node); @@ -109,11 +135,19 @@ const buildNode = async ( try { let children = await getChildren(node, newContext); + if (node.pathSegment.startsWith(':')) { + pathParams[ + node.pathSegment.replace(':', '') + ] = RoutingHelpers.sanitizeParam(urlPathElement); + } + const newNodeNamesInCurrentPath = nodeNamesInCurrentPath.slice(1); + result = buildNode( - nodeNamesInCurrentPath, + newNodeNamesInCurrentPath, nodesInCurrentPath, children, - newContext + newContext, + pathParams ); } catch (err) { console.error('Error getting nodes children', err); @@ -147,52 +181,13 @@ export const findMatchingNode = (urlPathElement, nodes) => { } } nodes.some(node => { - // Static nodes - if (node.pathSegment === urlPathElement) { - result = node; - return true; - } - - // Dynamic nodes if ( - (node.pathSegment && node.pathSegment.startsWith(':')) || - (node.pathParam && node.pathParam.key) + // Static nodes + node.pathSegment === urlPathElement || + // Dynamic nodes + (node.pathSegment && node.pathSegment.startsWith(':')) ) { - if (node.pathParam && node.pathParam.key) { - node.viewUrl = node.pathParam.viewUrl; - node.context = node.pathParam.context - ? Object.assign({}, node.pathParam.context) - : undefined; - node.pathSegment = node.pathParam.pathSegment; - } else { - node.pathParam = { - key: node.pathSegment.slice(0), - pathSegment: node.pathSegment, - viewUrl: node.viewUrl, - context: node.context ? Object.assign({}, node.context) : undefined - }; - } - node.pathParam.value = urlPathElement; - - // path substitutions - node.pathSegment = node.pathSegment.replace( - node.pathParam.key, - urlPathElement - ); - - if (node.viewUrl) { - node.viewUrl = node.viewUrl.replace(node.pathParam.key, urlPathElement); - } - - if (node.context) { - Object.entries(node.context).map(entry => { - const dynKey = entry[1]; - if (dynKey === node.pathParam.key) { - node.context[entry[0]] = dynKey.replace(dynKey, urlPathElement); - } - }); - } - + // Return last matching node result = node; return true; } diff --git a/core/src/services/iframe.js b/core/src/services/iframe.js index c5c59043df..5b8260b823 100644 --- a/core/src/services/iframe.js +++ b/core/src/services/iframe.js @@ -2,10 +2,9 @@ // Please consider adding any new methods to 'iframe-helpers' if they don't require anything from this file. import * as IframeHelpers from '../utilities/helpers/iframe-helpers'; import * as GenericHelpers from '../utilities/helpers/generic-helpers'; +import * as RoutingHelpers from '../utilities/helpers/routing-helpers'; const iframeNavFallbackTimeout = 2000; -const contextVarPrefix = 'context.'; -const nodeParamsVarPrefix = 'nodeParams.'; let timeoutHandle; export const getActiveIframe = node => { @@ -39,22 +38,7 @@ export const navigateIframe = (config, component, node) => { const componentData = component.get(); let viewUrl = componentData.viewUrl; if (viewUrl) { - viewUrl = IframeHelpers.replaceVars( - viewUrl, - componentData.pathParams, - ':', - false - ); - viewUrl = IframeHelpers.replaceVars( - viewUrl, - componentData.context, - contextVarPrefix - ); - viewUrl = IframeHelpers.replaceVars( - viewUrl, - componentData.nodeParams, - nodeParamsVarPrefix - ); + viewUrl = RoutingHelpers.substituteViewUrl(viewUrl, componentData); } const isSameDomain = IframeHelpers.isSameDomain(config, component); diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 2107db8265..af617d1ae9 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -33,29 +33,6 @@ export const concatenatePath = (basePath, relativePath) => { return path; }; -export const matchPath = async path => { - try { - const pathUrl = - 0 < path.length ? GenericHelpers.getPathWithoutHash(path) : path; - const pathData = await Navigation.getNavigationPath( - LuigiConfig.getConfigValueAsync('navigation.nodes'), - GenericHelpers.trimTrailingSlash(pathUrl.split('?')[0]) - ); - if (pathData.navigationPath.length > 0) { - const lastNode = - pathData.navigationPath[pathData.navigationPath.length - 1]; - return RoutingHelpers.buildRoute( - lastNode, - '/' + (lastNode.pathSegment ? lastNode.pathSegment : ''), - pathUrl.split('?')[1] - ); - } - } catch (err) { - console.error('Could not match path', err); - } - return null; -}; - /** navigateTo used for navigation @param route string absolute path of the new route @@ -134,7 +111,12 @@ export const getCurrentPath = () => window.location.search : GenericHelpers.trimLeadingSlash(window.location.pathname); -export const handleRouteChange = async (path, component, node, config) => { +export const handleRouteChange = async ( + path, + component, + iframeElement, + config +) => { const defaultPattern = [/access_token=/, /id_token=/]; const patterns = LuigiConfig.getConfigValue('routing.skipRoutingForUrlPatterns') || @@ -156,7 +138,7 @@ export const handleRouteChange = async (path, component, node, config) => { component.showUnsavedChangesModal().then( () => { path && - handleRouteChange(path, component, node, config) && + handleRouteChange(path, component, iframeElement, config) && history.replaceState(window.state, '', newUrl); }, () => {} @@ -166,36 +148,23 @@ export const handleRouteChange = async (path, component, node, config) => { const pathUrlRaw = path && path.length ? GenericHelpers.getPathWithoutHash(path) : ''; - const pathUrl = GenericHelpers.trimTrailingSlash(pathUrlRaw.split('?')[0]); const pathData = await Navigation.getNavigationPath( LuigiConfig.getConfigValueAsync('navigation.nodes'), - pathUrl - ); - - const hideNav = LuigiConfig.getConfigBooleanValue( - 'settings.hideNavigation' - ); - - const { - viewUrl = '', - isolateView = undefined, - hideSideNav = false - } = RoutingHelpers.getLastNodeObject(pathData); - const params = RoutingHelpers.parseParams(pathUrlRaw.split('?')[1]); - const nodeParams = RoutingHelpers.getNodeParams(params); - const pathParams = RoutingHelpers.getPathParams(pathData.navigationPath); - const viewGroup = RoutingHelpers.findViewGroup( - RoutingHelpers.getLastNodeObject(pathData) + path ); + const lastNode = RoutingHelpers.getLastNodeObject(pathData); + const viewUrl = lastNode.viewUrl || ''; if (!viewUrl) { - const routeExists = RoutingHelpers.isExistingRoute(pathUrl, pathData); const defaultChildNode = await RoutingHelpers.getDefaultChildNode( pathData ); - if (routeExists) { + if (pathData.isExistingRoute) { //normal navigation can be performed - navigateTo(`${pathUrl ? `/${pathUrl}` : ''}/${defaultChildNode}`); + const trimmedPathUrl = GenericHelpers.getTrimmedUrl(path); + navigateTo( + `${trimmedPathUrl ? `/${trimmedPathUrl}` : ''}/${defaultChildNode}` + ); } else { if (defaultChildNode && pathData.navigationPath.length > 1) { //last path segment was invalid but a default node could be in its place @@ -222,42 +191,57 @@ export const handleRouteChange = async (path, component, node, config) => { return; } - if (!GenericHelpers.containsAllSegments(pathUrl, pathData.navigationPath)) { - const matchedPath = await matchPath(pathUrlRaw); - showPageNotFoundError(component, matchedPath, pathUrlRaw, true); + if (!pathData.isExistingRoute) { + showPageNotFoundError(component, pathData.matchedPath, pathUrlRaw, true); } - const previousCompData = component.get(); - component.set({ + const hideNav = LuigiConfig.getConfigBooleanValue( + 'settings.hideNavigation' + ); + const params = RoutingHelpers.parseParams(pathUrlRaw.split('?')[1]); + const nodeParams = RoutingHelpers.getNodeParams(params); + const viewGroup = RoutingHelpers.findViewGroup(lastNode); + const currentNode = + pathData.navigationPath && pathData.navigationPath.length > 0 + ? pathData.navigationPath[pathData.navigationPath.length - 1] + : null; + + const newNodeData = { hideNav, - hideSideNav, viewUrl, - navigationPath: pathData.navigationPath, - currentNode: - pathData.navigationPath && pathData.navigationPath.length > 0 - ? pathData.navigationPath[pathData.navigationPath.length - 1] - : null, - context: pathData.context, nodeParams, - pathParams, - isolateView, viewGroup, - previousNodeValues: previousCompData - ? { - viewUrl: previousCompData.viewUrl, - isolateView: previousCompData.isolateView, - viewGroup: previousCompData.viewGroup - } - : {} - }); + currentNode, + navigationPath: pathData.navigationPath, + context: RoutingHelpers.substituteDynamicParamsInObject( + Object.assign({}, pathData.context, currentNode.context), + pathData.pathParams + ), + pathParams: pathData.pathParams, + hideSideNav: lastNode.hideSideNav || false, + isolateView: lastNode.isolateView || false + }; - Iframe.navigateIframe(config, component, node); + const previousCompData = component.get(); + component.set( + Object.assign({}, newNodeData, { + previousNodeValues: previousCompData + ? { + viewUrl: previousCompData.viewUrl, + isolateView: previousCompData.isolateView, + viewGroup: previousCompData.viewGroup + } + : {} + }) + ); + + Iframe.navigateIframe(config, component, iframeElement); } catch (err) { console.info('Could not handle route change', err); } }; -export const handleRouteClick = node => { +export const handleRouteClick = (node, componentData) => { if (node.externalLink && node.externalLink.url) { node.externalLink.sameWindow ? (window.location.href = node.externalLink.url) @@ -270,7 +254,9 @@ export const handleRouteClick = node => { navigateTo(link); } else { const route = RoutingHelpers.buildRoute(node, `/${node.pathSegment}`); - navigateTo(route); + navigateTo( + GenericHelpers.replaceVars(route, componentData.pathParams, ':', false) + ); } }; diff --git a/core/src/utilities/helpers/generic-helpers.js b/core/src/utilities/helpers/generic-helpers.js index e3849167e4..ebbc65ab60 100644 --- a/core/src/utilities/helpers/generic-helpers.js +++ b/core/src/utilities/helpers/generic-helpers.js @@ -88,26 +88,6 @@ export const prependOrigin = path => { return window.location.origin; }; -export const containsAllSegments = (sourceUrl, targetPathSegments) => { - if ( - sourceUrl === undefined || - sourceUrl === null || - !targetPathSegments || - !targetPathSegments.length - ) { - console.error( - 'Ooops, seems like the developers have misconfigured something' - ); - return false; - } - const mandatorySegmentsUrl = trimTrailingSlash(sourceUrl.split('?')[0]); - const pathSegmentsUrl = targetPathSegments - .filter(x => x.pathSegment) // filter out root node with empty path segment - .map(x => x.pathSegment) - .join('/'); - return trimTrailingSlash(pathSegmentsUrl) === mandatorySegmentsUrl; -}; - /** * Adds a leading slash to a string if it has none * @param {str} string @@ -141,6 +121,11 @@ export const trimLeadingSlash = str => str.replace(/^\/+/g, ''); */ export const trimTrailingSlash = str => str.replace(/\/+$/, ''); +export const getTrimmedUrl = path => { + const pathUrl = 0 < path.length ? getPathWithoutHash(path) : path; + return trimTrailingSlash(pathUrl.split('?')[0]); +}; + /** * Returns a path that starts and end with one (and only one) slash, * regardless of the slashes being already present in the path given as input @@ -177,3 +162,35 @@ export const canComponentHandleModal = component => export const escapeRegExp = string => { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; + +export const replaceVars = ( + inputString, + params, + prefix, + parenthesis = true +) => { + let processedString = inputString; + if (params) { + Object.entries(params).forEach(entry => { + processedString = processedString.replace( + new RegExp( + escapeRegExp( + (parenthesis ? '{' : '') + + prefix + + entry[0] + + (parenthesis ? '}' : '') + ), + 'g' + ), + encodeURIComponent(entry[1]) + ); + }); + } + if (parenthesis) { + processedString = processedString.replace( + new RegExp('\\{' + escapeRegExp(prefix) + '[^\\}]+\\}', 'g'), + '' + ); + } + return processedString; +}; diff --git a/core/src/utilities/helpers/routing-helpers.js b/core/src/utilities/helpers/routing-helpers.js index 9eff7579f4..87e2bdc24e 100644 --- a/core/src/utilities/helpers/routing-helpers.js +++ b/core/src/utilities/helpers/routing-helpers.js @@ -1,6 +1,7 @@ -// Helper methods for 'routing.js' file. They don't require any method from 'routing.js` but are required by them. +// Helper methods for 'routing.js' file. They don't require any method from 'routing.js' but are required by them. // They are also rarely used directly from outside of 'routing.js' import * as AsyncHelpers from './async-helpers'; +import * as GenericHelpers from './generic-helpers'; import { LuigiConfig } from '../../services/config'; import * as Routing from '../../services/routing'; @@ -38,19 +39,6 @@ export const getDefaultChildNode = async pathData => { return ''; }; -export const isExistingRoute = (path, pathData) => { - if (!path) { - return true; - } - - const lastElement = - pathData.navigationPath[pathData.navigationPath.length - 1]; - const routeSplit = path.replace(/\/$/, '').split('/'); - const lastPathSegment = routeSplit[routeSplit.length - 1]; - - return lastElement.pathSegment === lastPathSegment; -}; - export const parseParams = paramsString => { const result = {}; const viewParamString = paramsString; @@ -82,17 +70,6 @@ export const getNodeParams = params => { return sanitizeParams(result); }; -export const getPathParams = nodes => { - const params = {}; - nodes - .filter(n => n.pathParam) - .map(n => n.pathParam) - .forEach(pp => { - params[pp.key.replace(':', '')] = pp.value; - }); - return sanitizeParams(params); -}; - export const findViewGroup = node => { if (node.viewGroup) { return node.viewGroup; @@ -126,18 +103,58 @@ export const buildRoute = (node, path, params) => ? path + (params ? '?' + params : '') : buildRoute(node.parent, `/${node.parent.pathSegment}${path}`, params); -export const sanitizeParams = paramsMap => { - function encodeParam(param) { - return String(param) - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\//g, '/'); - } +export const substituteDynamicParamsInObject = ( + object, + paramMap, + paramPrefix = ':' +) => { + return Object.entries(object) + .map(([key, value]) => { + let foundKey = Object.keys(paramMap).find( + key2 => value === paramPrefix + key2 + ); + return [key, foundKey ? paramMap[foundKey] : value]; + }) + .reduce((acc, [key, value]) => { + return Object.assign(acc, { [key]: value }); + }, {}); +}; + +export const substituteViewUrl = (viewUrl, componentData) => { + const contextVarPrefix = 'context.'; + const nodeParamsVarPrefix = 'nodeParams.'; + viewUrl = GenericHelpers.replaceVars( + viewUrl, + componentData.pathParams, + ':', + false + ); + viewUrl = GenericHelpers.replaceVars( + viewUrl, + componentData.context, + contextVarPrefix + ); + viewUrl = GenericHelpers.replaceVars( + viewUrl, + componentData.nodeParams, + nodeParamsVarPrefix + ); + return viewUrl; +}; + +export const sanitizeParam = param => { + return String(param) + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); +}; + +export const sanitizeParams = paramsMap => { return Object.entries(paramsMap).reduce((sanitizedMap, paramPair) => { - sanitizedMap[encodeParam(paramPair[0])] = encodeParam(paramPair[1]); + sanitizedMap[sanitizeParam(paramPair[0])] = sanitizeParam(paramPair[1]); return sanitizedMap; }, {}); }; diff --git a/core/test/services/navigation.spec.js b/core/test/services/navigation.spec.js index a72a65326e..24717336f6 100644 --- a/core/test/services/navigation.spec.js +++ b/core/test/services/navigation.spec.js @@ -5,9 +5,9 @@ const assert = chai.assert; const sinon = require('sinon'); import { LuigiConfig } from '../../src/services/config'; -const sampleNavPromise = new Promise(function(resolve) { +const sampleNavPromise = new Promise(function (resolve) { const lazyLoadedChildrenNodesProviderFn = () => { - return new Promise(function(resolve) { + return new Promise(function (resolve) { resolve([ { pathSegment: 'b1', @@ -51,11 +51,11 @@ const sampleNavPromise = new Promise(function(resolve) { ]); }); -describe('Navigation', function() { +describe('Navigation', function () { before(() => { function mockStorage() { return { - getItem: function(key) { + getItem: function (key) { return JSON.stringify({ accessTokenExpirationDate: Number(new Date()) + 1 }); @@ -69,7 +69,7 @@ describe('Navigation', function() { // reset LuigiConfig.config = {}; }); - describe('getNavigationPath', function() { + describe('getNavigationPath', function () { it('should not fail for undefined arguments', () => { navigation.getNavigationPath(undefined, undefined); }); @@ -242,7 +242,7 @@ describe('Navigation', function() { }); }); describe('findMatchingNode', () => { - it('substitutes dynamic path', () => { + it('with dynamic path, does not substitute values', () => { // given const staticNode = () => ({ label: 'Other', @@ -274,10 +274,10 @@ describe('Navigation', function() { ]); // // then - expect(resStaticOk.pathSegment).to.equal('other'); - expect(resDynamicOk.pathSegment).to.equal('avengers'); - expect(resDynamicOk.viewUrl).to.contain('/avengers'); - expect(resDynamicOk.context.currentGroup).to.equal('avengers'); + expect(resStaticOk.pathSegment).to.equal('other', 'resStaticOk.pathSegment'); + expect(resDynamicOk.pathSegment).to.equal(':group', 'resDynamicOk.pathSegment'); + expect(resDynamicOk.viewUrl).to.contain('/:group', 'resDynamicOk.viewUrl'); + expect(resDynamicOk.context.currentGroup).to.equal(':group', 'resDynamicOk.context'); // falsy tests const resNull = navigation.findMatchingNode('avengers', [staticNode()]); @@ -289,7 +289,7 @@ describe('Navigation', function() { dynamicNode() ]); expect(resStaticWarning.pathSegment).to.equal( - 'avengers', + ':group', 'static warning pathSegment: ' + resStaticWarning.pathSegment ); sinon.assert.calledOnce(console.warn); diff --git a/core/test/services/routing.spec.js b/core/test/services/routing.spec.js index 450a6a767f..cae113e0a1 100644 --- a/core/test/services/routing.spec.js +++ b/core/test/services/routing.spec.js @@ -225,7 +225,7 @@ describe('Routing', () => { } }; sinon.stub(document, 'createElement').callsFake(() => ({ src: null })); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then assert.equal(component.get().viewUrl, expectedViewUrl); @@ -368,7 +368,7 @@ describe('Routing', () => { LuigiConfig.config = sampleLuigiConfig; const iframeMock = { src: null }; sinon.stub(document, 'createElement').callsFake(() => iframeMock); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then assert.equal(component.get().viewUrl, expectedViewUrl); @@ -430,7 +430,7 @@ describe('Routing', () => { LuigiConfig.config = sampleLuigiConfig; const iframeMock = { src: null }; sinon.stub(document, 'createElement').callsFake(() => iframeMock); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then assert.equal(component.get().viewUrl, expectedViewUrl); @@ -468,7 +468,7 @@ describe('Routing', () => { window.Luigi.config = sampleLuigiConfig; const iframeMock = { src: null }; sinon.stub(document, 'createElement').callsFake(() => iframeMock); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then assert.equal(iframeMock.src, expectedViewUrl); @@ -505,7 +505,7 @@ describe('Routing', () => { window.Luigi.config = sampleLuigiConfig; const iframeMock = { src: null }; sinon.stub(document, 'createElement').callsFake(() => iframeMock); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then assert.equal(iframeMock.src, expectedViewUrl); @@ -534,7 +534,7 @@ describe('Routing', () => { // when LuigiConfig.config = sampleLuigiConfig; LuigiConfig.config.navigation.hideNav = false; - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); // then sinon.assert.calledWith( @@ -560,7 +560,7 @@ describe('Routing', () => { assert.equal(component.get().hideSideNav, undefined); - await routing.handleRouteChange(path, component, node, config, window); + await routing.handleRouteChange(path, component, node, config); assert.equal(component.get().hideSideNav, true); }); @@ -580,6 +580,11 @@ describe('Routing', () => { const nodeWithoutParent = { pathSegment: 'projects' }; + const mockComponentData = { + pathParams: {}, + nodeParams: {}, + context: {} + }; it('should set proper location hash with parent node', () => { // given @@ -587,7 +592,7 @@ describe('Routing', () => { LuigiConfig.getConfigValue.returns(true); // when - routing.handleRouteClick(nodeWithParent); + routing.handleRouteClick(nodeWithParent, mockComponentData); // then assert.equal(window.location.hash, expectedRoute); @@ -599,7 +604,7 @@ describe('Routing', () => { LuigiConfig.getConfigValue.returns(true); // when - routing.handleRouteClick(nodeWithoutParent); + routing.handleRouteClick(nodeWithoutParent, mockComponentData); // then assert.equal(window.location.hash, expectedRoute); @@ -616,7 +621,7 @@ describe('Routing', () => { LuigiConfig.getConfigValue.returns(false); // when - routing.handleRouteClick(nodeWithParent); + routing.handleRouteClick(nodeWithParent, mockComponentData); // then const pushStateArgs = window.history.pushState.args[0]; @@ -637,7 +642,7 @@ describe('Routing', () => { LuigiConfig.getConfigValue.returns(false); // when - routing.handleRouteClick(nodeWithoutParent); + routing.handleRouteClick(nodeWithoutParent, mockComponentData); // then const pushStateArgs = window.history.pushState.args[0]; @@ -658,7 +663,7 @@ describe('Routing', () => { LuigiConfig.getConfigValue.returns(false); // when - routing.handleRouteClick(nodeWithoutParent); + routing.handleRouteClick(nodeWithoutParent, mockComponentData); // then const pushStateArgs = window.history.pushState.args[0]; @@ -680,7 +685,7 @@ describe('Routing', () => { // when LuigiConfig.getConfigValue.returns(true); - routing.handleRouteClick(inputNode, window); + routing.handleRouteClick(inputNode, mockComponentData); // then assert.equal(window.location.hash, expectedRoute); @@ -698,7 +703,7 @@ describe('Routing', () => { // when LuigiConfig.getConfigValue.returns(true); - routing.handleRouteClick(inputNode, window); + routing.handleRouteClick(inputNode, mockComponentData); // then assert.equal(window.location.hash, expectedRoute); diff --git a/core/test/utilities/helpers/generic-helpers.spec.js b/core/test/utilities/helpers/generic-helpers.spec.js index 73ecf75742..4ed25ccf05 100644 --- a/core/test/utilities/helpers/generic-helpers.spec.js +++ b/core/test/utilities/helpers/generic-helpers.spec.js @@ -2,123 +2,4 @@ const chai = require('chai'); const assert = chai.assert; const GenericHelpers = require('../../../src/utilities/helpers/generic-helpers'); -describe('Generic-helpers', () => { - describe('containsAllSegments', () => { - it('should return true when proper data provided', async () => { - const sourceUrl = 'mas/ko/pa/tol/'; - const targetPathSegments = [ - { - //doesn't matter, it's omitted anyway - }, - { - pathSegment: 'mas' - }, - { - pathSegment: 'ko' - }, - { - pathSegment: 'pa' - }, - { - pathSegment: 'tol' - } - ]; - assert.equal( - GenericHelpers.containsAllSegments(sourceUrl, targetPathSegments), - true - ); - }); - - it('should return false when wrong data provided', async () => { - const differentSourceUrl = 'mas/ko/pa/tol'; - const similarSourceUrl = 'luigi/is/os/awesome'; - const targetPathSegments = [ - { - //doesn't matter, it's omitted anyway - }, - { - pathSegment: 'luigi' - }, - { - pathSegment: 'is' - }, - { - pathSegment: 'so' - }, - { - pathSegment: 'awesome' - } - ]; - assert.equal( - GenericHelpers.containsAllSegments( - differentSourceUrl, - targetPathSegments - ), - false - ); - assert.equal( - GenericHelpers.containsAllSegments( - similarSourceUrl, - targetPathSegments - ), - false - ); - }); - - it("should return false when pathSegments numbers don't match", async () => { - const tooShortSourceUrl = 'one/two'; - const tooLongSourceUrl = 'three/four/five/six'; - const targetPathSegments = [ - { - //doesn't matter, it's omitted anyway - }, - { - pathSegment: 'one' - }, - { - pathSegment: 'two' - }, - { - pathSegment: 'three' - } - ]; - assert.equal( - GenericHelpers.containsAllSegments( - tooShortSourceUrl, - targetPathSegments - ), - false - ); - assert.equal( - GenericHelpers.containsAllSegments( - tooLongSourceUrl, - targetPathSegments - ), - false - ); - }); - - it('should ignore GET parameters', async () => { - const sourceUrl = 'one/two/three?masko=patol&four=five'; - - const targetPathSegments = [ - { - //doesn't matter, it's omitted anyway - }, - { - pathSegment: 'one' - }, - { - pathSegment: 'two' - }, - { - pathSegment: 'three' - } - ]; - assert.equal( - GenericHelpers.containsAllSegments(sourceUrl, targetPathSegments), - true - ); - }); - }); -}); +describe('Generic-helpers', () => {}); diff --git a/core/test/utilities/helpers/routing-helpers.spec.js b/core/test/utilities/helpers/routing-helpers.spec.js index c5929b4cf5..14754989e3 100644 --- a/core/test/utilities/helpers/routing-helpers.spec.js +++ b/core/test/utilities/helpers/routing-helpers.spec.js @@ -1,8 +1,50 @@ const chai = require('chai'); +const expect = chai.expect; const assert = chai.assert; import * as RoutingHelpers from '../../../src/utilities/helpers/routing-helpers'; describe('Routing-helpers', () => { + describe('substituteDynamicParamsInObject', () => { + it('substitutes an object', () => { + const input = { + key1: 'something', + key2: ':group' + }; + const paramMap = { + group: 'mygroup' + }; + + const expectedOutput = { + key1: 'something', + key2: 'mygroup' + }; + + expect( + RoutingHelpers.substituteDynamicParamsInObject(input, paramMap) + ).to.deep.equal(expectedOutput); + expect(input.key2).to.equal(':group'); + }); + it('substitutes an object using custom prefix', () => { + const input = { + key1: 'something', + key2: '#group' + }; + const paramMap = { + group: 'mygroup' + }; + + const expectedOutput = { + key1: 'something', + key2: 'mygroup' + }; + + expect( + RoutingHelpers.substituteDynamicParamsInObject(input, paramMap, '#') + ).to.deep.equal(expectedOutput); + expect(input.key2).to.equal('#group'); + }); + }); + describe('defaultChildNodes', () => { const mockPathData = { navigationPath: [ diff --git a/docs/navigation-configuration.md b/docs/navigation-configuration.md index 520fdf7cac..d650fdebf2 100644 --- a/docs/navigation-configuration.md +++ b/docs/navigation-configuration.md @@ -176,7 +176,7 @@ The navigation structure with the project list view using such sample node param pathSegment: 'home', label: 'Home', viewUrl: 'https://my.microfrontend.com/', - children: [The + children: [ { pathSegment: 'projects', label: 'Projects',