From 8c51d8b448f42975aae98370d022bb28e50256e3 Mon Sep 17 00:00:00 2001 From: JohannesDoberer Date: Tue, 2 Apr 2019 15:16:00 +0200 Subject: [PATCH] open microfrontend in modal (#446) --- client/luigi-client.d.ts | 26 +- client/src/luigi-client.js | 32 ++- .../luigi-sample-angular/package-lock.json | 8 - .../src/app/project/project.component.html | 32 ++- core/package-lock.json | 55 +++-- core/src/App.html | 224 +++++++++++++----- core/src/Modal.html | 139 +++++++++++ 7 files changed, 422 insertions(+), 94 deletions(-) create mode 100644 core/src/Modal.html diff --git a/client/luigi-client.d.ts b/client/luigi-client.d.ts index 75a7af3755..3b2c89cb97 100644 --- a/client/luigi-client.d.ts +++ b/client/luigi-client.d.ts @@ -16,6 +16,11 @@ export declare interface ConfirmationModalSettings { buttonDismiss?: string; } +export declare interface ModalSettings { + title?: string; + size?: 'l' | 'm' | 's'; +} + export declare interface Context { authData?: AuthData; context?: { parentNavigationContext?: string[] }; @@ -149,12 +154,20 @@ export declare interface LinkManager { * @param {string} path path to be navigated to * @param {string} sessionId current Luigi **sessionId** * @param {boolean} preserveView Preserve a view by setting it to `true`. It keeps the current view opened in the background and opens the new route in a new frame. Use the {@link #goBack goBack()} function to navigate back. You can use this feature across different levels. Preserved views are discarded as soon as the standard {@link #navigate navigate()} function is used instead of {@link #goBack goBack()}. + * @param {Object} modalSettings opens a microfrontend as a modal with possibility to specify a title and size + * @param {string} modalSettings.title modal title + * @param {string} modalSettings.size size of the modal (l=large 80% default, m=medium 60%, s=small 40%) * @example * LuigiClient.linkManager().navigate('/overview') * LuigiClient.linkManager().navigate('users/groups/stakeholders') * LuigiClient.linkManager().navigate('/settings', null, true) // preserve view */ - navigate: (path: string, sessionId?: string, preserveView?: boolean) => void; + navigate: ( + path: string, + sessionId?: string, + preserveView?: boolean, + modalSettings?: ModalSettings + ) => void; /** * Checks if the path you can navigate to exists in the main application. For example, you can use this helper method conditionally to display a DOM element like a button. @@ -182,6 +195,17 @@ export declare interface LinkManager { * LuigiClient.linkManager.fromContext("currentTeam").withParams({foo: "bar"}).navigate("path") */ withParams: (nodeParams: NodeParams) => this; + + /** + * Opens a microfrontend as a modal + * @param {string} path path to be navigated to + * @param {Object} modalSettings settings to customize the modal title and size + * @param {string} modalSettings.title modal title + * @param {string} modalSettings.size size of the modal (l=large 80% default, m=medium 60%, s=small 40%) + * @example + * LuigiClient.linkManager().openAsModal('projects/pr1/users', {title:'Users', size:'m'}); + */ + openAsModal: (nodepath: string, modalSettings?: ModalSettings) => void; } /** diff --git a/client/src/luigi-client.js b/client/src/luigi-client.js index 1efdc80130..986be0bd3b 100644 --- a/client/src/luigi-client.js +++ b/client/src/luigi-client.js @@ -250,12 +250,20 @@ const LuigiClient = { * @param {string} path path to be navigated to * @param {string} sessionId current Luigi **sessionId** * @param {boolean} preserveView Preserve a view by setting it to `true`. It keeps the current view opened in the background and opens the new route in a new frame. Use the {@link #goBack goBack()} function to navigate back. You can use this feature across different levels. Preserved views are discarded as soon as the standard {@link #navigate navigate()} function is used instead of {@link #goBack goBack()}. + * @param {Object} modalSettings opens a microfrontend as a modal with possibility to specify a title and size + * @param {string} modalSettings.title modal title + * @param {string} modalSettings.size size of the modal (l=large 80% default, m=medium 60%, s=small 40%) * @example * LuigiClient.linkManager().navigate('/overview') * LuigiClient.linkManager().navigate('users/groups/stakeholders') * LuigiClient.linkManager().navigate('/settings', null, true) // preserve view */ - navigate: function navigate(path, sessionId, preserveView) { + navigate: function navigate( + path, + sessionId, + preserveView, + modalSettings + ) { if (options.errorSkipNavigation) { options.errorSkipNavigation = false; return; @@ -267,12 +275,24 @@ const LuigiClient = { sessionId: sessionId, params: Object.assign(options, { link: path, - relative: relativePath + relative: relativePath, + modal: modalSettings }) }; window.parent.postMessage(navigationOpenMsg, '*'); }, - + /** + * Opens a microfrontend as a modal + * @param {string} path path to be navigated to + * @param {Object} modalSettings settings to customize the modal title and size + * @param {string} modalSettings.title modal title + * @param {string} modalSettings.size size of the modal (l=large 80% default, m=medium 60%, s=small 40%) + * @example + * LuigiClient.linkManager().openAsModal('projects/pr1/users', {title:'Users', size:'m'}); + */ + openAsModal: function(path, modalSettings) { + this.navigate(path, 0, true, modalSettings || {}); + }, /** * Sets the current navigation context to that of a specific parent node which has the {@link navigation-configuration.md navigationContext} field declared in the navigation configuration. This navigation context is then used by the `navigate` function. * @param {string} navigationContext @@ -374,7 +394,10 @@ const LuigiClient = { * @returns {boolean} indicating if there is a preserved view you can return to. */ hasBack: function hasBack() { - return Boolean(currentContext.internal.viewStackSize !== 0); + return ( + !!currentContext.internal.modal || + currentContext.internal.viewStackSize !== 0 + ); }, /** @@ -487,7 +510,6 @@ const LuigiClient = { }); return promises.confirmationModal.promise; }, - /** * Shows an alert. * @param {Object} settings the settings for the alert diff --git a/core/examples/luigi-sample-angular/package-lock.json b/core/examples/luigi-sample-angular/package-lock.json index 7ba5d51b3a..da8218cf45 100644 --- a/core/examples/luigi-sample-angular/package-lock.json +++ b/core/examples/luigi-sample-angular/package-lock.json @@ -1836,14 +1836,6 @@ } } }, - "@kyma-project/luigi-client": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@kyma-project/luigi-client/-/luigi-client-0.4.9.tgz", - "integrity": "sha512-nC6DN6Lm52Vvw8hPhMepRsqzqOujjgcMGDOqMi29ISr36ruA3tMfMSEmz+YnaeSy7bD4EBYJ4nZbPNiQDp0MLA==" - }, - "@kyma-project/luigi-core": { - "version": "0.4.10" - }, "@ngtools/webpack": { "version": "6.1.5", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-6.1.5.tgz", diff --git a/core/examples/luigi-sample-angular/src/app/project/project.component.html b/core/examples/luigi-sample-angular/src/app/project/project.component.html index 46c5180910..325324a077 100644 --- a/core/examples/luigi-sample-angular/src/app/project/project.component.html +++ b/core/examples/luigi-sample-angular/src/app/project/project.component.html @@ -42,6 +42,7 @@

LuigiClient uxManager methods

Luigi confirmation modal has been {{ confirmationModalResult }}

+

Alert

@@ -155,7 +156,6 @@

LuigiClient uxManager methods

LuigiClient linkManager methods

-
- diff --git a/core/package-lock.json b/core/package-lock.json index 5fb229a96f..2507708826 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1837,7 +1837,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -3177,7 +3177,7 @@ }, "eslint": { "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", "dev": true, "requires": { @@ -3500,7 +3500,7 @@ }, "external-editor": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "dev": true, "requires": { @@ -3855,7 +3855,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3876,12 +3877,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3896,17 +3899,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4023,7 +4029,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4035,6 +4042,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4049,6 +4057,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4056,12 +4065,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4080,6 +4091,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4160,7 +4172,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4172,6 +4185,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4257,7 +4271,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4293,6 +4308,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4312,6 +4328,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4355,12 +4372,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -4478,7 +4497,7 @@ "dependencies": { "axios": { "version": "0.15.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.15.3.tgz", + "resolved": "http://registry.npmjs.org/axios/-/axios-0.15.3.tgz", "integrity": "sha1-LJ1jiy4ZGgjqHWzJiOrda6W9wFM=", "dev": true, "requires": { @@ -6110,7 +6129,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -9814,7 +9833,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -13047,7 +13066,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/core/src/App.html b/core/src/App.html index c142dd3767..91c527ff26 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -12,6 +12,13 @@ {#if alert.isDisplayed} {/if} + {#if mfModal.isDisplayed} + + {/if}
@@ -47,6 +54,7 @@ import LeftNav from './navigation/LeftNav.html'; import ConfirmationModal from './ConfirmationModal.html'; import Alert from './Alert.html'; + import Modal from './Modal.html'; import { LuigiConfig } from './services/config.js'; import * as Routing from './services/routing.js'; import * as Navigation from './navigation/services/navigation.js'; @@ -165,15 +173,19 @@ { msg: 'luigi.init', context: JSON.stringify( - Object.assign({}, component.get().context, goBackContext) + Object.assign( + {}, + config.context || component.get().context, + goBackContext + ) ), nodeParams: JSON.stringify( - Object.assign({}, component.get().nodeParams) + Object.assign({}, config.nodeParams || component.get().nodeParams) ), pathParams: JSON.stringify( - Object.assign({}, component.get().pathParams) + Object.assign({}, config.pathParams || component.get().pathParams) ), - internal: JSON.stringify(component.prepareInternalData()), + internal: JSON.stringify(component.prepareInternalData(config.modal)), authData: AuthHelpers.getStoredAuthData() }, '*' @@ -218,6 +230,15 @@ }; }; + const getDefaultMicrofrontendModalData = () => { + return { + mfModal: { + isDisplayed: false, + settings: {} + } + }; + }; + export default { data() { return Object.assign( @@ -240,7 +261,8 @@ hideSideNav: false }, getDefaultAlertData(), - getDefaultConfirmationModalData() + getDefaultConfirmationModalData(), + getDefaultMicrofrontendModalData() ); }, oncreate() {}, @@ -273,16 +295,32 @@ } }); window.addEventListener('message', async e => { - if ('luigi.get-context' === e.data.msg && config.iframe) { - sendContextToClient(this, config, {}); - - const loadingIndicatorAutoHideEnabled = - GenericHelpers.getConfigValueFromObject( - this.get(), - 'currentNode.loadingIndicator.hideAutomatically' - ) !== false; - if (loadingIndicatorAutoHideEnabled) { - this.set({ showLoadingIndicator: false }); + if ('luigi.get-context' === e.data.msg) { + if ( + this.get().modalIframe && + e.source === this.get().modalIframe.contentWindow + ) { + let ctx = this.get().modalIframeData.context; + const modalConfig = { + ...config, + iframe: this.get().modalIframe, + context: ctx, + pathParams: this.get().modalIframeData.pathParams, + nodeParams: this.get().modalIframeData.nodeParams, + modal: true + }; + sendContextToClient(this, modalConfig, {}); + } else if (config.iframe) { + sendContextToClient(this, config, {}); + + const loadingIndicatorAutoHideEnabled = + GenericHelpers.getConfigValueFromObject( + this.get(), + 'currentNode.loadingIndicator.hideAutomatically' + ) !== false; + if (loadingIndicatorAutoHideEnabled) { + this.set({ showLoadingIndicator: false }); + } } } @@ -300,35 +338,60 @@ if ('luigi.navigation.open' === e.data.msg) { this.set({ isNavigateBack: false }); - this.getUnsavedChangesModalPromise().then(() => { - this.handleNavigation(e.data, config); - }); + if (e.data.params.modal !== undefined) { + let path = buildPath(this, e.data.params); + path = GenericHelpers.addLeadingSlash(path); + this.set( + Object.assign( + { contentNode: node }, + getDefaultMicrofrontendModalData() + ) + ); + this.openViewInModal(path, e.data.params.modal); + } else { + this.getUnsavedChangesModalPromise().then(() => { + this.handleNavigation(e.data, config); + this.closeModal(); + }); + } } if ('luigi.navigation.back' === e.data.msg) { - // go back: context from the view - const preservedViews = this.get().preservedViews; - if (preservedViews && preservedViews.length) { - this.getUnsavedChangesModalPromise().then(() => { - // remove current active iframe and data - Iframe.setActiveIframeToPrevious(node); - const previousActiveIframeData = preservedViews.pop(); - // set new active iframe and preservedViews - config.iframe = Iframe.getActiveIframe(node); - this.set({ - isNavigateBack: true, - preservedViews, - goBackContext: - e.data.goBackContext && JSON.parse(e.data.goBackContext) - }); - // TODO: check if getNavigationPath or history pop to update hash / path - const path = this.handleNavigation( - { params: { link: previousActiveIframeData.path } }, - config - ); + if ( + this.get().modalIframe && + e.source === this.get().modalIframe.contentWindow + ) { + this.closeModal(); + sendContextToClient(this, config, { + goBackContext: e.data.goBackContext && JSON.parse(e.data.goBackContext) }); } else { - console.error('goBack() not possible, no preserved views found.'); + // go back: context from the view + const preservedViews = this.get().preservedViews; + if (preservedViews && preservedViews.length) { + this.getUnsavedChangesModalPromise().then(() => { + // remove current active iframe and data + Iframe.setActiveIframeToPrevious(node); + const previousActiveIframeData = preservedViews.pop(); + // set new active iframe and preservedViews + config.iframe = Iframe.getActiveIframe(node); + this.set({ + isNavigateBack: true, + preservedViews, + goBackContext: + e.data.goBackContext && JSON.parse(e.data.goBackContext) + }); + // TODO: check if getNavigationPath or history pop to update hash / path + const path = this.handleNavigation( + { params: { link: previousActiveIframeData.path } }, + config + ); + }); + } else { + console.error( + 'goBack() not possible, no preserved views found.' + ); + } } } @@ -357,12 +420,21 @@ } if ('luigi.set-page-dirty' === e.data.msg) { - this.set({ - unsavedChanges: { - isDirty: e.data.dirty || false, - persistUrl: window.location.href - } - }); + if (!this.get().unsavedChanges.dirtySet) { + const dirtySet = new Set(); + dirtySet.add(e.source); + this.set({ + unsavedChanges: { + dirtySet: dirtySet + } + }); + } + this.get().unsavedChanges.persistUrl = window.location.href; + if (e.data.dirty) { + this.get().unsavedChanges.dirtySet.add(e.source); + } else { + this.get().unsavedChanges.dirtySet.delete(e.source); + } } if ('luigi.ux.confirmationModal.show' === e.data.msg) { @@ -416,10 +488,11 @@ addPreserveView(this, data, config); Routing.navigateTo(path); //navigate to the raw path. Any errors/alerts are handled later }, - prepareInternalData() { + prepareInternalData(modal = false) { return { isNavigateBack: this.get().isNavigateBack, - viewStackSize: this.get().preservedViews.length + viewStackSize: this.get().preservedViews.length, + modal: modal }; }, handleNavClick(node) { @@ -430,9 +503,6 @@ handleModalResult(result) { const { promise, openFromClient } = this.get().confirmationModal; if (result) { - this.set({ - unsavedChanges: { isDirty: false, persistUrl: null } - }); promise.resolve(); } else { promise.reject(); @@ -452,11 +522,21 @@ ); } }, - getUnsavedChangesModalPromise() { + getUnsavedChangesModalPromise(source) { return new Promise(resolve => { - if (this.shouldShowUnsavedChangesModal()) { + if (this.shouldShowUnsavedChangesModal(source)) { this.showUnsavedChangesModal().then( () => { + if ( + this.get().unsavedChanges && + this.get().unsavedChanges.dirtySet + ) { + if (source) { + this.get().unsavedChanges.dirtySet.delete(source); + } else { + this.get().unsavedChanges.dirtySet.clear(); + } + } resolve(); }, () => {} @@ -466,11 +546,18 @@ } }); }, - shouldShowUnsavedChangesModal() { - return ( + shouldShowUnsavedChangesModal(source) { + if ( GenericHelpers.canComponentHandleModal(this) && - this.get().unsavedChanges.isDirty - ); + this.get().unsavedChanges.dirtySet + ) { + if (source) { + return this.get().unsavedChanges.dirtySet.has(source); + } else if (this.get().unsavedChanges.dirtySet.size > 0) { + return true; + } + } + return false; }, showUnsavedChangesModal() { return this.showModal({ @@ -504,6 +591,28 @@ }); }); }, + openViewInModal(nodepath, modalSettings) { + this.set({ + mfModal: { + isDisplayed: true, + nodepath, + modalSettings + } + }); + }, + closeModal() { + if (this.get().modalIframe) { + this.getUnsavedChangesModalPromise( + this.get().modalIframe.contentWindow + ).then(() => { + this.set({ + mfModal: { + isDisplayed: false + } + }); + }); + } + }, handleAlertDismiss() { const openFromClient = this.get().alert.openFromClient; this.set(getDefaultAlertData()); @@ -526,7 +635,8 @@ TopNav, LeftNav, ConfirmationModal, - Alert + Alert, + Modal }, transitions: { fade } }; diff --git a/core/src/Modal.html b/core/src/Modal.html new file mode 100644 index 0000000000..9cfaf3672a --- /dev/null +++ b/core/src/Modal.html @@ -0,0 +1,139 @@ +
+
+
+
+ {#if modalSettings.title} +

{modalSettings.title}

+ {/if} + +
+
+
+
+
+ +