Skip to content

Commit

Permalink
Virtual tree navigation (#1075)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxmarkus authored Feb 19, 2020
1 parent 296ae68 commit 334e578
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,6 @@ class Navigation {
defaultLabel: 'Select Environment ...',
parentNodePath: '/environments', // absolute path
lazyloadOptions: true, // load options on click instead on page load
preserveSubPathOnSwitch: true,
// alwaysShowDropdown: false, // disable dropdown if there is only one option and no actions
options: () =>
[...Array(10).keys()]
.filter(n => n !== 0)
Expand Down
80 changes: 78 additions & 2 deletions core/src/navigation/services/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class NavigationClass {
this.rootNode = { children: topNavNodes };
}

await this.getChildren(this.rootNode); // keep it, mutates and filters children
await this.getChildren(this.rootNode, null, activePath); // keep it, mutates and filters children
}

const nodeNamesInCurrentPath = activePath.split('/');
Expand Down Expand Up @@ -205,7 +205,18 @@ class NavigationClass {
pathParams
);
try {
let children = await this.getChildren(node, newContext);
/**
* If its a virtual tree,
* build static children
*/
this.buildVirtualTree(node, nodeNamesInCurrentPath, pathParams);

// STANDARD PROCEDURE
let children = await this.getChildren(
node,
newContext,
nodeNamesInCurrentPath
);
const newNodeNamesInCurrentPath = nodeNamesInCurrentPath.slice(1);
result = this.buildNode(
newNodeNamesInCurrentPath,
Expand All @@ -222,6 +233,71 @@ class NavigationClass {
return result;
}

/**
* Requires str to include :virtualPath
* and pathParams consist of :virtualSegment_N
* for deep nested virtual tree building
*
* @param {string} str
* @param {Object} pathParams
* @param {number} _virtualPathIndex
*/
buildVirtualViewUrl(str, pathParams, _virtualPathIndex) {
let newStr = '';
for (const key in pathParams) {
if (key.startsWith('virtualSegment')) {
newStr += ':' + key + '/';
}
}
newStr += ':virtualSegment_' + _virtualPathIndex + '/';
return str + '/' + newStr;
}

buildVirtualTree(node, nodeNamesInCurrentPath, pathParams) {
const virtualTreeRoot = node.virtualTree;
// Temporary store values that will be cleaned up when creating a copy
const virtualTreeChild = node._virtualTree;
const _virtualViewUrl = node._virtualViewUrl || node.viewUrl;
if ((virtualTreeRoot || virtualTreeChild) && nodeNamesInCurrentPath[0]) {
let _virtualPathIndex = node._virtualPathIndex;
if (virtualTreeRoot) {
_virtualPathIndex = 0;
node.keepSelectedForChildren = true;
}

// Allowing maximum of 50 path segments to avoid memory issues
const maxPathDepth = 50;
if (_virtualPathIndex > maxPathDepth) {
return;
}

_virtualPathIndex++;
const keysToClean = [
'_*',
'virtualTree',
'parent',
'children',
'keepSelectedForChildren',
'navigationContext'
];
const newChild = GenericHelpers.removeProperties(node, keysToClean);
Object.assign(newChild, {
pathSegment: ':virtualSegment_' + _virtualPathIndex,
label: ':virtualSegment_' + _virtualPathIndex,
viewUrl: this.buildVirtualViewUrl(
_virtualViewUrl,
pathParams,
_virtualPathIndex
),
_virtualTree: true,
_virtualPathIndex,
_virtualViewUrl
});

node.children = [newChild];
}
}

findMatchingNode(urlPathElement, nodes) {
let result = null;
const segmentsLength = nodes.filter(n => !!n.pathSegment).length;
Expand Down
33 changes: 33 additions & 0 deletions core/src/utilities/helpers/generic-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,39 @@ class GenericHelpersClass {
input
);
}

/**
* Returns a new Object with the same object,
* without the keys that were given.
* References still stay.
* Allows wildcard ending keys
*
* @param {Object} input any given object
* @param {Array} of keys, allows also wildcards at the end, like: _*
*/
removeProperties(input, keys) {
const res = {};
if (!keys instanceof Array || !keys.length) {
console.error(
'[ERROR] removeProperties requires second parameter: array of keys to remove from object.'
);
return input;
}
for (const key in input) {
if (input.hasOwnProperty(key)) {
const noFullMatch = keys.filter(k => key.includes(k)).length === 0;
const noPartialMatch =
keys
.filter(k => k.endsWith('*'))
.map(k => k.slice(0, -1))
.filter(k => key.startsWith(k)).length === 0;
if (noFullMatch && noPartialMatch) {
res[key] = input[key];
}
}
}
return res;
}
}

export const GenericHelpers = new GenericHelpersClass();
131 changes: 128 additions & 3 deletions core/test/services/navigation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,16 @@ describe('Navigation', function() {
beforeEach(() => {
Navigation._rootNodeProviderUsed = undefined;
Navigation.rootNode = undefined;
console.warn = sinon.spy();
console.error = sinon.spy();
console.warn.resetHistory();
console.error.resetHistory();
});
afterEach(() => {
// reset
LuigiConfig.config = {};
sinon.restore();
sinon.reset();
});
describe('getNavigationPath', function() {
it('should not fail for undefined arguments', () => {
Expand Down Expand Up @@ -434,9 +439,6 @@ describe('Navigation', function() {
]
});

console.warn = sinon.spy();
console.error = sinon.spy();

// truthy tests
// when
const resStaticOk = Navigation.findMatchingNode('other', [staticNode()]);
Expand Down Expand Up @@ -914,4 +916,127 @@ describe('Navigation', function() {
assert.deepEqual(result, expected);
});
});
describe('buildVirtualViewUrl', () => {
it('returns valid substituted string without proper pathParams', () => {
const mock = {
url: 'https://mf.luigi-project.io#!',
pathParams: {
otherParam: 'foo'
},
index: 1
};
const expected = 'https://mf.luigi-project.io#!/:virtualSegment_1/';

assert.equal(
Navigation.buildVirtualViewUrl(mock.url, mock.pathParams, mock.index),
expected
);
});
it('returns valid substituted string with pathParams', () => {
const mock = {
url: 'https://mf.luigi-project.io#!',
pathParams: {
otherParam: 'foo',
virtualSegment_1: 'one',
virtualSegment_2: 'two'
},
index: 3
};
const expected =
'https://mf.luigi-project.io#!/:virtualSegment_1/:virtualSegment_2/:virtualSegment_3/';

assert.equal(
Navigation.buildVirtualViewUrl(mock.url, mock.pathParams, mock.index),
expected
);
});
});
describe('buildVirtualTree', () => {
it('unchanged node if not a virtual tree root', () => {
const given = {
label: 'Luigi'
};
const expected = Object.assign({}, given);

Navigation.buildVirtualTree(given);

assert.deepEqual(given, expected);
});
it('unchanged if directly accessing a node which is defined as virtual tree root', () => {
const mockNode = {
label: 'Luigi',
virtualTree: true,
viewUrl: 'foo'
};
const mockNodeNames = []; // no further child segments

const expected = Object.assign({}, mockNode);

Navigation.buildVirtualTree(mockNode, mockNodeNames);

assert.deepEqual(mockNode, expected);
});
it('with first virtual tree segment', () => {
const mockNode = {
label: 'Luigi',
virtualTree: true,
viewUrl: 'http://mf.luigi-project.io'
};
const mockNodeNames = ['foo'];

const expected = Object.assign({}, mockNode, {
keepSelectedForChildren: true,
children: [
{
_virtualTree: true,
_virtualPathIndex: 1,
label: ':virtualSegment_1',
pathSegment: ':virtualSegment_1',
viewUrl: 'http://mf.luigi-project.io/:virtualSegment_1/',
_virtualViewUrl: 'http://mf.luigi-project.io'
}
]
});

Navigation.buildVirtualTree(mockNode, mockNodeNames);

assert.deepEqual(expected, mockNode);
});
it('with a deep nested virtual tree segment', () => {
const mockNode = {
_virtualTree: true,
_virtualPathIndex: 3,
label: ':virtualSegment_3',
pathSegment: ':virtualSegment_3',
viewUrl:
'http://mf.luigi-project.io/:virtualSegment_2/:virtualSegment_3/',
_virtualViewUrl: 'http://mf.luigi-project.io'
};
const mockNodeNames = ['foo'];
const pathParams = {
otherParam: 'foo',
virtualSegment_1: 'one',
virtualSegment_2: 'two',
virtualSegment_3: 'three'
};

const expected = Object.assign({}, mockNode, {
children: [
{
_virtualTree: true,
_virtualPathIndex: 4,
label: ':virtualSegment_4',
pathSegment: ':virtualSegment_4',
viewUrl:
'http://mf.luigi-project.io/:virtualSegment_1/:virtualSegment_2/:virtualSegment_3/:virtualSegment_4/',
_virtualViewUrl: 'http://mf.luigi-project.io'
}
]
});

Navigation.buildVirtualTree(mockNode, mockNodeNames, pathParams);

assert.deepEqual(expected, mockNode);
});
});
});
15 changes: 15 additions & 0 deletions core/test/utilities/helpers/generic-helpers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,19 @@ describe('Generic-helpers', () => {
};
assert.deepEqual(GenericHelpers.removeInternalProperties(input), expected);
});
it('removeProperties', () => {
const input = {
some: true,
value: true,
_internal: true,
_somefn: () => true,
internalOne: true,
internalTwo: true
};
const keys = ['_*', 'value', 'internal*'];
const expected = {
some: true
};
assert.deepEqual(GenericHelpers.removeProperties(input, keys), expected);
});
});
15 changes: 15 additions & 0 deletions docs/navigation-parameters-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,21 @@ settings: {
- **type**: boolean or "exclusive"
- **description**: when set to `true`, the node is always accessible. When set to `exclusive`, the node is only visible in logged-out state. Requires **auth.disableAutoLogin** to be set to `true`. **anonymousAccess** needs to be defined both on parent and child nodes.
### virtualTree
- **type**: boolean
- **description**: marks the node as the beginning of a virtual tree. Allows navigation to any of its children's paths without the need of specifying nested children. The path that comes after the node marked as **virtualTree** is appended to its **viewUrl**. [**keepSelectedForChildren**](#keepSelectedForChildren) is automatically applied.
- **example**:
In this example, navigating to `core.tld/settings/some/nested/view` will result in opening `/sampleapp.html#/settings/some/nested/view`.
```javascript
{
pathSegment: 'settings',
label: 'Settings',
viewUrl: '/sampleapp.html#/settings',
navigationContext: 'settings',
virtualTree: true
}
```
## Context switcher
The context switcher is a drop-down list available in the top navigation bar. It allows you to switch between a curated list of navigation elements such as Environments. To do so, add the **contextSwitcher** parameter to the **navigation** object using the following optional parameters:
Expand Down

0 comments on commit 334e578

Please sign in to comment.