diff --git a/packages/admin-ui/src/app/shared/components/extension-host/extension-host-config.ts b/packages/admin-ui/src/app/shared/components/extension-host/extension-host-config.ts index b010b7285e..3faab58540 100644 --- a/packages/admin-ui/src/app/shared/components/extension-host/extension-host-config.ts +++ b/packages/admin-ui/src/app/shared/components/extension-host/extension-host-config.ts @@ -1,10 +1,13 @@ export interface ExtensionHostOptions { extensionUrl: string; + openInNewTab?: boolean; } export class ExtensionHostConfig { public extensionUrl: string; + public openInNewTab: boolean; constructor(options: ExtensionHostOptions) { this.extensionUrl = options.extensionUrl; + this.openInNewTab = options.openInNewTab != null ? options.openInNewTab : false; } } diff --git a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.html b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.html index 52972e6bc5..d1f7bdbe9b 100644 --- a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.html +++ b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.html @@ -1 +1,20 @@ - + + + + +
+
+ +

+ {{ 'common.extension-running-in-separate-window' | translate }} +

+
+
+
diff --git a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.scss b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.scss index 4b27f93399..d0f39f4bd6 100644 --- a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.scss +++ b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.scss @@ -10,3 +10,34 @@ iframe { height: 100%; border: none; } + +.launch-button { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + border: none; + padding: 24px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s; + text-align: center; + &.launched { + background-color: $color-grey-300; + } +} + +.window-hint { + visibility: hidden; + opacity: 0; + transition: visibility 0.3s 0, opacity 0.3s; + &.visible { + visibility: visible; + opacity: 1; + transition: visibility 0, opacity 0.3s; + } +} diff --git a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.ts b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.ts index 8acd90adce..e422c4eefd 100644 --- a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.ts +++ b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; @@ -12,9 +20,13 @@ import { ExtensionHostService } from './extension-host.service'; changeDetection: ChangeDetectionStrategy.Default, providers: [ExtensionHostService], }) -export class ExtensionHostComponent implements OnInit { +export class ExtensionHostComponent implements OnInit, AfterViewInit, OnDestroy { extensionUrl: SafeResourceUrl; - @ViewChild('extensionFrame', { static: true }) private extensionFrame: ElementRef; + openInIframe = true; + extensionWindowIsOpen = false; + private config: ExtensionHostConfig; + private extensionWindow?: Window; + @ViewChild('extensionFrame', { static: false }) private extensionFrame: ElementRef; constructor( private route: ActivatedRoute, @@ -29,15 +41,53 @@ export class ExtensionHostComponent implements OnInit { `Expected an ExtensionHostConfig object, got ${JSON.stringify(data.extensionHostConfig)}`, ); } + this.config = data.extensionHostConfig; + this.openInIframe = !this.config.openInNewTab; this.extensionUrl = this.sanitizer.bypassSecurityTrustResourceUrl( - data.extensionHostConfig.extensionUrl || 'about:blank', + this.config.extensionUrl || 'about:blank', ); - const { contentWindow } = this.extensionFrame.nativeElement; - if (contentWindow) { - this.extensionHostService.init(contentWindow); + } + + ngAfterViewInit() { + if (this.openInIframe) { + const extensionWindow = this.extensionFrame.nativeElement.contentWindow; + if (extensionWindow) { + this.extensionHostService.init(extensionWindow); + } + } + } + + ngOnDestroy(): void { + if (this.extensionWindow) { + this.extensionWindow.close(); } } + launchExtensionWindow() { + const extensionWindow = window.open(this.config.extensionUrl); + if (!extensionWindow) { + return; + } + this.extensionHostService.init(extensionWindow); + this.extensionWindowIsOpen = true; + this.extensionWindow = extensionWindow; + + let timer: number; + function pollWindowState(extwindow: Window, onClosed: () => void) { + if (extwindow.closed) { + window.clearTimeout(timer); + onClosed(); + } else { + timer = window.setTimeout(() => pollWindowState(extwindow, onClosed), 250); + } + } + + pollWindowState(extensionWindow, () => { + this.extensionWindowIsOpen = false; + this.extensionHostService.destroy(); + }); + } + private isExtensionHostConfig(input: any): input is ExtensionHostConfig { return input.hasOwnProperty('extensionUrl'); } diff --git a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.service.ts b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.service.ts index 78f5701e9e..69d43191c5 100644 --- a/packages/admin-ui/src/app/shared/components/extension-host/extension-host.service.ts +++ b/packages/admin-ui/src/app/shared/components/extension-host/extension-host.service.ts @@ -21,11 +21,15 @@ export class ExtensionHostService implements OnDestroy { window.addEventListener('message', this.handleMessage); } - ngOnDestroy(): void { + destroy() { window.removeEventListener('message', this.handleMessage); this.destroyMessage$.next(); } + ngOnDestroy(): void { + this.destroy(); + } + private handleMessage = (message: MessageEvent) => { const { data, origin } = message; if (this.isExtensionMessage(data)) { diff --git a/packages/admin-ui/src/i18n-messages/en.json b/packages/admin-ui/src/i18n-messages/en.json index c85be3f15c..e266b2dca3 100644 --- a/packages/admin-ui/src/i18n-messages/en.json +++ b/packages/admin-ui/src/i18n-messages/en.json @@ -140,10 +140,12 @@ "edit": "Edit", "edit-field": "Edit field", "enabled": "Enabled", + "extension-running-in-separate-window": "Extension is running in a separate window", "guest": "Guest", "items-per-page-option": "{ count } per page", "jobs-in-progress": "{ count } {count, plural, one {job} other {jobs}} in progress", "language": "Language", + "launch-extension": "Launch extension", "log-out": "Log out", "login": "Log in", "more": "More...", @@ -555,6 +557,7 @@ "catalog": "Catalog", "channel": "Channel", "channel-token": "Channel token", + "confirm-delete-role": "Delete role?", "create": "Create", "create-new-channel": "Create new channel", "create-new-country": "Create new country", @@ -604,4 +607,4 @@ "update": "Update", "zone": "Zone" } -} \ No newline at end of file +} diff --git a/packages/dev-server/ui-plugin/extensions/ui-plugin.module.ts b/packages/dev-server/ui-plugin/extensions/ui-plugin.module.ts index 85cb3d1515..a1e1f3dc4a 100644 --- a/packages/dev-server/ui-plugin/extensions/ui-plugin.module.ts +++ b/packages/dev-server/ui-plugin/extensions/ui-plugin.module.ts @@ -56,6 +56,7 @@ export class TestComponent { ], extensionHostConfig: new ExtensionHostConfig({ extensionUrl: './assets/vue-app/index.html', + openInNewTab: true, }), }, }, diff --git a/packages/ui-devkit/src/devkit-api.ts b/packages/ui-devkit/src/devkit-api.ts index ee7f7ebd6e..421e1cae27 100644 --- a/packages/ui-devkit/src/devkit-api.ts +++ b/packages/ui-devkit/src/devkit-api.ts @@ -96,6 +96,7 @@ function sendMessage(type: T['type'], data: T['data }; return new Observable(subscriber => { + const hostWindow = window.opener || window.parent; const handleReply = (event: MessageEvent) => { const response: MessageResponse = event.data; if (response && response.requestId === requestId) { @@ -113,10 +114,10 @@ function sendMessage(type: T['type'], data: T['data } }; const tearDown = () => { - window.parent.postMessage({ requestId, type: 'cancellation', data: null }, targetOrigin); + hostWindow.postMessage({ requestId, type: 'cancellation', data: null }, targetOrigin); }; window.addEventListener('message', handleReply); - window.parent.postMessage(message, targetOrigin); + hostWindow.postMessage(message, targetOrigin); return tearDown; });