Browse Source

Merge branch 'minor'

Michael Bromley 1 năm trước cách đây
mục cha
commit
bb562a688b
42 tập tin đã thay đổi với 795 bổ sung757 xóa
  1. 1 0
      docs/docs/guides/extending-the-admin-ui/defining-routes/index.md
  2. 16 0
      docs/docs/guides/extending-the-admin-ui/page-tabs/index.md
  3. 8 4
      docs/docs/reference/admin-ui-api/react-hooks/use-lazy-query.md
  4. 6 2
      docs/docs/reference/admin-ui-api/react-hooks/use-query.md
  5. 1 1
      docs/docs/reference/admin-ui-api/routes/register-route-component.md
  6. 1 1
      docs/docs/reference/admin-ui-api/ui-devkit/compile-ui-extensions.md
  7. 20 0
      docs/docs/reference/admin-ui-api/ui-devkit/ui-extension-build-command.md
  8. 4 4
      docs/docs/reference/admin-ui-api/ui-devkit/ui-extension-compiler-options.md
  9. 2 3
      docs/docs/reference/typescript-api/events/event-types.md
  10. 232 637
      package-lock.json
  11. 4 4
      package.json
  12. 1 1
      packages/admin-ui/package.json
  13. 22 2
      packages/admin-ui/src/lib/core/src/extension/components/route.component.ts
  14. 5 1
      packages/admin-ui/src/lib/core/src/extension/register-route-component.ts
  15. 21 9
      packages/admin-ui/src/lib/react/src/react-hooks/use-query.ts
  16. 13 13
      packages/core/package.json
  17. 1 0
      packages/core/src/api/config/configure-graphql-module.ts
  18. 6 1
      packages/core/src/config/config.module.ts
  19. 2 0
      packages/core/src/config/default-config.ts
  20. 1 0
      packages/core/src/config/index.ts
  21. 53 0
      packages/core/src/config/refund/default-refund-process.ts
  22. 41 0
      packages/core/src/config/refund/refund-process.ts
  23. 9 0
      packages/core/src/config/vendure-config.ts
  24. 1 1
      packages/core/src/event-bus/events/order-line-event.ts
  25. 21 0
      packages/core/src/event-bus/events/refund-event.ts
  26. 1 0
      packages/core/src/event-bus/index.ts
  27. 2 0
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  28. 70 31
      packages/core/src/service/helpers/refund-state-machine/refund-state-machine.ts
  29. 18 16
      packages/core/src/service/helpers/refund-state-machine/refund-state.ts
  30. 41 1
      packages/core/src/service/services/order.service.ts
  31. 15 0
      packages/create/src/gather-user-responses.ts
  32. 15 0
      packages/create/src/helpers.ts
  33. 3 0
      packages/create/templates/vendure-config.hbs
  34. 31 1
      packages/dev-server/example-plugins/ui-extensions-library/ui/providers.ts
  35. 4 0
      packages/dev-server/example-plugins/ui-extensions-library/ui/routes.ts
  36. 13 3
      packages/email-plugin/src/handler/event-handler.ts
  37. 51 2
      packages/email-plugin/src/plugin.spec.ts
  38. 8 0
      packages/email-plugin/src/types.ts
  39. 1 1
      packages/testing/package.json
  40. 16 12
      packages/ui-devkit/src/compiler/compile.ts
  41. 10 2
      packages/ui-devkit/src/compiler/types.ts
  42. 4 4
      packages/ui-devkit/src/compiler/utils.ts

+ 1 - 0
docs/docs/guides/extending-the-admin-ui/defining-routes/index.md

@@ -482,6 +482,7 @@ export default [
     registerRouteComponent({
     registerRouteComponent({
         component: TestComponent,
         component: TestComponent,
         title: 'Test',
         title: 'Test',
+        locationId: 'my-location-id'
         // highlight-next-line
         // highlight-next-line
         breadcrumb: 'Test',
         breadcrumb: 'Test',
     }),
     }),

+ 16 - 0
docs/docs/guides/extending-the-admin-ui/page-tabs/index.md

@@ -23,6 +23,22 @@ export default [
 
 
 ![./ui-extensions-tabs.webp](./ui-extensions-tabs.webp)
 ![./ui-extensions-tabs.webp](./ui-extensions-tabs.webp)
 
 
+If you want to add page tabs to a custom admin page, specify the `locationId` property:
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { TestComponent } from './components/test/test.component';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        title: 'Test',
+        // highlight-next-line
+        locationId: 'my-location-id'
+    }),
+];
+```
+
 :::note
 :::note
 Currently it is only possible to define new tabs using Angular components.
 Currently it is only possible to define new tabs using Angular components.
 :::
 :::

+ 8 - 4
docs/docs/reference/admin-ui-api/react-hooks/use-lazy-query.md

@@ -11,9 +11,9 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## useLazyQuery
 ## useLazyQuery
 
 
-<GenerationInfo sourceFile="packages/admin-ui/src/lib/react/src/react-hooks/use-query.ts" sourceLine="113" packageName="@vendure/admin-ui" since="2.2.0" />
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/react/src/react-hooks/use-query.ts" sourceLine="121" packageName="@vendure/admin-ui" since="2.2.0" />
 
 
-A React hook which allows you to execute a GraphQL query.
+A React hook which allows you to execute a GraphQL query lazily.
 
 
 *Example*
 *Example*
 
 
@@ -37,7 +37,7 @@ type ProductResponse = {
 }
 }
 
 
 export const MyComponent = () => {
 export const MyComponent = () => {
-    const [getProduct, { data, loading, error }] = useLazyQuery<ProductResponse>(GET_PRODUCT);
+    const [getProduct, { data, loading, error }] = useLazyQuery<ProductResponse>(GET_PRODUCT, { refetchOnChannelChange: true });
 
 
    const handleClick = () => {
    const handleClick = () => {
         getProduct({
         getProduct({
@@ -64,7 +64,7 @@ export const MyComponent = () => {
 ```
 ```
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function useLazyQuery<T, V extends Record<string, any> = Record<string, any>>(query: DocumentNode | TypedDocumentNode<T, V>): void
+function useLazyQuery<T, V extends Record<string, any> = Record<string, any>>(query: DocumentNode | TypedDocumentNode<T, V>, options: {refetchOnChannelChange: boolean } = {refetchOnChannelChange: false}): void
 ```
 ```
 Parameters
 Parameters
 
 
@@ -72,3 +72,7 @@ Parameters
 
 
 <MemberInfo kind="parameter" type={`DocumentNode | TypedDocumentNode&#60;T, V&#62;`} />
 <MemberInfo kind="parameter" type={`DocumentNode | TypedDocumentNode&#60;T, V&#62;`} />
 
 
+### options
+
+<MemberInfo kind="parameter" type={`{refetchOnChannelChange: boolean }`} />
+

+ 6 - 2
docs/docs/reference/admin-ui-api/react-hooks/use-query.md

