Skip to content

Commit

Permalink
Implement intent based navigation (SAP#1634)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndricimrr authored Oct 28, 2020
1 parent 4a7a8de commit 659e1e7
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 3 deletions.
1 change: 1 addition & 0 deletions client/luigi-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export declare interface LinkManager {
* LuigiClient.linkManager().navigate('/overview')
* LuigiClient.linkManager().navigate('users/groups/stakeholders')
* LuigiClient.linkManager().navigate('/settings', null, true) // preserve view
* LuigiClient.linkManager().navigate('#?intent=Sales-order?id=13') // intent navigation
*/
navigate: (
path: string,
Expand Down
3 changes: 3 additions & 0 deletions client/src/linkManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class linkManager extends LuigiClientBase {
* LuigiClient.linkManager().navigate('/overview')
* LuigiClient.linkManager().navigate('users/groups/stakeholders')
* LuigiClient.linkManager().navigate('/settings', null, true) // preserve view
* LuigiClient.linkManager().navigate('#?Intent=Sales-order?id=13') // intent navigation
*/
navigate(path, sessionId, preserveView, modalSettings, splitViewSettings) {
if (this.options.errorSkipNavigation) {
Expand All @@ -61,12 +62,14 @@ export class linkManager extends LuigiClientBase {

this.options.preserveView = preserveView;
const relativePath = path[0] !== '/';
const hasIntent = path.toLowerCase().includes('?intent=');
const navigationOpenMsg = {
msg: 'luigi.navigation.open',
sessionId: sessionId,
params: Object.assign(this.options, {
link: path,
relative: relativePath,
intent: hasIntent,
modal: modalSettings,
splitView: splitViewSettings
})
Expand Down
3 changes: 3 additions & 0 deletions core/src/App.html
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,9 @@
n => navigationContext === n.navigationContext
);
path = Routing.concatenatePath(getSubPath(node), params.link);
} else if (params.intent) {
const intentPath = RoutingHelpers.getIntentPath(params.link);
path = intentPath ? intentPath : path;
} else if (params.relative) {
// relative
path = Routing.concatenatePath(getSubPath(currentNode), params.link);
Expand Down
24 changes: 23 additions & 1 deletion core/src/services/routing.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,43 @@ class RoutingClass {
}

getHashPath(url = window.location.hash) {
// check for intent, if any
if (url && /\?intent=/i.test(url)) {
const hash = url.replace('#/#', '#'); // handle default hash and intent specific hash
const intentHash = RoutingHelpers.getIntentPath(hash.split('#')[1]);
if (intentHash) {
return intentHash;
}
}

return url.split('#/')[1];
}

getModifiedPathname() {
// check for intent, if any
if (window.location.hash && /\?intent=/i.test(window.location.hash)) {
const hash = window.location.hash.replace('#/#', '').replace('#', '');
const intentPath = RoutingHelpers.getIntentPath(hash);
return intentPath ? intentPath : '/';
}
const path =
(window.history.state && window.history.state.path) ||
window.location.pathname;

return path
.split('/')
.slice(1)
.join('/');
}

getCurrentPath() {
if (/\?intent=/i.test(window.location.hash)) {
const hash = window.location.hash.replace('#/#', '').replace('#', '');
const intentPath = RoutingHelpers.getIntentPath(hash);
if (intentPath) {
// if intent faulty or illegal then skip
return intentPath;
}
}
return LuigiConfig.getConfigValue('routing.useHashRouting')
? window.location.hash.replace('#', '') // TODO: GenericHelpers.getPathWithoutHash(window.location.hash) fails in ContextSwitcher
: window.location.search
Expand Down
132 changes: 130 additions & 2 deletions core/src/utilities/helpers/routing-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,11 @@ class RoutingHelpersClass {

getNodeHref(node, pathParams) {
if (LuigiConfig.getConfigBooleanValue('navigation.addNavHrefs')) {
const link = RoutingHelpers.getRouteLink(node, pathParams,
LuigiConfig.getConfigValue('routing.useHashRouting')?"#":'');
const link = RoutingHelpers.getRouteLink(
node,
pathParams,
LuigiConfig.getConfigValue('routing.useHashRouting') ? '#' : ''
);
return link.url || link;
}
return 'javascript:void(0)';
Expand Down Expand Up @@ -264,6 +267,131 @@ class RoutingHelpersClass {
featureToggleList.forEach(ft => LuigiFeatureToggles.setFeatureToggle(ft));
}
}

/**
* This function takes an intentLink and parses it conforming certain limitations in characters usage.
* Limitations include:
* - `semanticObject` allows only alphanumeric characters
* - `action` allows alphanumeric characters and the '_' sign
*
* Example of resulting output:
* ```
* {
* semanticObject: "Sales",
* action: "order",
* params: [{param1: "value1"},{param2: "value2"}]
* };
* ```
* @param {string} link the intent link represents the semantic intent defined by the user
* i.e.: #?intent=semanticObject-action?param=value
*/
getIntentObject(intentLink) {
const intentParams = intentLink.split('?intent=')[1];
if (intentParams) {
const elements = intentParams.split('-');
if (elements.length === 2) {
// avoids usage of '-' in semantic object and action
const semanticObject = elements[0];
const actionAndParams = elements[1].split('?');
// length 2 involves parameters, length 1 involves no parameters
if (actionAndParams.length === 2 || actionAndParams.length === 1) {
let action = actionAndParams[0];
let params = actionAndParams[1];
// parse parameters, if any
if (params) {
params = params.split('&');
let paramObjects = [];
params.forEach(item => {
const param = item.split('=');
param.length === 2 && paramObjects.push({ [param[0]]: param[1] });
});
params = paramObjects;
}
const alphanumeric = /^[0-9a-zA-Z]+$/;
const alphanumericOrUnderscores = /^[0-9a-zA-Z_]+$/;
// TODO: check for character size limit
if (
semanticObject.match(alphanumeric) &&
action.match(alphanumericOrUnderscores)
) {
return {
semanticObject,
action,
params
};
} else {
console.warn(
'Intent found contains illegal characters. Semantic object must be alphanumeric, action must be (alphanumeric+underscore)'
);
}
}
}
}
return false;
}

/**
* This function compares the intentLink parameter with the configuration intentMapping
* and returns the path segment that is matched together with the parameters, if any
*
* Example:
*
* For intentLink = `#?intent=Sales-order?foo=bar`
* and Luigi configuration:
* ```
* intentMapping: [{
* semanticObject: 'Sales',
* action: 'order',
* pathSegment: '/projects/pr2/order'
* }]
* ```
* the given intentLink is matched with the configuration's same semanticObject and action,
* resulting in pathSegment `/projects/pr2/order` being returned. The parameter is also added in
* this case resulting in: `/projects/pr2/order?~foo=bar`
* @param {string} intentLink the intentLink represents the semantic intent defined by the user
* i.e.: #?intent=semanticObject-action?param=value
*/
getIntentPath(intentLink) {
const mappings = LuigiConfig.getConfigValue('navigation.intentMapping');
if (mappings && mappings.length > 0) {
const caseInsensitiveLink = intentLink.replace(/\?intent=/i, '?intent=');
const intentObject = this.getIntentObject(caseInsensitiveLink);
if (intentObject) {
let realPath = mappings.find(
item =>
item.semanticObject === intentObject.semanticObject &&
item.action === intentObject.action
);
if (!realPath) {
return false;
}
realPath = realPath.pathSegment;
if (intentObject.params) {
// get custom node param prefixes if any or default to ~
let nodeParamPrefix = LuigiConfig.getConfigValue(
'routing.nodeParamPrefix'
);
nodeParamPrefix = nodeParamPrefix ? nodeParamPrefix : '~';
realPath = realPath.concat(`?${nodeParamPrefix}`);
intentObject.params.forEach(param => {
realPath = realPath.concat(Object.keys(param)[0]); // append param name
realPath = realPath.concat('=');
// append param value and prefix in case of multiple params
realPath = realPath
.concat(param[Object.keys(param)[0]])
.concat(`&${nodeParamPrefix}`);
});
realPath = realPath.slice(0, -(nodeParamPrefix.length + 1)); // slice extra prefix
}
return realPath;
} else {
console.warn('Could not parse given intent link.');
}
} else {
console.warn('No intent mappings are defined in Luigi configuration.');
}
return false;
}
}

export const RoutingHelpers = new RoutingHelpersClass();
Loading

0 comments on commit 659e1e7

Please sign in to comment.