devkit-client-api.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import {
  2. ActiveRouteData,
  3. BaseExtensionMessage,
  4. ExtensionMessage,
  5. MessageResponse,
  6. NotificationMessage,
  7. WatchQueryFetchPolicy,
  8. } from '@vendure/common/lib/extension-host-types';
  9. import { Observable } from 'rxjs';
  10. import { take } from 'rxjs/operators';
  11. let targetOrigin = 'http://localhost:3000';
  12. /**
  13. * @description
  14. * Set the [window.postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
  15. * `targetOrigin`. The Vendure ui-devkit uses the postMessage API to
  16. * enable cross-frame and cross-origin communication between the ui extension code and the Admin UI
  17. * app. The `targetOrigin` is a security feature intended to provide control over where messages are sent.
  18. *
  19. * @docsCategory ui-devkit
  20. * @docsPage UiDevkitClient
  21. */
  22. export function setTargetOrigin(value: string) {
  23. targetOrigin = value;
  24. }
  25. /**
  26. * @description
  27. * Retrieves information about the current route of the host application, since it is not possible
  28. * to otherwise get this information from within the child iframe.
  29. *
  30. * @example
  31. * ```TypeScript
  32. * import { getActivatedRoute } from '\@vendure/ui-devkit';
  33. *
  34. * const route = await getActivatedRoute();
  35. * const slug = route.params.slug;
  36. * ```
  37. * @docsCategory ui-devkit
  38. * @docsPage UiDevkitClient
  39. */
  40. export function getActivatedRoute(): Promise<ActiveRouteData> {
  41. return sendMessage('active-route', {}).toPromise();
  42. }
  43. /**
  44. * @description
  45. * Perform a GraphQL query and returns either an Observable or a Promise of the result.
  46. *
  47. * @example
  48. * ```TypeScript
  49. * import { graphQlQuery } from '\@vendure/ui-devkit';
  50. *
  51. * const productList = await graphQlQuery(`
  52. * query GetProducts($skip: Int, $take: Int) {
  53. * products(options: { skip: $skip, take: $take }) {
  54. * items { id, name, enabled },
  55. * totalItems
  56. * }
  57. * }`, {
  58. * skip: 0,
  59. * take: 10,
  60. * }).then(data => data.products);
  61. * ```
  62. *
  63. * @docsCategory ui-devkit
  64. * @docsPage UiDevkitClient
  65. */
  66. export function graphQlQuery<T, V extends { [key: string]: any }>(
  67. document: string,
  68. variables?: { [key: string]: any },
  69. fetchPolicy?: WatchQueryFetchPolicy,
  70. ): {
  71. then: Promise<T>['then'];
  72. stream: Observable<T>;
  73. } {
  74. const result$ = sendMessage('graphql-query', { document, variables, fetchPolicy });
  75. return {
  76. then: (...args: any[]) =>
  77. result$
  78. .pipe(take(1))
  79. .toPromise()
  80. .then(...args),
  81. stream: result$,
  82. };
  83. }
  84. /**
  85. * @description
  86. * Perform a GraphQL mutation and returns either an Observable or a Promise of the result.
  87. *
  88. * @example
  89. * ```TypeScript
  90. * import { graphQlMutation } from '\@vendure/ui-devkit';
  91. *
  92. * const disableProduct = (id: string) => {
  93. * return graphQlMutation(`
  94. * mutation DisableProduct($id: ID!) {
  95. * updateProduct(input: { id: $id, enabled: false }) {
  96. * id
  97. * enabled
  98. * }
  99. * }`, { id })
  100. * .then(data => data.updateProduct)
  101. * }
  102. * ```
  103. *
  104. * @docsCategory ui-devkit
  105. * @docsPage UiDevkitClient
  106. */
  107. export function graphQlMutation<T, V extends { [key: string]: any }>(
  108. document: string,
  109. variables?: { [key: string]: any },
  110. ): {
  111. then: Promise<T>['then'];
  112. stream: Observable<T>;
  113. } {
  114. const result$ = sendMessage('graphql-mutation', { document, variables });
  115. return {
  116. then: (...args: any[]) =>
  117. result$
  118. .pipe(take(1))
  119. .toPromise()
  120. .then(...args),
  121. stream: result$,
  122. };
  123. }
  124. /**
  125. * @description
  126. * Display a toast notification.
  127. *
  128. * @example
  129. * ```TypeScript
  130. * import { notify } from '\@vendure/ui-devkit';
  131. *
  132. * notify({
  133. * message: 'Updated Product',
  134. * type: 'success'
  135. * });
  136. * ```
  137. *
  138. * @docsCategory ui-devkit
  139. * @docsPage UiDevkitClient
  140. */
  141. export function notify(options: NotificationMessage['data']): void {
  142. sendMessage('notification', options).toPromise();
  143. }
  144. function sendMessage<T extends ExtensionMessage>(type: T['type'], data: T['data']): Observable<any> {
  145. const requestId = type + '__' + Math.random().toString(36).substr(3);
  146. const message: BaseExtensionMessage = {
  147. requestId,
  148. type,
  149. data,
  150. };
  151. return new Observable<any>(subscriber => {
  152. const hostWindow = window.opener || window.parent;
  153. const handleReply = (event: MessageEvent) => {
  154. const response: MessageResponse = event.data;
  155. if (response && response.requestId === requestId) {
  156. if (response.complete) {
  157. subscriber.complete();
  158. tearDown();
  159. return;
  160. }
  161. if (response.error) {
  162. subscriber.error(response.data);
  163. tearDown();
  164. return;
  165. }
  166. subscriber.next(response.data);
  167. }
  168. };
  169. const tearDown = () => {
  170. hostWindow.postMessage({ requestId, type: 'cancellation', data: null }, targetOrigin);
  171. };
  172. window.addEventListener('message', handleReply);
  173. hostWindow.postMessage(message, targetOrigin);
  174. return tearDown;
  175. });
  176. }