Skip to content

Commit

Permalink
feat(ui-devkit): Expose route data in hosted UI extensions
Browse files Browse the repository at this point in the history
Closes #1281
  • Loading branch information
michaelbromley committed Dec 10, 2021
1 parent 788f839 commit c3a21ff
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}

Expand All @@ -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(
Expand All @@ -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
Expand All @@ -70,7 +92,7 @@ export class ExtensionHostService implements OnDestroy {
assertNever(data);
}
}
}
};

private createObserver(requestId: string, origin: string): Observer<any> {
return {
Expand All @@ -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')
);
Expand Down
16 changes: 15 additions & 1 deletion packages/common/src/extension-host-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -44,7 +57,8 @@ export interface DestroyMessage extends BaseExtensionMessage {
data: null;
}

export type ExtensionMesssage =
export type ExtensionMessage =
| ActivatedRouteMessage
| QueryMessage
| MutationMessage
| NotificationMessage
Expand Down
75 changes: 66 additions & 9 deletions packages/ui-devkit/src/client/devkit-client-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ActiveRouteData,
BaseExtensionMessage,
ExtensionMesssage,
ExtensionMessage,
MessageResponse,
NotificationMessage,
WatchQueryFetchPolicy,
Expand All @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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,
Expand Down

0 comments on commit c3a21ff

Please sign in to comment.