Explorar el Código

feat(ui-devkit): Expose route data in hosted UI extensions

Closes #1281
Michael Bromley hace 4 años
padre
commit
c3a21ff96d

+ 8 - 0
docs/content/plugins/extending-the-admin-ui/using-other-frameworks/_index.md

@@ -54,6 +54,14 @@ import { hostExternalFrame } from '@vendure/admin-ui/core';
     RouterModule.forChild([
       hostExternalFrame({
         path: '',
+          
+        // You can also use parameters which allow the app
+        // to have dynamic routing, e.g.
+        // path: ':slug'
+        // Then you can use the getActivatedRoute() function from the
+        // UiDevkitClient in order to access the value of the "slug"
+        // parameter.
+          
         breadcrumbLabel: 'React App',
         // This is the URL to the compiled React app index.
         // The next step will explain the "assets/react-app" path.

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts

@@ -56,7 +56,7 @@ export class ExtensionHostComponent implements OnInit, AfterViewInit, OnDestroy
         if (this.openInIframe) {
             const extensionWindow = this.extensionFrame.nativeElement.contentWindow;
             if (extensionWindow) {
-                this.extensionHostService.init(extensionWindow);
+                this.extensionHostService.init(extensionWindow, this.route.snapshot);
             }
         }
     }
@@ -72,7 +72,7 @@ export class ExtensionHostComponent implements OnInit, AfterViewInit, OnDestroy
         if (!extensionWindow) {
             return;
         }
-        this.extensionHostService.init(extensionWindow);
+        this.extensionHostService.init(extensionWindow, this.route.snapshot);
         this.extensionWindowIsOpen = true;
         this.extensionWindow = extensionWindow;
 

+ 27 - 5
packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.service.ts

@@ -1,5 +1,6 @@
 import { Injectable, OnDestroy } from '@angular/core';
-import { ExtensionMesssage, MessageResponse } from '@vendure/common/lib/extension-host-types';
+import { ActivatedRouteSnapshot, Router } from '@angular/router';
+import { ActiveRouteData, ExtensionMessage, MessageResponse } from '@vendure/common/lib/extension-host-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { parse } from 'graphql';
 import { merge, Observer, Subject } from 'rxjs';
@@ -11,13 +12,15 @@ import { NotificationService } from '../../../providers/notification/notificatio
 @Injectable()
 export class ExtensionHostService implements OnDestroy {
     private extensionWindow: Window;
+    private routeSnapshot: ActivatedRouteSnapshot;
     private cancellationMessage$ = new Subject<string>();
     private destroyMessage$ = new Subject<void>();
 
     constructor(private dataService: DataService, private notificationService: NotificationService) {}
 
-    init(extensionWindow: Window) {
+    init(extensionWindow: Window, routeSnapshot: ActivatedRouteSnapshot) {
         this.extensionWindow = extensionWindow;
+        this.routeSnapshot = routeSnapshot;
         window.addEventListener('message', this.handleMessage);
     }
 
@@ -30,7 +33,7 @@ export class ExtensionHostService implements OnDestroy {
         this.destroy();
     }
 
-    private handleMessage = (message: MessageEvent) => {
+    private handleMessage = (message: MessageEvent<ExtensionMessage>) => {
         const { data, origin } = message;
         if (this.isExtensionMessage(data)) {
             const cancellation$ = this.cancellationMessage$.pipe(
@@ -46,6 +49,25 @@ export class ExtensionHostService implements OnDestroy {
                     this.destroyMessage$.next();
                     break;
                 }
+                case 'active-route': {
+                    const routeData: ActiveRouteData = {
+                        url: window.location.href,
+                        origin: window.location.origin,
+                        pathname: window.location.pathname,
+                        params: this.routeSnapshot.params,
+                        queryParams: this.routeSnapshot.queryParams,
+                        fragment: this.routeSnapshot.fragment,
+                    };
+                    this.sendMessage(
+                        { data: routeData, error: false, complete: false, requestId: data.requestId },
+                        origin,
+                    );
+                    this.sendMessage(
+                        { data: null, error: false, complete: true, requestId: data.requestId },
+                        origin,
+                    );
+                    break;
+                }
                 case 'graphql-query': {
                     const { document, variables, fetchPolicy } = data.data;
                     this.dataService
@@ -70,7 +92,7 @@ export class ExtensionHostService implements OnDestroy {
                     assertNever(data);
             }
         }
-    }
+    };
 
     private createObserver(requestId: string, origin: string): Observer<any> {
         return {
@@ -84,7 +106,7 @@ export class ExtensionHostService implements OnDestroy {
         this.extensionWindow.postMessage(response, origin);
     }
 
-    private isExtensionMessage(input: any): input is ExtensionMesssage {
+    private isExtensionMessage(input: any): input is ExtensionMessage {
         return (
             input.hasOwnProperty('type') && input.hasOwnProperty('data') && input.hasOwnProperty('requestId')
         );

+ 15 - 1
packages/common/src/extension-host-types.ts

@@ -7,6 +7,19 @@ export interface BaseExtensionMessage {
     data: any;
 }
 
+export interface ActiveRouteData {
+    url: string;
+    origin: string;
+    pathname: string;
+    params: { [key: string]: any };
+    queryParams: { [key: string]: any };
+    fragment: string | null;
+}
+
+export interface ActivatedRouteMessage extends BaseExtensionMessage {
+    type: 'active-route';
+}
+
 export interface QueryMessage extends BaseExtensionMessage {
     type: 'graphql-query';
     data: {
@@ -44,7 +57,8 @@ export interface DestroyMessage extends BaseExtensionMessage {
     data: null;
 }
 
-export type ExtensionMesssage =
+export type ExtensionMessage =
+    | ActivatedRouteMessage
     | QueryMessage
     | MutationMessage
     | NotificationMessage

+ 66 - 9
packages/ui-devkit/src/client/devkit-client-api.ts

@@ -1,6 +1,7 @@
 import {
+    ActiveRouteData,
     BaseExtensionMessage,
-    ExtensionMesssage,
+    ExtensionMessage,
     MessageResponse,
     NotificationMessage,
     WatchQueryFetchPolicy,
@@ -24,10 +25,45 @@ export function setTargetOrigin(value: string) {
     targetOrigin = value;
 }
 
+/**
+ * @description
+ * Retrieves information about the current route of the host application, since it is not possible
+ * to otherwise get this information from within the child iframe.
+ *
+ * @example
+ * ```TypeScript
+ * import { getActivatedRoute } from '\@vendure/ui-devkit';
+ *
+ * const route = await getActivatedRoute();
+ * const slug = route.params.slug;
+ * ```
+ * @docsCategory ui-devkit
+ * @docsPage UiDevkitClient
+ */
+export function getActivatedRoute(): Promise<ActiveRouteData> {
+    return sendMessage('active-route', {}).toPromise();
+}
+
 /**
  * @description
  * Perform a GraphQL query and returns either an Observable or a Promise of the result.
  *
+ * @example
+ * ```TypeScript
+ * import { graphQlQuery } from '\@vendure/ui-devkit';
+ *
+ * const productList = await graphQlQuery(`
+ *   query GetProducts($skip: Int, $take: Int) {
+ *       products(options: { skip: $skip, take: $take }) {
+ *           items { id, name, enabled },
+ *           totalItems
+ *       }
+ *   }`, {
+ *     skip: 0,
+ *     take: 10,
+ *   }).then(data => data.products);
+ * ```
+ *
  * @docsCategory ui-devkit
  * @docsPage UiDevkitClient
  */
@@ -54,6 +90,22 @@ export function graphQlQuery<T, V extends { [key: string]: any }>(
  * @description
  * Perform a GraphQL mutation and returns either an Observable or a Promise of the result.
  *
+ * @example
+ * ```TypeScript
+ * import { graphQlMutation } from '\@vendure/ui-devkit';
+ *
+ * const disableProduct = (id: string) => {
+ *   return graphQlMutation(`
+ *     mutation DisableProduct($id: ID!) {
+ *       updateProduct(input: { id: $id, enabled: false }) {
+ *         id
+ *         enabled
+ *       }
+ *     }`, { id })
+ *     .then(data => data.updateProduct)
+ * }
+ * ```
+ *
  * @docsCategory ui-devkit
  * @docsPage UiDevkitClient
  */
@@ -79,20 +131,25 @@ export function graphQlMutation<T, V extends { [key: string]: any }>(
  * @description
  * Display a toast notification.
  *
+ * @example
+ * ```TypeScript
+ * import { notify } from '\@vendure/ui-devkit';
+ *
+ * notify({
+ *   message: 'Updated Product',
+ *   type: 'success'
+ * });
+ * ```
+ *
  * @docsCategory ui-devkit
  * @docsPage UiDevkitClient
  */
-export function notify(options: NotificationMessage['data']) {
+export function notify(options: NotificationMessage['data']): void {
     sendMessage('notification', options).toPromise();
 }
 
-function sendMessage<T extends ExtensionMesssage>(type: T['type'], data: T['data']): Observable<any> {
-    const requestId =
-        type +
-        '__' +
-        Math.random()
-            .toString(36)
-            .substr(3);
+function sendMessage<T extends ExtensionMessage>(type: T['type'], data: T['data']): Observable<any> {
+    const requestId = type + '__' + Math.random().toString(36).substr(3);
     const message: BaseExtensionMessage = {
         requestId,
         type,