@@ -31,7 +31,7 @@ const GET_PRODUCT = gql`
    }`;
    }`;
 
 
 export const MyComponent = () => {
 export const MyComponent = () => {
-    const { data, loading, error } = useQuery(GET_PRODUCT, { id: '1' });
+    const { data, loading, error } = useQuery(GET_PRODUCT, { id: '1' }, { refetchOnChannelChange: true });
 
 
     if (loading) return <div>Loading...</div>;
     if (loading) return <div>Loading...</div>;
     if (error) return <div>Error! { error }</div>;
     if (error) return <div>Error! { error }</div>;
@@ -45,7 +45,7 @@ export const MyComponent = () => {
 ```
 ```
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function useQuery<T, V extends Record<string, any> = Record<string, any>>(query: DocumentNode | TypedDocumentNode<T, V>, variables?: V): void
+function useQuery<T, V extends Record<string, any> = Record<string, any>>(query: DocumentNode | TypedDocumentNode<T, V>, variables?: V, options: { refetchOnChannelChange: boolean } = { refetchOnChannelChange: false }): void
 ```
 ```
 Parameters
 Parameters
 
 
@@ -57,3 +57,7 @@ Parameters
 
 
 <MemberInfo kind="parameter" type={`V`} />
 <MemberInfo kind="parameter" type={`V`} />
 
 
+### options
+
+<MemberInfo kind="parameter" type={`{ refetchOnChannelChange: boolean }`} />
+

+ 1 - 1
docs/docs/reference/admin-ui-api/routes/register-route-component.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## registerRouteComponent
 ## registerRouteComponent
 
 
-<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/extension/register-route-component.ts" sourceLine="77" packageName="@vendure/admin-ui" />
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/extension/register-route-component.ts" sourceLine="79" packageName="@vendure/admin-ui" />
 
 
 Registers an Angular standalone component to be rendered in a route.
 Registers an Angular standalone component to be rendered in a route.
 
 

+ 1 - 1
docs/docs/reference/admin-ui-api/ui-devkit/compile-ui-extensions.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## compileUiExtensions
 ## compileUiExtensions
 
 
-<GenerationInfo sourceFile="packages/ui-devkit/src/compiler/compile.ts" sourceLine="35" packageName="@vendure/ui-devkit" />
+<GenerationInfo sourceFile="packages/ui-devkit/src/compiler/compile.ts" sourceLine="36" packageName="@vendure/ui-devkit" />
 
 
 Compiles the Admin UI app with the specified extensions.
 Compiles the Admin UI app with the specified extensions.
 
 

+ 20 - 0
docs/docs/reference/admin-ui-api/ui-devkit/ui-extension-build-command.md

@@ -0,0 +1,20 @@
+---
+title: "UiExtensionBuildCommand"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## UiExtensionBuildCommand
+
+<GenerationInfo sourceFile="packages/ui-devkit/src/compiler/types.ts" sourceLine="356" packageName="@vendure/ui-devkit" />
+
+The package manager to use when invoking the Angular CLI to build UI extensions.
+
+```ts title="Signature"
+type UiExtensionBuildCommand = 'npm' | 'yarn' | 'pnpm'
+```

+ 4 - 4
docs/docs/reference/admin-ui-api/ui-devkit/ui-extension-compiler-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## UiExtensionCompilerOptions
 ## UiExtensionCompilerOptions
 
 
-<GenerationInfo sourceFile="packages/ui-devkit/src/compiler/types.ts" sourceLine="356" packageName="@vendure/ui-devkit" />
+<GenerationInfo sourceFile="packages/ui-devkit/src/compiler/types.ts" sourceLine="364" packageName="@vendure/ui-devkit" />
 
 
 Options to configure how the Admin UI should be compiled.
 Options to configure how the Admin UI should be compiled.
 
 
@@ -23,7 +23,7 @@ interface UiExtensionCompilerOptions {
     devMode?: boolean;
     devMode?: boolean;
     baseHref?: string;
     baseHref?: string;
     watchPort?: number;
     watchPort?: number;
-    command?: 'yarn' | 'npm';
+    command?: UiExtensionBuildCommand;
     additionalProcessArguments?: UiExtensionCompilerProcessArgument[];
     additionalProcessArguments?: UiExtensionCompilerProcessArgument[];
 }
 }
 ```
 ```
@@ -102,11 +102,11 @@ In watch mode, allows the port of the dev server to be specified. Defaults to th
 of `4200`.
 of `4200`.
 ### command
 ### command
 
 
-<MemberInfo kind="property" type={`'yarn' | 'npm'`}  since="1.5.0"  />
+<MemberInfo kind="property" type={`<a href='/reference/admin-ui-api/ui-devkit/ui-extension-build-command#uiextensionbuildcommand'>UiExtensionBuildCommand</a>`}  since="1.5.0"  />
 
 
 Internally, the Angular CLI will be invoked as an npm script. By default, the compiler will use Yarn
 Internally, the Angular CLI will be invoked as an npm script. By default, the compiler will use Yarn
 to run the script if it is detected, otherwise it will use npm. This setting allows you to explicitly
 to run the script if it is detected, otherwise it will use npm. This setting allows you to explicitly
-set which command to use, rather than relying on the default behavior.
+set which command to use, including pnpm, rather than relying on the default behavior.
 ### additionalProcessArguments
 ### additionalProcessArguments
 
 
 <MemberInfo kind="property" type={`<a href='/reference/admin-ui-api/ui-devkit/ui-extension-compiler-process-argument#uiextensioncompilerprocessargument'>UiExtensionCompilerProcessArgument</a>[]`} default="undefined"  since="1.5.0"  />
 <MemberInfo kind="property" type={`<a href='/reference/admin-ui-api/ui-devkit/ui-extension-compiler-process-argument#uiextensioncompilerprocessargument'>UiExtensionCompilerProcessArgument</a>[]`} default="undefined"  since="1.5.0"  />

+ 2 - 3
docs/docs/reference/typescript-api/events/event-types.md

@@ -811,12 +811,11 @@ class OrderEvent extends VendureEvent {
 
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/order-line-event.ts" sourceLine="13" packageName="@vendure/core" />
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/order-line-event.ts" sourceLine="13" packageName="@vendure/core" />
 
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a> is added, updated
-or deleted.
+This event is fired whenever an <a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a> is added, updated, deleted or cancelled.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
 class OrderLineEvent extends VendureEvent {
 class OrderLineEvent extends VendureEvent {
-    constructor(ctx: RequestContext, order: Order, orderLine: OrderLine, type: 'created' | 'updated' | 'deleted')
+    constructor(ctx: RequestContext, order: Order, orderLine: OrderLine, type: 'created' | 'updated' | 'deleted' | 'cancelled')
 }
 }
 ```
 ```
 * Extends: <code><a href='/reference/typescript-api/events/vendure-event#vendureevent'>VendureEvent</a></code>
 * Extends: <code><a href='/reference/typescript-api/events/vendure-event#vendureevent'>VendureEvent</a></code>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 232 - 637
package-lock.json


+ 4 - 4
package.json

@@ -34,9 +34,9 @@
     "@graphql-codegen/cli": "5.0.2",
     "@graphql-codegen/cli": "5.0.2",
     "@graphql-codegen/fragment-matcher": "5.0.2",
     "@graphql-codegen/fragment-matcher": "5.0.2",
     "@graphql-codegen/typed-document-node": "^5.0.6",
     "@graphql-codegen/typed-document-node": "^5.0.6",
-    "@graphql-codegen/typescript": "4.0.6",
-    "@graphql-codegen/typescript-operations": "4.2.0",
-    "@graphql-tools/schema": "^10.0.3",
+    "@graphql-codegen/typescript": "4.0.9",
+    "@graphql-codegen/typescript-operations": "4.2.3",
+    "@graphql-tools/schema": "^10.0.4",
     "@swc/core": "^1.4.6",
     "@swc/core": "^1.4.6",
     "@types/klaw-sync": "^6.0.5",
     "@types/klaw-sync": "^6.0.5",
     "@types/node": "^20.11.19",
     "@types/node": "^20.11.19",
@@ -44,7 +44,7 @@
     "conventional-changelog-core": "^7.0.0",
     "conventional-changelog-core": "^7.0.0",
     "cross-env": "^7.0.3",
     "cross-env": "^7.0.3",
     "find": "^0.3.0",
     "find": "^0.3.0",
-    "graphql": "16.8.1",
+    "graphql": "~16.9.0",
     "husky": "^4.3.0",
     "husky": "^4.3.0",
     "klaw-sync": "^6.0.0",
     "klaw-sync": "^6.0.0",
     "lerna": "^8.1.2",
     "lerna": "^8.1.2",

+ 1 - 1
packages/admin-ui/package.json

@@ -56,7 +56,7 @@
         "chartist": "^1.3.0",
         "chartist": "^1.3.0",
         "codejar": "^4.2.0",
         "codejar": "^4.2.0",
         "dayjs": "^1.11.10",
         "dayjs": "^1.11.10",
-        "graphql": "16.8.1",
+        "graphql": "~16.9.0",
         "just-extend": "^6.2.0",
         "just-extend": "^6.2.0",
         "messageformat": "2.3.0",
         "messageformat": "2.3.0",
         "ngx-pagination": "^6.0.3",
         "ngx-pagination": "^6.0.3",

+ 22 - 2
packages/admin-ui/src/lib/core/src/extension/components/route.component.ts

@@ -1,4 +1,4 @@
-import { Component, inject, InjectionToken } from '@angular/core';
+import { Component, inject, InjectionToken, Input } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { ActivatedRoute } from '@angular/router';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { combineLatest, Observable, of, switchMap } from 'rxjs';
 import { combineLatest, Observable, of, switchMap } from 'rxjs';
@@ -7,6 +7,9 @@ import { BreadcrumbValue } from '../../providers/breadcrumb/breadcrumb.service';
 import { SharedModule } from '../../shared/shared.module';
 import { SharedModule } from '../../shared/shared.module';
 import { PageMetadataService } from '../providers/page-metadata.service';
 import { PageMetadataService } from '../providers/page-metadata.service';
 import { AngularRouteComponentOptions } from '../types';
 import { AngularRouteComponentOptions } from '../types';
+import { HeaderTab } from '../../shared/components/page-header-tabs/page-header-tabs.component';
+import { PageService } from '../../providers/page/page.service';
+import { PageLocationId } from '../../common/component-registry-types';
 
 
 export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<AngularRouteComponentOptions>(
 export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<AngularRouteComponentOptions>(
     'ROUTE_COMPONENT_OPTIONS',
     'ROUTE_COMPONENT_OPTIONS',
@@ -17,6 +20,8 @@ export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<AngularRouteComponentO
     template: `
     template: `
         <vdr-page-header>
         <vdr-page-header>
             <vdr-page-title *ngIf="title$ | async as title" [title]="title"></vdr-page-title>
             <vdr-page-title *ngIf="title$ | async as title" [title]="title"></vdr-page-title>
+            <vdr-page-header-description *ngIf="description">{{ description }}</vdr-page-header-description>
+            <vdr-page-header-tabs *ngIf="headerTabs.length > 1" [tabs]="headerTabs"></vdr-page-header-tabs>
         </vdr-page-header>
         </vdr-page-header>
         <vdr-page-body><ng-content /></vdr-page-body>
         <vdr-page-body><ng-content /></vdr-page-body>
     `,
     `,
@@ -26,8 +31,14 @@ export const ROUTE_COMPONENT_OPTIONS = new InjectionToken<AngularRouteComponentO
 })
 })
 export class RouteComponent {
 export class RouteComponent {
     protected title$: Observable<string | undefined>;
     protected title$: Observable<string | undefined>;
+    @Input() protected locationId: PageLocationId;
+    @Input() protected description: string;
+    headerTabs: HeaderTab[] = [];
 
 
-    constructor(private route: ActivatedRoute) {
+    constructor(
+        private route: ActivatedRoute,
+        private pageService: PageService,
+    ) {
         const breadcrumbLabel$ = this.route.data.pipe(
         const breadcrumbLabel$ = this.route.data.pipe(
             switchMap(data => {
             switchMap(data => {
                 if (data.breadcrumb instanceof Observable) {
                 if (data.breadcrumb instanceof Observable) {
@@ -53,5 +64,14 @@ export class RouteComponent {
         this.title$ = combineLatest([inject(ROUTE_COMPONENT_OPTIONS).title$, breadcrumbLabel$]).pipe(
         this.title$ = combineLatest([inject(ROUTE_COMPONENT_OPTIONS).title$, breadcrumbLabel$]).pipe(
             map(([title, breadcrumbLabel]) => title ?? breadcrumbLabel),
             map(([title, breadcrumbLabel]) => title ?? breadcrumbLabel),
         );
         );
+
+        this.locationId = this.route.snapshot.data.locationId;
+        this.description = this.route.snapshot.data.description;
+        this.headerTabs = this.pageService.getPageTabs(this.locationId).map(tab => ({
+            id: tab.tab,
+            label: tab.tab,
+            icon: tab.tabIcon,
+            route: tab.route ? [tab.route] : ['./'],
+        }));
     }
     }
 }
 }

+ 5 - 1
packages/admin-ui/src/lib/core/src/extension/register-route-component.ts

@@ -25,6 +25,8 @@ export type RegisterRouteComponentOptions<
 > = {
 > = {
     component: Type<Component> | Component;
     component: Type<Component> | Component;
     title?: string;
     title?: string;
+    locationId?: string;
+    description?: string;
     breadcrumb?: BreadcrumbValue;
     breadcrumb?: BreadcrumbValue;
     path?: string;
     path?: string;
     query?: T;
     query?: T;
@@ -81,7 +83,7 @@ export function registerRouteComponent<
     Field extends keyof ResultOf<T>,
     Field extends keyof ResultOf<T>,
     R extends Field,
     R extends Field,
 >(options: RegisterRouteComponentOptions<Component, Entity, T, Field, R>) {
 >(options: RegisterRouteComponentOptions<Component, Entity, T, Field, R>) {
-    const { query, entityKey, variables, getBreadcrumbs } = options;
+    const { query, entityKey, variables, getBreadcrumbs, locationId, description } = options;
 
 
     const breadcrumbSubject$ = new BehaviorSubject<BreadcrumbValue>(options.breadcrumb ?? '');
     const breadcrumbSubject$ = new BehaviorSubject<BreadcrumbValue>(options.breadcrumb ?? '');
     const titleSubject$ = new BehaviorSubject<string | undefined>(options.title);
     const titleSubject$ = new BehaviorSubject<string | undefined>(options.title);
@@ -129,6 +131,8 @@ export function registerRouteComponent<
         ...(options.routeConfig ?? {}),
         ...(options.routeConfig ?? {}),
         resolve: { ...(resolveFn ? { detail: resolveFn } : {}), ...(options.routeConfig?.resolve ?? {}) },
         resolve: { ...(resolveFn ? { detail: resolveFn } : {}), ...(options.routeConfig?.resolve ?? {}) },
         data: {
         data: {
+            locationId,
+            description,
             breadcrumb: breadcrumbSubject$,
             breadcrumb: breadcrumbSubject$,
             ...(options.routeConfig?.data ?? {}),
             ...(options.routeConfig?.data ?? {}),
             ...(getBreadcrumbs && query && entityKey
             ...(getBreadcrumbs && query && entityKey

+ 21 - 9
packages/admin-ui/src/lib/react/src/react-hooks/use-query.ts

@@ -25,7 +25,7 @@ import { HostedComponentContext } from '../directives/react-component-host.direc
  *    }`;
  *    }`;
  *
  *
  * export const MyComponent = () => {
  * export const MyComponent = () => {
- *     const { data, loading, error } = useQuery(GET_PRODUCT, { id: '1' });
+ *     const { data, loading, error } = useQuery(GET_PRODUCT, { id: '1' }, { refetchOnChannelChange: true });
  *
  *
  *     if (loading) return <div>Loading...</div>;
  *     if (loading) return <div>Loading...</div>;
  *     if (error) return <div>Error! { error }</div>;
  *     if (error) return <div>Error! { error }</div>;
@@ -43,10 +43,16 @@ import { HostedComponentContext } from '../directives/react-component-host.direc
 export function useQuery<T, V extends Record<string, any> = Record<string, any>>(
 export function useQuery<T, V extends Record<string, any> = Record<string, any>>(
     query: DocumentNode | TypedDocumentNode<T, V>,
     query: DocumentNode | TypedDocumentNode<T, V>,
     variables?: V,
     variables?: V,
+    options: { refetchOnChannelChange: boolean } = { refetchOnChannelChange: false },
 ) {
 ) {
-    const { data, loading, error, runQuery } = useDataService<T, V>(
-        (dataService, vars) => dataService.query(query, vars).stream$,
-    );
+    const { refetchOnChannelChange } = options;
+    const { data, loading, error, runQuery } = useDataService<T, V>((dataService, vars) => {
+        let queryFn = dataService.query(query, vars);
+        if (refetchOnChannelChange) {
+            queryFn = queryFn.refetchOnChannelChange();
+        }
+        return queryFn.stream$;
+    });
     useEffect(() => {
     useEffect(() => {
         const subscription = runQuery(variables).subscribe();
         const subscription = runQuery(variables).subscribe();
         return () => subscription.unsubscribe();
         return () => subscription.unsubscribe();
@@ -58,7 +64,7 @@ export function useQuery<T, V extends Record<string, any> = Record<string, any>>
 
 
 /**
 /**
  * @description
  * @description
- * A React hook which allows you to execute a GraphQL query.
+ * A React hook which allows you to execute a GraphQL query lazily.
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
@@ -81,7 +87,7 @@ export function useQuery<T, V extends Record<string, any> = Record<string, any>>
  * }
  * }
  *
  *
  * export const MyComponent = () => {
  * export const MyComponent = () => {
- *     const [getProduct, { data, loading, error }] = useLazyQuery<ProductResponse>(GET_PRODUCT);
+ *     const [getProduct, { data, loading, error }] = useLazyQuery<ProductResponse>(GET_PRODUCT, { refetchOnChannelChange: true });
  *
  *
  *    const handleClick = () => {
  *    const handleClick = () => {
  *         getProduct({
  *         getProduct({
@@ -112,10 +118,16 @@ export function useQuery<T, V extends Record<string, any> = Record<string, any>>
  */
  */
 export function useLazyQuery<T, V extends Record<string, any> = Record<string, any>>(
 export function useLazyQuery<T, V extends Record<string, any> = Record<string, any>>(
     query: DocumentNode | TypedDocumentNode<T, V>,
     query: DocumentNode | TypedDocumentNode<T, V>,
+    options: { refetchOnChannelChange: boolean } = { refetchOnChannelChange: false },
 ) {
 ) {
-    const { data, loading, error, runQuery } = useDataService<T, V>(
-        (dataService, vars) => dataService.query(query, vars).stream$,
-    );
+    const { refetchOnChannelChange } = options;
+    const { data, loading, error, runQuery } = useDataService<T, V>((dataService, vars) => {
+        let queryFn = dataService.query(query, vars);
+        if (refetchOnChannelChange) {
+            queryFn = queryFn.refetchOnChannelChange();
+        }
+        return queryFn.stream$;
+    });
     const rest = { data, loading, error };
     const rest = { data, loading, error };
     const execute = (variables?: V) => firstValueFrom(runQuery(variables));
     const execute = (variables?: V) => firstValueFrom(runQuery(variables));
     return [execute, rest] as [typeof execute, typeof rest];
     return [execute, rest] as [typeof execute, typeof rest];

+ 13 - 13
packages/core/package.json

@@ -40,16 +40,16 @@
         "cli/**/*"
         "cli/**/*"
     ],
     ],
     "dependencies": {
     "dependencies": {
-        "@apollo/server": "^4.10.1",
-        "@graphql-tools/stitch": "^9.0.5",
-        "@nestjs/apollo": "^12.1.0",
-        "@nestjs/common": "10.3.3",
-        "@nestjs/core": "10.3.3",
-        "@nestjs/graphql": "12.1.1",
-        "@nestjs/platform-express": "10.3.3",
-        "@nestjs/terminus": "10.2.3",
-        "@nestjs/testing": "10.3.3",
-        "@nestjs/typeorm": "10.0.2",
+        "@apollo/server": "^4.10.4",
+        "@graphql-tools/stitch": "^9.2.10",
+        "@nestjs/apollo": "~12.2.0",
+        "@nestjs/common": "~10.3.10",
+        "@nestjs/core": "~10.3.10",
+        "@nestjs/graphql": "~12.2.0",
+        "@nestjs/platform-express": "~10.3.10",
+        "@nestjs/terminus": "~10.2.3",
+        "@nestjs/testing": "~10.3.10",
+        "@nestjs/typeorm": "~10.0.2",
         "@types/fs-extra": "^9.0.1",
         "@types/fs-extra": "^9.0.1",
         "@vendure/common": "^2.2.7",
         "@vendure/common": "^2.2.7",
         "bcrypt": "^5.1.1",
         "bcrypt": "^5.1.1",
@@ -58,13 +58,13 @@
         "csv-parse": "^5.5.5",
         "csv-parse": "^5.5.5",
         "express": "^4.18.3",
         "express": "^4.18.3",
         "fs-extra": "^11.2.0",
         "fs-extra": "^11.2.0",
-        "graphql": "16.8.1",
+        "graphql": "~16.9.0",
         "graphql-fields": "^2.0.3",
         "graphql-fields": "^2.0.3",
         "graphql-scalars": "^1.22.5",
         "graphql-scalars": "^1.22.5",
         "graphql-tag": "^2.12.6",
         "graphql-tag": "^2.12.6",
         "graphql-upload": "^16.0.2",
         "graphql-upload": "^16.0.2",
         "http-proxy-middleware": "^2.0.6",
         "http-proxy-middleware": "^2.0.6",
-        "i18next": "^23.10.1",
+        "i18next": "^23.12.1",
         "i18next-fs-backend": "^2.3.1",
         "i18next-fs-backend": "^2.3.1",
         "i18next-http-middleware": "^3.5.0",
         "i18next-http-middleware": "^3.5.0",
         "i18next-icu": "^2.3.0",
         "i18next-icu": "^2.3.0",
@@ -75,7 +75,7 @@
         "nanoid": "^3.3.7",
         "nanoid": "^3.3.7",
         "picocolors": "^1.0.0",
         "picocolors": "^1.0.0",
         "progress": "^2.0.3",
         "progress": "^2.0.3",
-        "reflect-metadata": "^0.2.1",
+        "reflect-metadata": "^0.2.2",
         "rxjs": "^7.8.1",
         "rxjs": "^7.8.1",
         "semver": "^7.6.0",
         "semver": "^7.6.0",
         "typeorm": "0.3.20"
         "typeorm": "0.3.20"

+ 1 - 0
packages/core/src/api/config/configure-graphql-module.ts

@@ -100,6 +100,7 @@ async function createGraphQLOptions(
         path: '/' + options.apiPath,
         path: '/' + options.apiPath,
         typeDefs: printSchema(builtSchema),
         typeDefs: printSchema(builtSchema),
         include: [options.resolverModule],
         include: [options.resolverModule],
+        inheritResolversFromInterfaces: true,
         fieldResolverEnhancers: ['guards'],
         fieldResolverEnhancers: ['guards'],
         resolvers,
         resolvers,
         // We no longer rely on the upload facility bundled with Apollo Server, and instead
         // We no longer rely on the upload facility bundled with Apollo Server, and instead

+ 6 - 1
packages/core/src/config/config.module.ts

@@ -14,7 +14,10 @@ import { ConfigService } from './config.service';
     exports: [ConfigService],
     exports: [ConfigService],
 })
 })
 export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdown {
 export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdown {
-    constructor(private configService: ConfigService, private moduleRef: ModuleRef) {}
+    constructor(
+        private configService: ConfigService,
+        private moduleRef: ModuleRef,
+    ) {}
 
 
     async onApplicationBootstrap() {
     async onApplicationBootstrap() {
         await this.initInjectableStrategies();
         await this.initInjectableStrategies();
@@ -106,6 +109,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
         const { entityIdStrategy } = this.configService.entityOptions;
         const { entityIdStrategy } = this.configService.entityOptions;
         const { healthChecks, errorHandlers } = this.configService.systemOptions;
         const { healthChecks, errorHandlers } = this.configService.systemOptions;
         const { assetImportStrategy } = this.configService.importExportOptions;
         const { assetImportStrategy } = this.configService.importExportOptions;
+        const { refundProcess: refundProcess } = this.configService.paymentOptions;
         return [
         return [
             ...adminAuthenticationStrategy,
             ...adminAuthenticationStrategy,
             ...shopAuthenticationStrategy,
             ...shopAuthenticationStrategy,
@@ -145,6 +149,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             stockLocationStrategy,
             stockLocationStrategy,
             productVariantPriceSelectionStrategy,
             productVariantPriceSelectionStrategy,
             guestCheckoutStrategy,
             guestCheckoutStrategy,
+            ...refundProcess,
         ];
         ];
     }
     }
 
 

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -43,6 +43,7 @@ import { DefaultOrderCodeStrategy } from './order/order-code-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
 import { defaultPaymentProcess } from './payment/default-payment-process';
 import { defaultPaymentProcess } from './payment/default-payment-process';
 import { defaultPromotionActions, defaultPromotionConditions } from './promotion';
 import { defaultPromotionActions, defaultPromotionConditions } from './promotion';
+import { defaultRefundProcess } from './refund/default-refund-process';
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
@@ -170,6 +171,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         paymentMethodHandlers: [],
         paymentMethodHandlers: [],
         customPaymentProcess: [],
         customPaymentProcess: [],
         process: [defaultPaymentProcess],
         process: [defaultPaymentProcess],
+        refundProcess: [defaultRefundProcess],
     },
     },
     taxOptions: {
     taxOptions: {
         taxZoneStrategy: new DefaultTaxZoneStrategy(),
         taxZoneStrategy: new DefaultTaxZoneStrategy(),

+ 1 - 0
packages/core/src/config/index.ts

@@ -89,3 +89,4 @@ export * from './tax/default-tax-zone-strategy';
 export * from './tax/tax-line-calculation-strategy';
 export * from './tax/tax-line-calculation-strategy';
 export * from './tax/tax-zone-strategy';
 export * from './tax/tax-zone-strategy';
 export * from './vendure-config';
 export * from './vendure-config';
+export * from './refund/default-refund-process';

+ 53 - 0
packages/core/src/config/refund/default-refund-process.ts

@@ -0,0 +1,53 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+
+import { RefundState } from '../../service/helpers/refund-state-machine/refund-state';
+
+import { RefundProcess } from './refund-process';
+
+let configService: import('../config.service').ConfigService;
+let historyService: import('../../service/index').HistoryService;
+
+/**
+ * @description
+ * The default {@link RefundProcess}.
+ *
+ * @docsCategory refund
+ */
+export const defaultRefundProcess: RefundProcess<RefundState> = {
+    transitions: {
+        Pending: {
+            to: ['Settled', 'Failed'],
+        },
+        Settled: {
+            to: [],
+        },
+        Failed: {
+            to: [],
+        },
+    },
+    async init(injector) {
+        const ConfigService = await import('../config.service.js').then(m => m.ConfigService);
+        const HistoryService = await import('../../service/index.js').then(m => m.HistoryService);
+        configService = injector.get(ConfigService);
+        historyService = injector.get(HistoryService);
+    },
+    onTransitionStart: async (fromState, toState, data) => {
+        return true;
+    },
+    onTransitionEnd: async (fromState, toState, data) => {
+        if (!historyService) {
+            throw new Error('HistoryService has not been initialized');
+        }
+        await historyService.createHistoryEntryForOrder({
+            ctx: data.ctx,
+            orderId: data.order.id,
+            type: HistoryEntryType.ORDER_REFUND_TRANSITION,
+            data: {
+                refundId: data.refund.id,
+                from: fromState,
+                to: toState,
+                reason: data.refund.reason,
+            },
+        });
+    },
+};

+ 41 - 0
packages/core/src/config/refund/refund-process.ts

@@ -0,0 +1,41 @@
+import {
+    OnTransitionEndFn,
+    OnTransitionErrorFn,
+    OnTransitionStartFn,
+    Transitions,
+} from '../../common/finite-state-machine/types';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import {
+    CustomRefundStates,
+    RefundState,
+    RefundTransitionData,
+} from '../../service/helpers/refund-state-machine/refund-state';
+
+/**
+ * @description
+ * A RefundProcess is used to define the way the refund process works as in: what states a Refund can be
+ * in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, a
+ * RefundProcess can perform checks before allowing a state transition to occur, and the `onTransitionEnd()`
+ * hook allows logic to be executed after a state change.
+ *
+ * For detailed description of the interface members, see the {@link StateMachineConfig} docs.
+ *
+ * @docsCategory refund
+ */
+export interface RefundProcess<State extends keyof CustomRefundStates | string> extends InjectableStrategy {
+    transitions?: Transitions<State, State | RefundState> & Partial<Transitions<RefundState | State>>;
+    onTransitionStart?: OnTransitionStartFn<State | RefundState, RefundTransitionData>;
+    onTransitionEnd?: OnTransitionEndFn<State | RefundState, RefundTransitionData>;
+    onTransitionError?: OnTransitionErrorFn<State | RefundState>;
+}
+
+/**
+ * @description
+ * Used to define extensions to or modifications of the default refund process.
+ *
+ * For detailed description of the interface members, see the {@link StateMachineConfig} docs.
+ *
+ * @deprecated Use RefundProcess
+ */
+export interface CustomRefundProcess<State extends keyof CustomRefundStates | string>
+    extends RefundProcess<State> {}

+ 9 - 0
packages/core/src/config/vendure-config.ts

@@ -48,6 +48,7 @@ import { PaymentMethodHandler } from './payment/payment-method-handler';
 import { PaymentProcess } from './payment/payment-process';
 import { PaymentProcess } from './payment/payment-process';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionCondition } from './promotion/promotion-condition';
 import { PromotionCondition } from './promotion/promotion-condition';
+import { RefundProcess } from './refund/refund-process';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
@@ -848,6 +849,14 @@ export interface PaymentOptions {
      * @since 2.0.0
      * @since 2.0.0
      */
      */
     process?: Array<PaymentProcess<any>>;
     process?: Array<PaymentProcess<any>>;
+    /**
+     * @description
+     * Allows the definition of custom states and transition logic for the refund process state machine.
+     * Takes an array of objects implementing the {@link RefundProcess} interface.
+     *
+     * @default defaultRefundProcess
+     */
+    refundProcess?: Array<RefundProcess<any>>;
 }
 }
 
 
 /**
 /**

+ 1 - 1
packages/core/src/event-bus/events/order-line-event.ts

@@ -15,7 +15,7 @@ export class OrderLineEvent extends VendureEvent {
         public ctx: RequestContext,
         public ctx: RequestContext,
         public order: Order,
         public order: Order,
         public orderLine: OrderLine,
         public orderLine: OrderLine,
-        public type: 'created' | 'updated' | 'deleted',
+        public type: 'created' | 'updated' | 'deleted' | 'cancelled',
     ) {
     ) {
         super();
         super();
     }
     }

+ 21 - 0
packages/core/src/event-bus/events/refund-event.ts

@@ -0,0 +1,21 @@
+import { RequestContext } from '../../api/common/request-context';
+import { Order, Refund } from '../../entity';
+import { VendureEvent } from '../vendure-event';
+
+/**
+ * @description
+ * This event is fired whenever a {@link Refund} is created
+ *
+ * @docsCategory events
+ * @docsPage Event Types
+ */
+export class RefundEvent extends VendureEvent {
+    constructor(
+        public ctx: RequestContext,
+        public order: Order,
+        public refund: Refund,
+        public type: 'created',
+    ) {
+        super();
+    }
+}

+ 1 - 0
packages/core/src/event-bus/index.ts

@@ -47,6 +47,7 @@ export * from './events/product-variant-channel-event';
 export * from './events/product-variant-event';
 export * from './events/product-variant-event';
 export * from './events/product-variant-price-event';
 export * from './events/product-variant-price-event';
 export * from './events/promotion-event';
 export * from './events/promotion-event';
+export * from './events/refund-event';
 export * from './events/refund-state-transition-event';
 export * from './events/refund-state-transition-event';
 export * from './events/role-change-event';
 export * from './events/role-change-event';
 export * from './events/role-event';
 export * from './events/role-event';

+ 2 - 0
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -341,6 +341,8 @@ export class OrderModifier {
                 await this.connection.getRepository(ctx, OrderLine).update(line.orderLineId, {
                 await this.connection.getRepository(ctx, OrderLine).update(line.orderLineId, {
                     quantity: orderLine.quantity - line.quantity,
                     quantity: orderLine.quantity - line.quantity,
                 });
                 });
+
+                await this.eventBus.publish(new OrderLineEvent(ctx, order, orderLine, 'cancelled'));
             }
             }
         }
         }
 
 

+ 70 - 31
packages/core/src/service/helpers/refund-state-machine/refund-state-machine.ts

@@ -1,46 +1,31 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
-import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
 import { IllegalOperationError } from '../../../common/error/errors';
 import { FSM } from '../../../common/finite-state-machine/finite-state-machine';
 import { FSM } from '../../../common/finite-state-machine/finite-state-machine';
-import { StateMachineConfig } from '../../../common/finite-state-machine/types';
+import { mergeTransitionDefinitions } from '../../../common/finite-state-machine/merge-transition-definitions';
+import { StateMachineConfig, Transitions } from '../../../common/finite-state-machine/types';
+import { validateTransitionDefinition } from '../../../common/finite-state-machine/validate-transition-definition';
+import { awaitPromiseOrObservable } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
+import { Logger } from '../../../config/logger/vendure-logger';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
-import { HistoryService } from '../../services/history.service';
 
 
-import { RefundState, refundStateTransitions, RefundTransitionData } from './refund-state';
+import { RefundState, RefundTransitionData } from './refund-state';
 
 
 @Injectable()
 @Injectable()
 export class RefundStateMachine {
 export class RefundStateMachine {
-    private readonly config: StateMachineConfig<RefundState, RefundTransitionData> = {
-        transitions: refundStateTransitions,
-        onTransitionStart: async (fromState, toState, data) => {
-            return true;
-        },
-        onTransitionEnd: async (fromState, toState, data) => {
-            await this.historyService.createHistoryEntryForOrder({
-                ctx: data.ctx,
-                orderId: data.order.id,
-                type: HistoryEntryType.ORDER_REFUND_TRANSITION,
-                data: {
-                    refundId: data.refund.id,
-                    from: fromState,
-                    to: toState,
-                    reason: data.refund.reason,
-                },
-            });
-        },
-        onError: (fromState, toState, message) => {
-            throw new IllegalOperationError(message || 'error.cannot-transition-refund-from-to', {
-                fromState,
-                toState,
-            });
-        },
-    };
-
-    constructor(private configService: ConfigService, private historyService: HistoryService) {}
+    private readonly config: StateMachineConfig<RefundState, RefundTransitionData>;
+    private readonly initialState: RefundState = 'Pending';
+
+    constructor(private configService: ConfigService) {
+        this.config = this.initConfig();
+    }
+
+    getInitialState(): RefundState {
+        return this.initialState;
+    }
 
 
     getNextStates(refund: Refund): readonly RefundState[] {
     getNextStates(refund: Refund): readonly RefundState[] {
         const fsm = new FSM(this.config, refund.state);
         const fsm = new FSM(this.config, refund.state);
@@ -53,4 +38,58 @@ export class RefundStateMachine {
         refund.state = state;
         refund.state = state;
         return result;
         return result;
     }
     }
+
+    private initConfig(): StateMachineConfig<RefundState, RefundTransitionData> {
+        const processes = [...(this.configService.paymentOptions.refundProcess ?? [])];
+        const allTransitions = processes.reduce(
+            (transitions, process) =>
+                mergeTransitionDefinitions(transitions, process.transitions as Transitions<any>),
+            {} as Transitions<RefundState>,
+        );
+
+        const validationResult = validateTransitionDefinition(allTransitions, this.initialState);
+        if (!validationResult.valid && validationResult.error) {
+            Logger.error(`The refund process has an invalid configuration:`);
+            throw new Error(validationResult.error);
+        }
+        if (validationResult.valid && validationResult.error) {
+            Logger.warn(`Refund process: ${validationResult.error}`);
+        }
+
+        return {
+            transitions: allTransitions,
+            onTransitionStart: async (fromState, toState, data) => {
+                for (const process of processes) {
+                    if (typeof process.onTransitionStart === 'function') {
+                        const result = await awaitPromiseOrObservable(
+                            process.onTransitionStart(fromState, toState, data),
+                        );
+                        if (result === false || typeof result === 'string') {
+                            return result;
+                        }
+                    }
+                }
+            },
+            onTransitionEnd: async (fromState, toState, data) => {
+                for (const process of processes) {
+                    if (typeof process.onTransitionEnd === 'function') {
+                        await awaitPromiseOrObservable(process.onTransitionEnd(fromState, toState, data));
+                    }
+                }
+            },
+            onError: async (fromState, toState, message) => {
+                for (const process of processes) {
+                    if (typeof process.onTransitionError === 'function') {
+                        await awaitPromiseOrObservable(
+                            process.onTransitionError(fromState, toState, message),
+                        );
+                    }
+                }
+                throw new IllegalOperationError(message || 'error.cannot-transition-refund-from-to', {
+                    fromState,
+                    toState,
+                });
+            },
+        };
+    }
 }
 }

+ 18 - 16
packages/core/src/service/helpers/refund-state-machine/refund-state.ts

@@ -1,28 +1,30 @@
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
-import { Transitions } from '../../../common/finite-state-machine/types';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
-import { Payment } from '../../../entity/payment/payment.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
 
 
 /**
 /**
  * @description
  * @description
- * These are the default states of the refund process.
+ * An interface to extend standard {@link RefundState}.
  *
  *
- * @docsCategory payment
+ * @deprecated use RefundStates
+ */
+export interface CustomRefundStates {}
+
+/**
+ * @description
+ * An interface to extend standard {@link RefundState}.
+ *
+ * @docsCategory refund
  */
  */
-export type RefundState = 'Pending' | 'Settled' | 'Failed';
+export interface RefundStates {}
 
 
-export const refundStateTransitions: Transitions<RefundState> = {
-    Pending: {
-        to: ['Settled', 'Failed'],
-    },
-    Settled: {
-        to: [],
-    },
-    Failed: {
-        to: [],
-    },
-};
+/**
+ * @description
+ * These are the default states of the refund process.
+ *
+ * @docsCategory refund
+ */
+export type RefundState = 'Pending' | 'Settled' | 'Failed' | keyof CustomRefundStates | keyof RefundStates;
 
 
 /**
 /**
  * @description
  * @description

+ 41 - 1
packages/core/src/service/services/order.service.ts

@@ -53,6 +53,7 @@ import {
     CancelPaymentError,
     CancelPaymentError,
     EmptyOrderLineSelectionError,
     EmptyOrderLineSelectionError,
     FulfillmentStateTransitionError,
     FulfillmentStateTransitionError,
+    RefundStateTransitionError,
     InsufficientStockOnHandError,
     InsufficientStockOnHandError,
     ItemsAlreadyFulfilledError,
     ItemsAlreadyFulfilledError,
     ManualPaymentStateError,
     ManualPaymentStateError,
@@ -98,6 +99,7 @@ import { CouponCodeEvent } from '../../event-bus/events/coupon-code-event';
 import { OrderEvent } from '../../event-bus/events/order-event';
 import { OrderEvent } from '../../event-bus/events/order-event';
 import { OrderLineEvent } from '../../event-bus/events/order-line-event';
 import { OrderLineEvent } from '../../event-bus/events/order-line-event';
 import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
 import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
+import { RefundEvent } from '../../event-bus/events/refund-event';
 import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
 import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
 import { FulfillmentState } from '../helpers/fulfillment-state-machine/fulfillment-state';
@@ -108,6 +110,7 @@ import { OrderModifier } from '../helpers/order-modifier/order-modifier';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
 import { PaymentState } from '../helpers/payment-state-machine/payment-state';
 import { PaymentState } from '../helpers/payment-state-machine/payment-state';
+import { RefundState } from '../helpers/refund-state-machine/refund-state';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { TranslatorService } from '../helpers/translator/translator.service';
 import { TranslatorService } from '../helpers/translator/translator.service';
@@ -987,6 +990,38 @@ export class OrderService {
         return result.fulfillment;
         return result.fulfillment;
     }
     }
 
 
+    /**
+     * @description
+     * Transitions a Refund to the given state
+     */
+    async transitionRefundToState(
+        ctx: RequestContext,
+        refundId: ID,
+        state: RefundState,
+        transactionId?: string,
+    ): Promise<Refund | RefundStateTransitionError> {
+        const refund = await this.connection.getEntityOrThrow(ctx, Refund, refundId, {
+            relations: ['payment', 'payment.order'],
+        });
+        if (transactionId && refund.transactionId !== transactionId) {
+            refund.transactionId = transactionId;
+        }
+        const fromState = refund.state;
+        const toState = state;
+        const { finalize } = await this.refundStateMachine.transition(
+            ctx,
+            refund.payment.order,
+            refund,
+            toState,
+        );
+        await this.connection.getRepository(ctx, Refund).save(refund);
+        await finalize();
+        await this.eventBus.publish(
+            new RefundStateTransitionEvent(fromState, toState, ctx, refund, refund.payment.order),
+        );
+        return refund;
+    }
+
     /**
     /**
      * @description
      * @description
      * Allows the Order to be modified, which allows several aspects of the Order to be changed:
      * Allows the Order to be modified, which allows several aspects of the Order to be changed:
@@ -1419,7 +1454,12 @@ export class OrderService {
             return new RefundOrderStateError({ orderState: order.state });
             return new RefundOrderStateError({ orderState: order.state });
         }
         }
 
 
-        return await this.paymentService.createRefund(ctx, input, order, payment);
+        const createdRefund = await this.paymentService.createRefund(ctx, input, order, payment);
+
+        if (createdRefund instanceof Refund) {
+            await this.eventBus.publish(new RefundEvent(ctx, order, createdRefund, 'created'));
+        }
+        return createdRefund;
     }
     }
 
 
     /**
     /**

+ 15 - 0
packages/create/src/gather-user-responses.ts

@@ -14,6 +14,7 @@ interface PromptAnswers {
     dbSchema?: string | symbol;
     dbSchema?: string | symbol;
     dbUserName: string | symbol;
     dbUserName: string | symbol;
     dbPassword: string | symbol;
     dbPassword: string | symbol;
+    dbSSL?: boolean | symbol;
     superadminIdentifier: string | symbol;
     superadminIdentifier: string | symbol;
     superadminPassword: string | symbol;
     superadminPassword: string | symbol;
     populateProducts: boolean | symbol;
     populateProducts: boolean | symbol;
@@ -72,6 +73,19 @@ export async function gatherUserResponses(
               })
               })
             : '';
             : '';
     checkCancel(dbSchema);
     checkCancel(dbSchema);
+    const dbSSL =
+        dbType === 'postgres'
+            ? await select({
+                  message:
+                      'Use SSL to connect to the database? (only enable if you database provider supports SSL)',
+                  options: [
+                      { label: 'no', value: false },
+                      { label: 'yes', value: true },
+                  ],
+                  initialValue: false,
+              })
+            : false;
+    checkCancel(dbSSL);
     const dbUserName = hasConnection
     const dbUserName = hasConnection
         ? await text({
         ? await text({
               message: "What's the database user name?",
               message: "What's the database user name?",
@@ -113,6 +127,7 @@ export async function gatherUserResponses(
         dbSchema,
         dbSchema,
         dbUserName,
         dbUserName,
         dbPassword,
         dbPassword,
+        dbSSL,
         superadminIdentifier,
         superadminIdentifier,
         superadminPassword,
         superadminPassword,
         populateProducts,
         populateProducts,

+ 15 - 0
packages/create/src/helpers.ts

@@ -352,6 +352,7 @@ async function checkPostgresDbExists(options: any, root: string): Promise<true>
         port: options.port,
         port: options.port,
         database: options.database,
         database: options.database,
         schema: options.schema,
         schema: options.schema,
+        ssl: options.ssl,
     };
     };
     const client = new Client(connectionOptions);
     const client = new Client(connectionOptions);
 
 
@@ -371,6 +372,8 @@ async function checkPostgresDbExists(options: any, root: string): Promise<true>
             throwDatabaseDoesNotExist(options.database);
             throwDatabaseDoesNotExist(options.database);
         } else if (e.message === 'NO_SCHEMA') {
         } else if (e.message === 'NO_SCHEMA') {
             throwDatabaseSchemaDoesNotExist(options.database, options.schema);
             throwDatabaseSchemaDoesNotExist(options.database, options.schema);
+        } else if (e.code === '28000') {
+            throwSSLConnectionError(e, options.ssl);
         }
         }
         throwConnectionError(e);
         throwConnectionError(e);
         await client.end();
         await client.end();
@@ -389,6 +392,18 @@ function throwConnectionError(err: any) {
     );
     );
 }
 }
 
 
+function throwSSLConnectionError(err: any, sslEnabled?: any) {
+    throw new Error(
+        'Could not connect to the database. ' +
+            (sslEnabled === undefined
+                ? 'Is your server requiring an SSL connection?'
+                : 'Are you sure your server supports SSL?') +
+            `Please check the connection settings in your Vendure config.\n[${
+                (err.message || err.toString()) as string
+            }]`,
+    );
+}
+
 function throwDatabaseDoesNotExist(name: string) {
 function throwDatabaseDoesNotExist(name: string) {
     throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
     throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
 }
 }

+ 3 - 0
packages/create/templates/vendure-config.hbs

@@ -52,6 +52,9 @@ export const config: VendureConfig = {
         {{#if dbSchema}}
         {{#if dbSchema}}
         schema: process.env.DB_SCHEMA,
         schema: process.env.DB_SCHEMA,
         {{/if}}
         {{/if}}
+        {{#if dbSSL}}
+        ssl: true,
+        {{/if}}
         {{#if isSQLjs}}
         {{#if isSQLjs}}
         location: path.join(__dirname, 'vendure.sqlite'),
         location: path.join(__dirname, 'vendure.sqlite'),
         autoSave: true,
         autoSave: true,

+ 31 - 1
packages/dev-server/example-plugins/ui-extensions-library/ui/providers.ts

@@ -1,4 +1,5 @@
-import { addNavMenuSection } from '@vendure/admin-ui/core';
+import { PageLocationId, addNavMenuSection, registerPageTab } from '@vendure/admin-ui/core';
+import { AngularUiComponent } from './angular-components/angular-ui/angular-ui.component';
 
 
 export default [
 export default [
     addNavMenuSection({
     addNavMenuSection({
@@ -17,4 +18,33 @@ export default [
             },
             },
         ],
         ],
     }),
     }),
+    //Testing page tabs on custom angular components
+    registerPageTab({
+        location: 'angular-ui' as PageLocationId,
+        tab: 'Example Tab 1',
+        route: '/extensions/ui-library/angular-ui',
+        tabIcon: 'star',
+        component: AngularUiComponent,
+    }),
+    registerPageTab({
+        location: 'angular-ui' as PageLocationId,
+        tab: 'Example Tab 2',
+        route: '/extensions/ui-library/angular-ui2',
+        tabIcon: 'star',
+        component: AngularUiComponent,
+    }),
+    registerPageTab({
+        location: 'react-ui' as PageLocationId,
+        tab: 'Example Tab 1',
+        route: '/extensions/ui-library/angular-ui',
+        tabIcon: 'star',
+        component: AngularUiComponent,
+    }),
+    registerPageTab({
+        location: 'react-ui' as PageLocationId,
+        tab: 'Example Tab 2',
+        route: '/extensions/ui-library/angular-ui2',
+        tabIcon: 'star',
+        component: AngularUiComponent,
+    }),
 ];
 ];

+ 4 - 0
packages/dev-server/example-plugins/ui-extensions-library/ui/routes.ts

@@ -9,11 +9,15 @@ export default [
         path: 'react-ui',
         path: 'react-ui',
         component: ReactUi,
         component: ReactUi,
         title: 'React UI',
         title: 'React UI',
+        description: "Test description",
+        locationId: "react-ui"
     }),
     }),
     registerRouteComponent({
     registerRouteComponent({
         path: 'angular-ui',
         path: 'angular-ui',
         component: AngularUiComponent,
         component: AngularUiComponent,
         title: 'Angular UI',
         title: 'Angular UI',
+        locationId: "angular-ui",
+        description: "Test description 2",
         routeConfig: {
         routeConfig: {
             canDeactivate: [CanDeactivateDetailGuard],
             canDeactivate: [CanDeactivateDetailGuard],
         },
         },

+ 13 - 3
packages/email-plugin/src/handler/event-handler.ts

@@ -14,6 +14,7 @@ import {
     LoadDataFn,
     LoadDataFn,
     SetAttachmentsFn,
     SetAttachmentsFn,
     SetOptionalAddressFieldsFn,
     SetOptionalAddressFieldsFn,
+    SetSubjectFn,
     SetTemplateVarsFn,
     SetTemplateVarsFn,
 } from '../types';
 } from '../types';
 
 
@@ -135,6 +136,7 @@ import {
 export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
 export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
     private setRecipientFn: (event: Event) => string;
     private setRecipientFn: (event: Event) => string;
     private setLanguageCodeFn: (event: Event) => LanguageCode | undefined;
     private setLanguageCodeFn: (event: Event) => LanguageCode | undefined;
+    private setSubjectFn?: SetSubjectFn<Event>;
     private setTemplateVarsFn: SetTemplateVarsFn<Event>;
     private setTemplateVarsFn: SetTemplateVarsFn<Event>;
     private setAttachmentsFn?: SetAttachmentsFn<Event>;
     private setAttachmentsFn?: SetAttachmentsFn<Event>;
     private setOptionalAddressFieldsFn?: SetOptionalAddressFieldsFn<Event>;
     private setOptionalAddressFieldsFn?: SetOptionalAddressFieldsFn<Event>;
@@ -214,8 +216,12 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
      * Sets the default subject of the email. The subject string may use Handlebars variables defined by the
      * Sets the default subject of the email. The subject string may use Handlebars variables defined by the
      * setTemplateVars() method.
      * setTemplateVars() method.
      */
      */
-    setSubject(defaultSubject: string): EmailEventHandler<T, Event> {
-        this.defaultSubject = defaultSubject;
+    setSubject(defaultSubject: string | SetSubjectFn<Event>): EmailEventHandler<T, Event> {
+        if (typeof defaultSubject === 'string') {
+            this.defaultSubject = defaultSubject;
+        } else {
+            this.setSubjectFn = defaultSubject;
+        }
         return this;
         return this;
     }
     }
 
 
@@ -370,7 +376,11 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         const { ctx } = event;
         const { ctx } = event;
         const languageCode = this.setLanguageCodeFn?.(event) || ctx.languageCode;
         const languageCode = this.setLanguageCodeFn?.(event) || ctx.languageCode;
         const configuration = this.getBestConfiguration(ctx.channel.code, languageCode);
         const configuration = this.getBestConfiguration(ctx.channel.code, languageCode);
-        const subject = configuration ? configuration.subject : this.defaultSubject;
+        const subject = configuration
+            ? configuration.subject
+            : this.setSubjectFn
+              ? await this.setSubjectFn(event, ctx, injector)
+              : this.defaultSubject;
         if (subject == null) {
         if (subject == null) {
             throw new Error(
             throw new Error(
                 `No subject field has been defined. ` +
                 `No subject field has been defined. ` +

+ 51 - 2
packages/email-plugin/src/plugin.spec.ts

@@ -29,7 +29,9 @@ import { EmailSender } from './sender/email-sender';
 import { EmailEventHandler } from './handler/event-handler';
 import { EmailEventHandler } from './handler/event-handler';
 import { EmailEventListener } from './event-listener';
 import { EmailEventListener } from './event-listener';
 import { EmailPlugin } from './plugin';
 import { EmailPlugin } from './plugin';
-import { EmailDetails, EmailPluginOptions, EmailTransportOptions } from './types';
+import { EmailDetails, EmailPluginOptions, EmailTransportOptions, LoadTemplateInput } from './types';
+import { TemplateLoader } from './template-loader/template-loader';
+import fs from 'fs-extra';
 
 
 describe('EmailPlugin', () => {
 describe('EmailPlugin', () => {
     let eventBus: EventBus;
     let eventBus: EventBus;
@@ -945,6 +947,53 @@ describe('EmailPlugin', () => {
             expect(transport.type).toBe('testing');
             expect(transport.type).toBe('testing');
         });
         });
     });
     });
+
+    describe('Dynamic subject handling', () => {
+        it('With string', async () => {
+            const ctx = RequestContext.deserialize({
+                _channel: { code: DEFAULT_CHANNEL_CODE },
+                _languageCode: LanguageCode.en,
+            } as any);
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello')
+                .setTemplateVars(event => ({ subjectVar: 'foo' }));
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend.mock.calls[0][0].subject).toBe('Hello');
+            expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
+            expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
+        });
+        it('With callback function', async () => {
+            const ctx = RequestContext.deserialize({
+                _channel: { code: DEFAULT_CHANNEL_CODE },
+                _languageCode: LanguageCode.en,
+            } as any);
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject(async (_e, _ctx, _i) => {
+                    const service = _i.get(MockService)
+                    const mockData = await service.someAsyncMethod()
+                    return `Hello from ${mockData} and {{ subjectVar }}`;
+                })
+                .setTemplateVars(event => ({ subjectVar: 'foo' }));
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend.mock.calls[0][0].subject).toBe('Hello from loaded data and foo');
+            expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
+            expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
+        });
+    })
 });
 });
 
 
 class FakeCustomSender implements EmailSender {
 class FakeCustomSender implements EmailSender {
@@ -966,4 +1015,4 @@ class MockService {
     someAsyncMethod() {
     someAsyncMethod() {
         return Promise.resolve('loaded data');
         return Promise.resolve('loaded data');
     }
     }
-}
+}

+ 8 - 0
packages/email-plugin/src/types.ts

@@ -425,6 +425,14 @@ export type SetTemplateVarsFn<Event> = (
  */
  */
 export type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<EmailAttachment[]>;
 export type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<EmailAttachment[]>;
 
 
+/**
+ * @description
+ * A function used to define the subject to be sent with the email.
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email Plugin Types
+ */
+export type SetSubjectFn<Event> = (event: Event, ctx: RequestContext, injector: Injector) => string | Promise<string>;
+
 /**
 /**
  * @description
  * @description
  * Optional address-related fields for sending the email.
  * Optional address-related fields for sending the email.

+ 1 - 1
packages/testing/package.json

@@ -40,7 +40,7 @@
         "@vendure/common": "^2.2.7",
         "@vendure/common": "^2.2.7",
         "faker": "^4.1.0",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "form-data": "^4.0.0",
-        "graphql": "16.8.1",
+        "graphql": "~16.9.0",
         "graphql-tag": "^2.12.6",
         "graphql-tag": "^2.12.6",
         "node-fetch": "^2.7.0",
         "node-fetch": "^2.7.0",
         "sql.js": "1.8.0"
         "sql.js": "1.8.0"

+ 16 - 12
packages/ui-devkit/src/compiler/compile.ts

@@ -11,19 +11,20 @@ import { copyGlobalStyleFile, setBaseHref, setupScaffold } from './scaffold';
 import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
 import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
 import {
 import {
     StaticAssetDefinition,
     StaticAssetDefinition,
+    UiExtensionBuildCommand,
     UiExtensionCompilerOptions,
     UiExtensionCompilerOptions,
     UiExtensionCompilerProcessArgument,
     UiExtensionCompilerProcessArgument,
 } from './types';
 } from './types';
 import {
 import {
     copyStaticAsset,
     copyStaticAsset,
     copyUiDevkit,
     copyUiDevkit,
+    determinePackageManager,
     getStaticAssetPath,
     getStaticAssetPath,
     isAdminUiExtension,
     isAdminUiExtension,
     isGlobalStylesExtension,
     isGlobalStylesExtension,
     isStaticAssetExtension,
     isStaticAssetExtension,
     isTranslationExtension,
     isTranslationExtension,
     normalizeExtensions,
     normalizeExtensions,
-    shouldUseYarn,
 } from './utils';
 } from './utils';
 
 
 /**
 /**
@@ -35,18 +36,21 @@ import {
 export function compileUiExtensions(
 export function compileUiExtensions(
     options: UiExtensionCompilerOptions,
     options: UiExtensionCompilerOptions,
 ): AdminUiAppConfig | AdminUiAppDevModeConfig {
 ): AdminUiAppConfig | AdminUiAppDevModeConfig {
-    const { devMode, watchPort, command } = options;
-    const usingYarn = command && command === 'npm' ? false : shouldUseYarn();
+    const { devMode, watchPort } = options;
+    const command: UiExtensionBuildCommand =
+        options.command && ['npm', 'pnpm'].includes(options.command)
+            ? options.command
+            : determinePackageManager();
     if (devMode) {
     if (devMode) {
         return runWatchMode({
         return runWatchMode({
             watchPort: watchPort || 4200,
             watchPort: watchPort || 4200,
-            usingYarn,
             ...options,
             ...options,
+            command,
         });
         });
     } else {
     } else {
         return runCompileMode({
         return runCompileMode({
-            usingYarn,
             ...options,
             ...options,
+            command,
         });
         });
     }
     }
 }
 }
@@ -55,10 +59,10 @@ function runCompileMode({
     outputPath,
     outputPath,
     baseHref,
     baseHref,
     extensions,
     extensions,
-    usingYarn,
+    command,
     additionalProcessArguments,
     additionalProcessArguments,
     ngCompilerPath,
     ngCompilerPath,
-}: UiExtensionCompilerOptions & { usingYarn: boolean }): AdminUiAppConfig {
+}: UiExtensionCompilerOptions & { command: UiExtensionBuildCommand }): AdminUiAppConfig {
     const distPath = path.join(outputPath, 'dist');
     const distPath = path.join(outputPath, 'dist');
 
 
     const compile = () =>
     const compile = () =>
@@ -66,13 +70,13 @@ function runCompileMode({
             await setupScaffold(outputPath, extensions);
             await setupScaffold(outputPath, extensions);
             await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
             await setBaseHref(outputPath, baseHref || DEFAULT_BASE_HREF);
 
 
-            let cmd = usingYarn ? 'yarn' : 'npm';
+            let cmd: UiExtensionBuildCommand | 'node' = command;
             let commandArgs = ['run', 'build'];
             let commandArgs = ['run', 'build'];
             if (ngCompilerPath) {
             if (ngCompilerPath) {
                 cmd = 'node';
                 cmd = 'node';
                 commandArgs = [ngCompilerPath, 'build', '--configuration production'];
                 commandArgs = [ngCompilerPath, 'build', '--configuration production'];
             } else {
             } else {
-                if (!usingYarn) {
+                if (cmd === 'npm') {
                     // npm requires `--` before any command line args being passed to a script
                     // npm requires `--` before any command line args being passed to a script
                     commandArgs.splice(2, 0, '--');
                     commandArgs.splice(2, 0, '--');
                 }
                 }
@@ -109,10 +113,10 @@ function runWatchMode({
     baseHref,
     baseHref,
     watchPort,
     watchPort,
     extensions,
     extensions,
-    usingYarn,
+    command,
     additionalProcessArguments,
     additionalProcessArguments,
     ngCompilerPath,
     ngCompilerPath,
-}: UiExtensionCompilerOptions & { usingYarn: boolean }): AdminUiAppDevModeConfig {
+}: UiExtensionCompilerOptions & { command: UiExtensionBuildCommand }): AdminUiAppDevModeConfig {
     const devkitPath = require.resolve('@vendure/ui-devkit');
     const devkitPath = require.resolve('@vendure/ui-devkit');
     let buildProcess: ChildProcess;
     let buildProcess: ChildProcess;
     let watcher: FSWatcher | undefined;
     let watcher: FSWatcher | undefined;
@@ -128,7 +132,7 @@ function runWatchMode({
             const globalStylesExtensions = extensions.filter(isGlobalStylesExtension);
             const globalStylesExtensions = extensions.filter(isGlobalStylesExtension);
             const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
             const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
             const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
             const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
-            let cmd = usingYarn ? 'yarn' : 'npm';
+            let cmd: UiExtensionBuildCommand | 'node' = command;
             let commandArgs = ['run', 'start'];
             let commandArgs = ['run', 'start'];
             if (ngCompilerPath) {
             if (ngCompilerPath) {
                 cmd = 'node';
                 cmd = 'node';

+ 10 - 2
packages/ui-devkit/src/compiler/types.ts

@@ -347,6 +347,14 @@ export interface AdminUiExtensionLazyModule {
  */
  */
 export type UiExtensionCompilerProcessArgument = string | [string, any];
 export type UiExtensionCompilerProcessArgument = string | [string, any];
 
 
+/**
+ * @description
+ * The package manager to use when invoking the Angular CLI to build UI extensions.
+ *
+ * @docsCategory UiDevkit
+ */
+export type UiExtensionBuildCommand = 'npm' | 'yarn' | 'pnpm';
+
 /**
 /**
  * @description
  * @description
  * Options to configure how the Admin UI should be compiled.
  * Options to configure how the Admin UI should be compiled.
@@ -435,11 +443,11 @@ export interface UiExtensionCompilerOptions {
      * @description
      * @description
      * Internally, the Angular CLI will be invoked as an npm script. By default, the compiler will use Yarn
      * Internally, the Angular CLI will be invoked as an npm script. By default, the compiler will use Yarn
      * to run the script if it is detected, otherwise it will use npm. This setting allows you to explicitly
      * to run the script if it is detected, otherwise it will use npm. This setting allows you to explicitly
-     * set which command to use, rather than relying on the default behavior.
+     * set which command to use, including pnpm, rather than relying on the default behavior.
      *
      *
      * @since 1.5.0
      * @since 1.5.0
      */
      */
-    command?: 'yarn' | 'npm';
+    command?: UiExtensionBuildCommand;
 
 
     /**
     /**
      * @description
      * @description

+ 4 - 4
packages/ui-devkit/src/compiler/utils.ts

@@ -23,14 +23,14 @@ export const logger = {
 };
 };
 
 
 /**
 /**
- * Checks for the global yarn binary and returns true if found.
+ * Checks for the global yarn binary to determine whether to use yarn or npm.
  */
  */
-export function shouldUseYarn(): boolean {
+export function determinePackageManager(): 'yarn' | 'npm' {
     try {
     try {
         execSync('yarnpkg --version', { stdio: 'ignore' });
         execSync('yarnpkg --version', { stdio: 'ignore' });
-        return true;
+        return 'yarn';
     } catch (e: any) {
     } catch (e: any) {
-        return false;
+        return 'npm';
     }
     }
 }
 }
 
 

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác