Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtual tree navigation #1075

Merged
merged 26 commits into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ace0991
Merge branch 'master' of github.com:SAP/luigi
maxmarkus Nov 20, 2019
305ad98
Merge remote-tracking branch 'upstream/master'
maxmarkus Nov 21, 2019
c4298f6
Merge remote-tracking branch 'upstream/master'
maxmarkus Nov 27, 2019
a3c55ee
Merge remote-tracking branch 'upstream/master'
maxmarkus Jan 10, 2020
3fc67ff
Merge remote-tracking branch 'upstream/master'
maxmarkus Jan 13, 2020
502f14c
Merge branch 'master' into poc-virtual-nodetrees
maxmarkus Jan 23, 2020
8f533b3
working half way, wrong viewUrl and non-updating on navigation
maxmarkus Jan 24, 2020
f1c4023
working simplified solution
maxmarkus Jan 27, 2020
a724592
added unit tets
maxmarkus Jan 27, 2020
18f21a0
refactored to simple virtualTree setting, docu updates
maxmarkus Jan 30, 2020
5e09d4e
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Jan 31, 2020
55af04d
Merge branch 'poc-virtual-nodetrees' of github.com:maxmarkus/luigi in…
maxmarkus Jan 31, 2020
cca66ee
cleanup and fix of unit tets
maxmarkus Jan 31, 2020
b031657
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 3, 2020
1cf18e2
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 4, 2020
da914fe
Update docs/navigation-parameters-reference.md
maxmarkus Feb 10, 2020
95d3e0a
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 10, 2020
2df92bc
Merge branch 'poc-virtual-nodetrees' of github.com:maxmarkus/luigi in…
maxmarkus Feb 11, 2020
a52edb4
Merge branch 'master' into poc-virtual-nodetrees
maxmarkus Feb 12, 2020
ef768cd
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 13, 2020
2d071d8
Merge branch 'poc-virtual-nodetrees' of github.com:maxmarkus/luigi in…
maxmarkus Feb 13, 2020
973977f
small docu improvement
maxmarkus Feb 13, 2020
7404239
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 14, 2020
d4c932e
Merge branch 'master' into poc-virtual-nodetrees
zarkosimic Feb 14, 2020
a79ea26
Merge branch 'master' into poc-virtual-nodetrees
alexandra-simeonova Feb 17, 2020
bd16f9f
Merge branch 'master' into poc-virtual-nodetrees
maxmarkus Feb 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
maxmarkus marked this conversation as resolved.
Show resolved Hide resolved
// Temporary store values that will be cleaned up when creating a copy
const virtualTreeChild = node._virtualTree;
maxmarkus marked this conversation as resolved.
Show resolved Hide resolved
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