diff --git a/docs/content/plugins/extending-the-admin-ui/using-other-frameworks/_index.md b/docs/content/plugins/extending-the-admin-ui/using-other-frameworks/_index.md index 3cfd767de5..baff4e7daf 100644 --- a/docs/content/plugins/extending-the-admin-ui/using-other-frameworks/_index.md +++ b/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. diff --git a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts index fa4be04839..f4f7e4ed74 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts +++ b/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; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.service.ts b/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.service.ts index bf720490e7..c33082c3e8 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.service.ts +++ b/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(); private destroyMessage$ = new Subject(); 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) => { 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 { 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') ); diff --git a/packages/common/src/extension-host-types.ts b/packages/common/src/extension-host-types.ts index 7038dcfa11..85f83df735 100644 --- a/packages/common/src/extension-host-types.ts +++ b/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 diff --git a/packages/ui-devkit/src/client/devkit-client-api.ts b/packages/ui-devkit/src/client/devkit-client-api.ts index 83bfadec5e..52ef3306db 100644 --- a/packages/ui-devkit/src/client/devkit-client-api.ts +++ b/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 { + 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( * @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( * @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(type: T['type'], data: T['data']): Observable { - const requestId = - type + - '__' + - Math.random() - .toString(36) - .substr(3); +function sendMessage(type: T['type'], data: T['data']): Observable { + const requestId = type + '__' + Math.random().toString(36).substr(3); const message: BaseExtensionMessage = { requestId, type,