Skip to content

Commit

Permalink
Adds navigateToIntent to wc client api (#3797)
Browse files Browse the repository at this point in the history
  • Loading branch information
walmazacn authored Jul 17, 2024
1 parent 26871fd commit 42c9d0d
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 8 deletions.
2 changes: 1 addition & 1 deletion client/src/linkManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class linkManager extends LuigiClientBase {
navigateToIntent(semanticSlug, params = {}) {
let newPath = '#?intent=';
newPath += semanticSlug;
if (params) {
if (params && Object.keys(params)?.length) {
const paramList = Object.entries(params);
// append parameters to the path if any
if (paramList.length > 0) {
Expand Down
20 changes: 16 additions & 4 deletions container/cypress/e2e/test-app/wc/wc-container.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,19 @@ describe('Web Container Test', () => {
expect(stub.getCall(0)).to.be.calledWith('LuigiClient.getAnchor()="testanchor"');
});
});

it('defer-init flag for webcomponent container', () => {
// the initialized webcomponent has id="defer-init-flag"
cy.get('#defer-init-flag').should('not.exist');
// click button that calls container.init()
cy.get('#init-button').click();

cy.get('#defer-init-flag').should('exist');
});

it('LuigiClient API getCurrentRoute for LuigiContainer', () => {
const stub = cy.stub();
cy.on('window:alert', stub);

cy.get(containerSelector)
.shadow()
.contains('getCurrentRoute')
Expand All @@ -103,6 +103,18 @@ describe('Web Container Test', () => {
});
});

it('LuigiClient API navigateToIntent for LuigiContainer', () => {
cy.on('window:alert', stub);

cy.get('[data-test-id="luigi-client-api-test-01"]')
.shadow()
.contains('navigateToIntent')
.click()
.then(() => {
expect(stub.getCall(0)).to.be.calledWith('navigated to: #?intent=Sales-settings');
});
});

it('updateContext', () => {
cy.on('window:alert', stub);

Expand Down
23 changes: 23 additions & 0 deletions container/src/services/webcomponents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ export class WebComponentService {
...options
});
},
navigateToIntent: (semanticSlug: string, params = {}): void => {
let newPath = '#?intent=';

newPath += semanticSlug;

if (params && Object.keys(params)?.length) {
const paramList = Object.entries(params);

// append parameters to the path if any
if (paramList.length > 0) {
newPath += '?';

for (const [key, value] of paramList) {
newPath += key + '=' + value + '&';
}

// trim potential excessive ampersand & at the end
newPath = newPath.slice(0, -1);
}
}

linkManagerInstance.navigate(newPath);
},
fromClosestContext: () => {
fromClosestContext = true;
return linkManagerInstance;
Expand Down
12 changes: 12 additions & 0 deletions container/test-app/wc/helloWorldWC.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export default class extends HTMLElement {
hasBack(), updateTopNavigation(), goBack(), pathExists()
</button>`;

const navigateToIntentBtn = document.createElement('template');
navigateToIntentBtn.innerHTML = '<button id="navigateToIntent">navigateToIntent</button>';

this._shadowRoot = this.attachShadow({
mode: 'open',
delegatesFocus: false
Expand All @@ -108,6 +111,7 @@ export default class extends HTMLElement {
this._shadowRoot.appendChild(linkManagerUpdateTopPathExistsBackBtn.content.cloneNode(true));
this._shadowRoot.appendChild(setViewGroupDataBtn.content.cloneNode(true));
this._shadowRoot.appendChild(getCurrentRouteBtn.content.cloneNode(true));
this._shadowRoot.appendChild(navigateToIntentBtn.content.cloneNode(true));

this._shadowRoot.appendChild(empty.content.cloneNode(true));

Expand Down Expand Up @@ -295,6 +299,14 @@ export default class extends HTMLElement {
alert('current route: ' + result);
});
});

this.$navigateToIntent = this._shadowRoot.querySelector('#navigateToIntent');
this.$navigateToIntent.addEventListener('click', () => {
if (this.LuigiClient) {
this.LuigiClient.linkManager().navigateToIntent('Sales-settings');
alert('navigated to: #?intent=Sales-settings');
}
});
}

get context() {
Expand Down
43 changes: 43 additions & 0 deletions container/test/services/webcomponents.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,49 @@ describe('createClientAPI', () => {
expect(dispatchEventSpy).toHaveBeenCalledWith(Events.NAVIGATION_REQUEST, expectedPayload);
});

it.each([
{ slug: null, params: null },
{ slug: 'Sales-settings', params: null },
{ slug: null, params: { project: 'pr2', user: 'john' } },
{ slug: 'Sales-settings', params: { project: 'pr2', user: 'john' } }
])('test linkManager navigateToIntent', (data) => {
let payloadLink = `#?intent=${data.slug}`;

if (data.params && Object.keys(data.params)?.length) {
const paramList = Object.entries(data.params);

if (paramList.length > 0) {
payloadLink += '?';

for (const [key, value] of paramList) {
payloadLink += key + '=' + value + '&';
}

payloadLink = payloadLink.slice(0, -1);
}
}

// mock and spy on functions
service.containerService.dispatch = jest.fn();
const dispatchEventSpy = jest.spyOn(service, 'dispatchLuigiEvent');

// act
const clientAPI = service.createClientAPI(undefined, 'nodeId', 'wc_id', 'component');
clientAPI.linkManager().navigateToIntent(data.slug, data.params);

// assert
const expectedPayload = {
fromClosestContext: false,
fromParent: false,
fromContext: null,
fromVirtualTreeRoot: false,
link: payloadLink,
nodeParams: {}
};

expect(dispatchEventSpy).toHaveBeenCalledWith(Events.NAVIGATION_REQUEST, expectedPayload);
});

it('test linkManager: openAsDrawer', () => {
const route = '/test/route';

Expand Down
38 changes: 38 additions & 0 deletions core/src/core-api/_internalLinkManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,44 @@ export class linkManager extends LuigiCoreAPIBase {
return remotePromise;
}

/**
* Offers an alternative way of navigating with intents. This involves specifying a semanticSlug and an object containing
* parameters.
* This method internally generates a URL of the form `#?intent=<semantic object>-<action>?<param_name>=<param_value>` through the given
* input arguments. This then follows a call to the original `linkManager.navigate(...)` function.
* Consequently, the following calls shall have the exact same effect:
* - linkManager().navigateToIntent('Sales-settings', {project: 'pr2', user: 'john'})
* - linkManager().navigate('/#?intent=Sales-settings?project=pr2&user=john')
* @param {string} semanticSlug concatenation of semantic object and action connected with a dash (-), i.e.: `<semanticObject>-<action>`
* @param {Object} params an object representing all the parameters passed, i.e.: `{param1: '1', param2: 2, param3: 'value3'}`.
* @example
* LuigiClient.linkManager().navigateToIntent('Sales-settings', {project: 'pr2', user: 'john'})
* LuigiClient.linkManager().navigateToIntent('Sales-settings')
*/
navigateToIntent(semanticSlug, params = {}) {
let newPath = '#?intent=';

newPath += semanticSlug;

if (params && Object.keys(params)?.length) {
const paramList = Object.entries(params);

// append parameters to the path if any
if (paramList.length > 0) {
newPath += '?';

for (const [key, value] of paramList) {
newPath += key + '=' + value + '&';
}

// trim potential excessive ampersand & at the end
newPath = newPath.slice(0, -1);
}
}

this.navigate(newPath);
}

/**
* This function navigates to a modal after adding the onClosePromise that handles the callback for when the modal is closed.
* @param {string} path the navigation path to open in the modal
Expand Down
14 changes: 14 additions & 0 deletions core/src/core-api/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ class LuigiNavigationManager {
return new linkManager().navigate(path, preserveView, modalSettings, splitViewSettings, drawerSettings);
}

/**
* Offers an alternative way of navigating with intents. This involves specifying a semanticSlug and an object containing parameters.
* @memberof LuigiNavigation
* @param {string} semanticSlug concatenation of semantic object and action connected with a dash (-)
* @param {Object} params an object representing all the parameters passed (optional, default '{}')
* @since NEXTRELEASE
* @example
* Luigi.navigation().navigateToIntent('Sales-settings')
* Luigi.navigation().navigateToIntent('Sales-settings', {project: 'pr1'})
*/
navigateToIntent(semanticSlug, params) {
return new linkManager().navigateToIntent(semanticSlug, params);
}

/**
* Opens a view in a modal. You can specify the modal's title and size. If you do not specify the title, it is the node label. If there is no node label, the title remains empty. The default size of the modal is `l`, which means 80%. You can also use `m` (60%) and `s` (40%) to set the modal size. Optionally, use it in combination with any of the navigation functions.
* @memberof LuigiNavigation
Expand Down
61 changes: 58 additions & 3 deletions core/test/core-api/internal-link-manager.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { linkManager } from '../../src/core-api/_internalLinkManager';
import { GenericHelpers } from '../../src/utilities/helpers';

const sinon = require('sinon');

import { linkManager } from '../../src/core-api/_internalLinkManager';

let lm;

describe('linkManager', function() {
Expand Down Expand Up @@ -112,6 +110,63 @@ describe('linkManager', function() {
});
});

describe('navigateToIntent', () => {
beforeEach(() => {
sinon.stub(lm, 'sendPostMessageToLuigiCore');
console.warn = sinon.spy();
});

it.each([
{ slug: null, params: null },
{ slug: 'Sales-settings', params: null },
{ slug: null, params: { project: 'pr2', user: 'john' } },
{ slug: 'Sales-settings', params: { project: 'pr2', user: 'john' } }
])('should call sendPostMessageToLuigiCore', (data) => {
const options = {
preserveView: false,
nodeParams: {},
errorSkipNavigation: false,
fromContext: null,
fromClosestContext: false,
relative: false,
link: ''
};
const modalSettings = { modalSetting: 'modalValue' };
const splitViewSettings = { splitViewSetting: 'splitViewValue' };
const drawerSettings = { drawerSetting: 'drawerValue' };
const relativePath = !!(data.slug && data.slug[0] !== '/');
let payloadLink = `#?intent=${data.slug}`;

if (data.params && Object.keys(data.params)?.length) {
const paramList = Object.entries(data.params);

if (paramList.length > 0) {
payloadLink += '?';

for (const [key, value] of paramList) {
payloadLink += key + '=' + value + '&';
}

payloadLink = payloadLink.slice(0, -1);
}
}

const navigationOpenMsg = {
msg: 'luigi.navigation.open',
params: Object.assign(options, {
link: payloadLink,
relative: relativePath,
modal: modalSettings,
splitView: splitViewSettings,
drawer: drawerSettings
})
};

lm.navigateToIntent(data.slug, data.params);
lm.sendPostMessageToLuigiCore.calledOnceWithExactly(navigationOpenMsg);
});
});

describe('openAsModal', () => {
beforeEach(() => {
sinon.stub(lm, 'navigate');
Expand Down
16 changes: 16 additions & 0 deletions docs/luigi-core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,22 @@ Luigi.navigation().navigate('users/groups/stakeholders')
Luigi.navigation().navigate('/settings', null, true) // preserve view
```

#### navigateToIntent

Offers an alternative way of navigating with intents. This involves specifying a semanticSlug and an object containing parameters.

##### Parameters

- `semanticSlug` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** concatenation of semantic object and action connected with a dash (-)
- `params` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** an object representing all the parameters passed (optional, default '{}')

##### Examples

```javascript
Luigi.navigation().navigateToIntent('Sales-settings')
Luigi.navigation().navigateToIntent('Sales-settings', {project: 'pr1'})
```

#### openAsModal

Opens a view in a modal. You can specify the modal's title and size. If you do not specify the title, it is the node label. If there is no node label, the title remains empty. The default size of the modal is `l`, which means 80%. You can also use `m` (60%) and `s` (40%) to set the modal size. Optionally, use it in combination with any of the navigation functions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ describe('JS-TEST-APP', () => {
});
});

it('navigateToIntent', () => {
cy.visitTestApp('/', newConfig);
cy.get('#app[configversion="normal-navigation"]');
cy.window().then(win => {
win.Luigi.navigation().navigate('/home').then(() => {
win.Luigi.navigation().navigateToIntent('Sales-setting');
cy.expectPathToBe('/home/two/#?intent=Sales-setting');
});
});
});

it('hideShellbar', () => {
cy.visitTestApp('/', newConfig);
cy.get('#app[configversion="normal-navigation"]');
Expand Down

0 comments on commit 42c9d0d

Please sign in to comment.