Browse Source

Merge branch 'master' into minor

Michael Bromley 6 months ago
parent
commit
22cb1ba3ef
76 changed files with 1668 additions and 497 deletions
  1. 10 0
      CHANGELOG.md
  2. 67 0
      docs/docs/guides/developer-guide/cache/index.mdx
  3. 1 1
      docs/docs/reference/core-plugins/email-plugin/email-event-handler-with-async-data.md
  4. 98 89
      docs/docs/reference/core-plugins/email-plugin/email-event-handler.md
  5. 18 12
      docs/docs/reference/core-plugins/email-plugin/template-loader.md
  6. 41 0
      docs/docs/reference/dashboard/components/data-table-bulk-actions.md
  7. 2 2
      docs/docs/reference/dashboard/components/detail-page.md
  8. 10 4
      docs/docs/reference/dashboard/components/list-page.md
  9. 12 9
      docs/docs/reference/dashboard/components/page-action-bar.md
  10. 11 11
      docs/docs/reference/dashboard/components/page-block.md
  11. 4 4
      docs/docs/reference/dashboard/components/page-layout.md
  12. 3 3
      docs/docs/reference/dashboard/components/page-title.md
  13. 3 3
      docs/docs/reference/dashboard/components/page.md
  14. 21 1
      docs/docs/reference/dashboard/extensions/dashboard-action-bar-item.md
  15. 80 0
      docs/docs/reference/dashboard/extensions/dashboard-alert-definition.md
  16. 28 0
      docs/docs/reference/dashboard/extensions/dashboard-base-widget-props.md
  17. 1 1
      docs/docs/reference/dashboard/extensions/dashboard-custom-form-component.md
  18. 35 0
      docs/docs/reference/dashboard/extensions/dashboard-custom-form-components.md
  19. 41 0
      docs/docs/reference/dashboard/extensions/dashboard-data-table-display-component.md
  20. 61 0
      docs/docs/reference/dashboard/extensions/dashboard-data-table-extension-definition.md
  21. 47 0
      docs/docs/reference/dashboard/extensions/dashboard-detail-form-display-component.md
  22. 47 0
      docs/docs/reference/dashboard/extensions/dashboard-detail-form-input-component.md
  23. 20 7
      docs/docs/reference/dashboard/extensions/dashboard-extension.md
  24. 1 1
      docs/docs/reference/dashboard/extensions/dashboard-page-block-definition.md
  25. 65 0
      docs/docs/reference/dashboard/extensions/dashboard-widget-definition.md
  26. 58 0
      docs/docs/reference/dashboard/extensions/dashboard-widget-instance.md
  27. 1 1
      docs/docs/reference/dashboard/extensions/define-dashboard-extension.md
  28. 1 1
      docs/docs/reference/dashboard/extensions/page-block-location.md
  29. 2 2
      docs/docs/reference/dashboard/hooks/use-auth.md
  30. 14 6
      docs/docs/reference/dashboard/hooks/use-detail-page.md
  31. 1 1
      docs/docs/reference/dashboard/hooks/use-permissions.md
  32. 1 1
      docs/docs/reference/typescript-api/configuration/merge-config.md
  33. 1 1
      lerna.json
  34. 65 65
      package-lock.json
  35. 4 4
      packages/admin-ui-plugin/package.json
  36. 2 2
      packages/admin-ui/package.json
  37. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  38. 3 3
      packages/asset-server-plugin/package.json
  39. 3 3
      packages/cli/package.json
  40. 1 1
      packages/common/package.json
  41. 278 0
      packages/core/e2e/custom-field-default-values.e2e-spec.ts
  42. 1 1
      packages/core/e2e/list-query-builder.e2e-spec.ts
  43. 2 2
      packages/core/package.json
  44. 3 2
      packages/core/src/api/api.module.ts
  45. 22 1
      packages/core/src/api/common/graphql-value-transformer.ts
  46. 288 0
      packages/core/src/api/middleware/custom-field-processing-interceptor.ts
  47. 0 172
      packages/core/src/api/middleware/validate-custom-fields-interceptor.ts
  48. 51 2
      packages/core/src/common/self-refreshing-cache.ts
  49. 1 2
      packages/core/src/service/helpers/translatable-saver/translatable-saver.ts
  50. 4 3
      packages/core/src/service/services/order.service.ts
  51. 3 3
      packages/create/package.json
  52. 3 3
      packages/dashboard/package.json
  53. 0 1
      packages/dashboard/vite/tests/fixtures-path-alias/aliased-plugin/index.ts
  54. 1 0
      packages/dashboard/vite/tests/fixtures-path-alias/js-aliased/index.ts
  55. 1 1
      packages/dashboard/vite/tests/fixtures-path-alias/js-aliased/src/js-aliased.plugin.ts
  56. 1 0
      packages/dashboard/vite/tests/fixtures-path-alias/star-aliased/index.ts
  57. 8 0
      packages/dashboard/vite/tests/fixtures-path-alias/star-aliased/src/star-aliased.plugin.ts
  58. 1 0
      packages/dashboard/vite/tests/fixtures-path-alias/ts-aliased/index.ts
  59. 8 0
      packages/dashboard/vite/tests/fixtures-path-alias/ts-aliased/src/ts-aliased.plugin.ts
  60. 4 3
      packages/dashboard/vite/tests/fixtures-path-alias/vendure-config.ts
  61. 35 7
      packages/dashboard/vite/tests/path-alias.spec.ts
  62. 19 9
      packages/dashboard/vite/tests/tsconfig.json
  63. 4 1
      packages/dashboard/vite/utils/plugin-discovery.ts
  64. 9 9
      packages/dev-server/package.json
  65. 3 3
      packages/elasticsearch-plugin/package.json
  66. 3 3
      packages/email-plugin/package.json
  67. 3 3
      packages/graphiql-plugin/package.json
  68. 3 3
      packages/harden-plugin/package.json
  69. 3 3
      packages/job-queue-plugin/package.json
  70. 4 4
      packages/payments-plugin/package.json
  71. 3 3
      packages/sentry-plugin/package.json
  72. 3 3
      packages/stellate-plugin/package.json
  73. 3 3
      packages/telemetry-plugin/package.json
  74. 3 3
      packages/testing/package.json
  75. 4 4
      packages/ui-devkit/package.json
  76. 1 0
      scripts/codegen/generate-graphql-types.ts

+ 10 - 0
CHANGELOG.md

@@ -1,3 +1,13 @@
+## <small>3.3.7 (2025-07-18)</small>
+
+
+#### Fixes
+
+* **core** Apply custom field defaults on entity creation (#3674) ([65804ba](https://github.com/vendure-ecommerce/vendure/commit/65804ba)), closes [#3674](https://github.com/vendure-ecommerce/vendure/issues/3674)
+* **core** Correct unfulfilled quantity calc for multiple fulfillment lines per orderline (#3647) ([430dcb8](https://github.com/vendure-ecommerce/vendure/commit/430dcb8)), closes [#3647](https://github.com/vendure-ecommerce/vendure/issues/3647)
+* **core** ID decoding in nested filter structures (#3677) ([b2c3bc9](https://github.com/vendure-ecommerce/vendure/commit/b2c3bc9)), closes [#3677](https://github.com/vendure-ecommerce/vendure/issues/3677)
+* **dashboard** Fix handling of more types of path alias (#3678) ([c9f993f](https://github.com/vendure-ecommerce/vendure/commit/c9f993f)), closes [#3678](https://github.com/vendure-ecommerce/vendure/issues/3678)
+
 ## <small>3.3.6 (2025-07-14)</small>
 ## <small>3.3.6 (2025-07-14)</small>
 
 
 
 

+ 67 - 0
docs/docs/guides/developer-guide/cache/index.mdx

@@ -258,3 +258,70 @@ which internally just uses whatever the current `CacheStrategy` is to store the
 This means that in most cases you don't need to worry about the session cache, but if you have specific
 This means that in most cases you don't need to worry about the session cache, but if you have specific
 requirements, you can create a custom session cache strategy and set it via the `authOptions.sessionCacheStrategy`
 requirements, you can create a custom session cache strategy and set it via the `authOptions.sessionCacheStrategy`
 config property.
 config property.
+
+## SelfRefreshingCache
+The [SelfRefreshingCache](/reference/typescript-api/cache/self-refreshing-cache) is a specialized in-memory cache which automatically
+refreshes itself if the value is found to be stale. This is useful to cache a single frequently-accessed value, that don't change often.
+
+It is created using the [createSelfRefreshingCache](/reference/typescript-api/cache/self-refreshing-cache#createselfrefreshingcache) function, which takes a configuration object that specifies the
+name of the cache, the time-to-live (TTL) for the cache entries, and a refresh function that will be called to update the value when it is stale.
+
+```ts title="SelfRefreshingCache Example"
+import {
+  Channel,
+  createSelfRefreshingCache,
+  EventBus,
+  InitializerEvent,
+  InternalServerError,
+  Logger,
+  RequestContext
+  SelfRefreshingCache,
+  TransactionalConnection,
+} from '@vendure/core';
+
+@Injectable()
+export class PublicChannelService {
+// highlight-start
+  private publicChannel: SelfRefreshingCache<Channel, [RequestContext]>;
+// highlight-end
+  private readonly logCtx = 'PublicChannelService';
+
+  constructor(
+    private connection: TransactionalConnection,
+    private eventBus: EventBus,
+  ) {
+    this.eventBus.ofType(InitializerEvent).subscribe(async () => {
+      // highlight-start
+      this.publicChannel = await createSelfRefreshingCache({
+        name: 'PublicChannelService.publicChannel',
+        ttl: 1000 * 60 * 5, // 5min
+        refresh: { fn: ctx => this.findPublicChannel(ctx), defaultArgs: [RequestContext.empty()] },
+      });
+      // highlight-end
+    });
+  }
+
+  async getPublicChannel(): Promise<Channel> {
+    // highlight-start
+    const publicChannel = await this.publicChannel.value();
+    // highlight-end
+    if (!publicChannel) {
+      throw new InternalServerError(`error.public-channel-not-found`);
+    }
+    return publicChannel;
+  }
+
+  private async findPublicChannel(ctx: RequestContext): Promise<Channel> {
+    const publicChannel = await this.connection.getRepository(ctx, Channel).findOne({
+      where: { code: DEFAULT_PUBLIC_CHANNEL_CODE },
+      relations: ['defaultShippingZone', 'defaultTaxZone'],
+    });
+
+    if (!publicChannel) {
+      Logger.error('Could not find public channel!', this.logCtx);
+      throw new InternalServerError(`error.public-channel-not-found`);
+    }
+    return publicChannel;
+  }
+}
+```

+ 1 - 1
docs/docs/reference/core-plugins/email-plugin/email-event-handler-with-async-data.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## EmailEventHandlerWithAsyncData
 ## EmailEventHandlerWithAsyncData
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="492" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="506" packageName="@vendure/email-plugin" />
 
 
 Identical to the <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a> but with a `data` property added to the `event` based on the result
 Identical to the <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a> but with a `data` property added to the `event` based on the result
 of the `.loadData()` function.
 of the `.loadData()` function.

+ 98 - 89
docs/docs/reference/core-plugins/email-plugin/email-event-handler.md

@@ -1,34 +1,33 @@
 ---
 ---
-title: 'EmailEventHandler'
+title: "EmailEventHandler"
 isDefaultIndex: false
 isDefaultIndex: false
 generated: true
 generated: true
 ---
 ---
-
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
 <!-- 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 MemberInfo from '@site/src/components/MemberInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 import MemberDescription from '@site/src/components/MemberDescription';
 
 
+
 ## EmailEventHandler
 ## EmailEventHandler
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="136" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="150" packageName="@vendure/email-plugin" />
 
 
 The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 
 
 A handler is created by creating a new <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a> and calling the `.on()` method
 A handler is created by creating a new <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a> and calling the `.on()` method
 to specify which event to respond to.
 to specify which event to respond to.
 
 
-_Example_
+*Example*
 
 
 ```ts
 ```ts
 const confirmationHandler = new EmailEventListener('order-confirmation')
 const confirmationHandler = new EmailEventListener('order-confirmation')
-    .on(OrderStateTransitionEvent)
-    .filter(event => event.toState === 'PaymentSettled')
-    .setRecipient(event => event.order.customer.emailAddress)
-    .setFrom('{{ fromAddress }}')
-    .setSubject(`Order confirmation for #{{ order.code }}`)
-    .setTemplateVars(event => ({ order: event.order }));
+  .on(OrderStateTransitionEvent)
+  .filter(event => event.toState === 'PaymentSettled')
+  .setRecipient(event => event.order.customer.emailAddress)
+  .setFrom('{{ fromAddress }}')
+  .setSubject(`Order confirmation for #{{ order.code }}`)
+  .setTemplateVars(event => ({ order: event.order }));
 ```
 ```
 
 
 This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
 This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
@@ -40,16 +39,16 @@ also to locate the directory of the email template files. So in the example abov
 
 
 ## Handling other languages
 ## Handling other languages
 
 
-By default, a handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
+By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
 and body template.
 and body template.
 
 
-Since v2.0 the `.addTemplate()` method has been **deprecated**. To serve different templates—for example, based on the current
-`languageCode`—implement a custom <a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a> and pass it to `EmailPlugin.init({ templateLoader: new MyTemplateLoader() })`.
+Since v2.0 the `.addTemplate()` method has been **deprecated**. To serve different templates  for example, based on the current
+`languageCode`  implement a custom <a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a> and pass it to `EmailPlugin.init({ templateLoader: new MyTemplateLoader() })`.
 
 
 The language is typically determined by the `languageCode` property of the event's `ctx` (<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) object, so the
 The language is typically determined by the `languageCode` property of the event's `ctx` (<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) object, so the
 `loadTemplate()` method can use that to locate the correct template file.
 `loadTemplate()` method can use that to locate the correct template file.
 
 
-_Example_
+*Example*
 
 
 ```ts
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -57,18 +56,18 @@ import { readFileSync } from 'fs';
 import path from 'path';
 import path from 'path';
 
 
 class CustomLanguageAwareTemplateLoader implements TemplateLoader {
 class CustomLanguageAwareTemplateLoader implements TemplateLoader {
-    constructor(private templateDir: string) {}
+  constructor(private templateDir: string) {}
 
 
-    async loadTemplate(_injector, ctx, { type, templateName }) {
-        // e.g. returns the content of "body.de.hbs" or "body.en.hbs" depending on ctx.languageCode
-        const filePath = path.join(this.templateDir, type, `${templateName}.${ctx.languageCode}.hbs`);
-        return readFileSync(filePath, 'utf-8');
-    }
+  async loadTemplate(_injector, ctx, { type, templateName }) {
+    // e.g. returns the content of "body.de.hbs" or "body.en.hbs" depending on ctx.languageCode
+    const filePath = path.join(this.templateDir, type, `${templateName}.${ctx.languageCode}.hbs`);
+    return readFileSync(filePath, 'utf-8');
+  }
 }
 }
 
 
 EmailPlugin.init({
 EmailPlugin.init({
-    templateLoader: new CustomLanguageAwareTemplateLoader(path.join(__dirname, '../static/email/templates')),
-    handlers: defaultEmailHandlers,
+  templateLoader: new CustomLanguageAwareTemplateLoader(path.join(__dirname, '../static/email/templates')),
+  handlers: defaultEmailHandlers,
 });
 });
 ```
 ```
 
 
@@ -111,7 +110,7 @@ The template would look something like this:
             of the quote you recently requested:
             of the quote you recently requested:
         </mj-text>
         </mj-text>
 
 
-        <--! your custom email layout goes here -->
+        <!-- your custom email layout goes here -->
     </mj-column>
     </mj-column>
 </mj-section>
 </mj-section>
 
 
@@ -130,75 +129,87 @@ import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 
 
 const config: VendureConfig = {
 const config: VendureConfig = {
-    // Add an instance of the plugin to the plugins array
-    plugins: [
-        EmailPlugin.init({
-            handler: [...defaultEmailHandlers, quoteRequestedHandler],
-            // ... etc
-        }),
-    ],
+  // Add an instance of the plugin to the plugins array
+  plugins: [
+    EmailPlugin.init({
+      handler: [...defaultEmailHandlers, quoteRequestedHandler],
+      // ... etc
+    }),
+  ],
 };
 };
 ```
 ```
 
 
+```ts title="Signature"
+class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
+    constructor(listener: EmailEventListener<T>, event: Type<Event>)
+    filter(filterFn: (event: Event) => boolean) => EmailEventHandler<T, Event>;
+    setRecipient(setRecipientFn: (event: Event) => string) => EmailEventHandler<T, Event>;
+    setLanguageCode(setLanguageCodeFn: (event: Event) => LanguageCode | undefined) => EmailEventHandler<T, Event>;
+    setTemplateVars(templateVarsFn: SetTemplateVarsFn<Event>) => EmailEventHandler<T, Event>;
+    setSubject(defaultSubject: string | SetSubjectFn<Event>) => EmailEventHandler<T, Event>;
+    setFrom(from: string) => EmailEventHandler<T, Event>;
+    setOptionalAddressFields(optionalAddressFieldsFn: SetOptionalAddressFieldsFn<Event>) => ;
+    setMetadata(optionalSetMetadataFn: SetMetadataFn<Event>) => ;
+    setAttachments(setAttachmentsFn: SetAttachmentsFn<Event>) => ;
+    addTemplate(config: EmailTemplateConfig) => EmailEventHandler<T, Event>;
+    loadData(loadDataFn: LoadDataFn<Event, R>) => EmailEventHandlerWithAsyncData<R, T, Event, EventWithAsyncData<Event, R>>;
+    setMockEvent(event: Omit<Event, 'ctx' | 'data'>) => EmailEventHandler<T, Event>;
+}
+```
+
 <div className="members-wrapper">
 <div className="members-wrapper">
 
 
 ### constructor
 ### constructor
 
 
-<MemberInfo kind="method" type={`(listener: <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a>&#60;T&#62;, event: Type&#60;Event&#62;) => EmailEventHandler`} />
+<MemberInfo kind="method" type={`(listener: <a href='/reference/core-plugins/email-plugin/email-event-listener#emaileventlistener'>EmailEventListener</a>&#60;T&#62;, event: Type&#60;Event&#62;) => EmailEventHandler`}   />
+
 
 
 ### filter
 ### filter
 
 
-<MemberInfo kind="method" type={`(filterFn: (event: Event) =&#62; boolean) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(filterFn: (event: Event) =&#62; boolean) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 Defines a predicate function which is used to determine whether the event will trigger an email.
 Defines a predicate function which is used to determine whether the event will trigger an email.
 Multiple filter functions may be defined.
 Multiple filter functions may be defined.
-
 ### setRecipient
 ### setRecipient
 
 
-<MemberInfo kind="method" type={`(setRecipientFn: (event: Event) =&#62; string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(setRecipientFn: (event: Event) =&#62; string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 A function which defines how the recipient email address should be extracted from the incoming event.
 A function which defines how the recipient email address should be extracted from the incoming event.
 
 
 The recipient can be a plain email address: `'foobar@example.com'`
 The recipient can be a plain email address: `'foobar@example.com'`
 Or with a formatted name (includes unicode support): `'Ноде Майлер <foobar@example.com>'`
 Or with a formatted name (includes unicode support): `'Ноде Майлер <foobar@example.com>'`
 Or a comma-separated list of addresses: `'foobar@example.com, "Ноде Майлер" <bar@example.com>'`
 Or a comma-separated list of addresses: `'foobar@example.com, "Ноде Майлер" <bar@example.com>'`
-
 ### setLanguageCode
 ### setLanguageCode
 
 
-<MemberInfo kind="method" type={`(setLanguageCodeFn: (event: Event) =&#62; <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a> | undefined) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} since="1.8.0" />
+<MemberInfo kind="method" type={`(setLanguageCodeFn: (event: Event) =&#62; <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a> | undefined) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}  since="1.8.0"  />
 
 
 A function which allows to override the language of the email. If not defined, the language from the context will be used.
 A function which allows to override the language of the email. If not defined, the language from the context will be used.
-
 ### setTemplateVars
 ### setTemplateVars
 
 
-<MemberInfo kind="method" type={`(templateVarsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#settemplatevarsfn'>SetTemplateVarsFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(templateVarsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#settemplatevarsfn'>SetTemplateVarsFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 A function which returns an object hash of variables which will be made available to the Handlebars template
 A function which returns an object hash of variables which will be made available to the Handlebars template
 and subject line for interpolation.
 and subject line for interpolation.
-
 ### setSubject
 ### setSubject
 
 
-<MemberInfo kind="method" type={`(defaultSubject: string | <a href='/reference/core-plugins/email-plugin/email-plugin-types#setsubjectfn'>SetSubjectFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(defaultSubject: string | <a href='/reference/core-plugins/email-plugin/email-plugin-types#setsubjectfn'>SetSubjectFn</a>&#60;Event&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 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.
-
 ### setFrom
 ### setFrom
 
 
-<MemberInfo kind="method" type={`(from: string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(from: string) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 Sets the default from field of the email. The from string may use Handlebars variables defined by the
 Sets the default from field of the email. The from string may use Handlebars variables defined by the
 setTemplateVars() method.
 setTemplateVars() method.
-
 ### setOptionalAddressFields
 ### setOptionalAddressFields
 
 
-<MemberInfo kind="method" type={`(optionalAddressFieldsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setoptionaladdressfieldsfn'>SetOptionalAddressFieldsFn</a>&#60;Event&#62;) => `} since="1.1.0" />
+<MemberInfo kind="method" type={`(optionalAddressFieldsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setoptionaladdressfieldsfn'>SetOptionalAddressFieldsFn</a>&#60;Event&#62;) => `}  since="1.1.0"  />
 
 
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a> to be specified such as "cc" and "bcc".
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a> to be specified such as "cc" and "bcc".
-
 ### setMetadata
 ### setMetadata
 
 
-<MemberInfo kind="method" type={`(optionalSetMetadataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setmetadatafn'>SetMetadataFn</a>&#60;Event&#62;) => `} since="3.1.0" />
+<MemberInfo kind="method" type={`(optionalSetMetadataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setmetadatafn'>SetMetadataFn</a>&#60;Event&#62;) => `}  since="3.1.0"  />
 
 
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#emailmetadata'>EmailMetadata</a> to be specified for the email. This can be used
 A function which allows <a href='/reference/core-plugins/email-plugin/email-plugin-types#emailmetadata'>EmailMetadata</a> to be specified for the email. This can be used
 to store arbitrary data about the email which can be used for tracking or other purposes.
 to store arbitrary data about the email which can be used for tracking or other purposes.
@@ -207,22 +218,23 @@ It will be exposed in the <a href='/reference/core-plugins/email-plugin/email-se
 
 
 - An <a href='/reference/typescript-api/events/event-types#orderstatetransitionevent'>OrderStateTransitionEvent</a> occurs, and the EmailEventListener starts processing it.
 - An <a href='/reference/typescript-api/events/event-types#orderstatetransitionevent'>OrderStateTransitionEvent</a> occurs, and the EmailEventListener starts processing it.
 - The EmailEventHandler attaches metadata to the email:
 - The EmailEventHandler attaches metadata to the email:
-    ```ts
-    new EmailEventListener(EventType.ORDER_CONFIRMATION).on(OrderStateTransitionEvent).setMetadata(event => ({
-        type: EventType.ORDER_CONFIRMATION,
-        orderId: event.order.id,
-    }));
-    ```
+   ```ts
+   new EmailEventListener(EventType.ORDER_CONFIRMATION)
+     .on(OrderStateTransitionEvent)
+     .setMetadata(event => ({
+       type: EventType.ORDER_CONFIRMATION,
+       orderId: event.order.id,
+     }));
+  ```
 - Then, the EmailPlugin tries to send the email and publishes <a href='/reference/core-plugins/email-plugin/email-send-event#emailsendevent'>EmailSendEvent</a>,
 - Then, the EmailPlugin tries to send the email and publishes <a href='/reference/core-plugins/email-plugin/email-send-event#emailsendevent'>EmailSendEvent</a>,
-  passing `ctx`, emailDetails, error or success, and this metadata.
+  passing ctx, emailDetails, error or success, and this metadata.
 - In another part of the server, we have an eventBus that subscribes to EmailSendEvent. We can use
 - In another part of the server, we have an eventBus that subscribes to EmailSendEvent. We can use
   `metadata.type` and `metadata.orderId` to identify the related order. For example, we can indicate on the
   `metadata.type` and `metadata.orderId` to identify the related order. For example, we can indicate on the
-  order that the email was successfully sent, or in case of an error, send a notification confirming
-  the order in another available way.
-
+   order that the email was successfully sent, or in case of an error, send a notification confirming
+   the order in another available way.
 ### setAttachments
 ### setAttachments
 
 
-<MemberInfo kind="method" type={`(setAttachmentsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setattachmentsfn'>SetAttachmentsFn</a>&#60;Event&#62;) => `} />
+<MemberInfo kind="method" type={`(setAttachmentsFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#setattachmentsfn'>SetAttachmentsFn</a>&#60;Event&#62;) => `}   />
 
 
 Defines one or more files to be attached to the email. An attachment can be specified
 Defines one or more files to be attached to the email. An attachment can be specified
 as either a `path` (to a file or URL) or as `content` which can be a string, Buffer or Stream.
 as either a `path` (to a file or URL) or as `content` which can be a string, Buffer or Stream.
@@ -232,34 +244,31 @@ into the job queue. For this reason the total size of all attachments passed as
 **less than ~50k**. If the attachments are greater than that limit, a warning will be logged and
 **less than ~50k**. If the attachments are greater than that limit, a warning will be logged and
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 
 
-_Example_
+*Example*
 
 
 ```ts
 ```ts
 const testAttachmentHandler = new EmailEventListener('activate-voucher')
 const testAttachmentHandler = new EmailEventListener('activate-voucher')
-    .on(ActivateVoucherEvent)
-    // ... omitted some steps for brevity
-    .setAttachments(async event => {
-        const { imageUrl, voucherCode } = await getVoucherDataForUser(event.user.id);
-        return [
-            {
-                filename: `voucher-${voucherCode}.jpg`,
-                path: imageUrl,
-            },
-        ];
-    });
+  .on(ActivateVoucherEvent)
+  // ... omitted some steps for brevity
+  .setAttachments(async (event) => {
+    const { imageUrl, voucherCode } = await getVoucherDataForUser(event.user.id);
+    return [
+      {
+        filename: `voucher-${voucherCode}.jpg`,
+        path: imageUrl,
+      },
+    ];
+  });
 ```
 ```
-
 ### addTemplate
 ### addTemplate
 
 
-<MemberInfo kind="method" deprecated="Define a custom TemplateLoader on plugin initalization to define templates based on the RequestContext.
-E.g. `EmailPlugin.init({ templateLoader: new CustomTemplateLoader() })`" type={`(config: EmailTemplateConfig) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(config: EmailTemplateConfig) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
 Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
 templates for channels or languageCodes other than the default.
 templates for channels or languageCodes other than the default.
-
 ### loadData
 ### loadData
 
 
-<MemberInfo kind="method" type={`(loadDataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loaddatafn'>LoadDataFn</a>&#60;Event, R&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler-with-async-data#emaileventhandlerwithasyncdata'>EmailEventHandlerWithAsyncData</a>&#60;R, T, Event, <a href='/reference/core-plugins/email-plugin/email-plugin-types#eventwithasyncdata'>EventWithAsyncData</a>&#60;Event, R&#62;&#62;`} />
+<MemberInfo kind="method" type={`(loadDataFn: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loaddatafn'>LoadDataFn</a>&#60;Event, R&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler-with-async-data#emaileventhandlerwithasyncdata'>EmailEventHandlerWithAsyncData</a>&#60;R, T, Event, <a href='/reference/core-plugins/email-plugin/email-plugin-types#eventwithasyncdata'>EventWithAsyncData</a>&#60;Event, R&#62;&#62;`}   />
 
 
 Allows data to be loaded asynchronously which can then be used as template variables.
 Allows data to be loaded asynchronously which can then be used as template variables.
 The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
 The `loadDataFn` has access to the event, the TypeORM `Connection` object, and an
@@ -267,28 +276,28 @@ The `loadDataFn` has access to the event, the TypeORM `Connection` object, and a
 by the <a href='/reference/typescript-api/plugin/plugin-common-module#plugincommonmodule'>PluginCommonModule</a>. The return value of the `loadDataFn` will be
 by the <a href='/reference/typescript-api/plugin/plugin-common-module#plugincommonmodule'>PluginCommonModule</a>. The return value of the `loadDataFn` will be
 added to the `event` as the `data` property.
 added to the `event` as the `data` property.
 
 
-_Example_
+*Example*
 
 
 ```ts
 ```ts
 new EmailEventListener('order-confirmation')
 new EmailEventListener('order-confirmation')
-    .on(OrderStateTransitionEvent)
-    .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
-    .loadData(({ event, injector }) => {
-        const orderService = injector.get(OrderService);
-        return orderService.getOrderPayments(event.order.id);
-    })
-    .setTemplateVars(event => ({
-        order: event.order,
-        payments: event.data,
-    }));
-// ...
+  .on(OrderStateTransitionEvent)
+  .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
+  .loadData(({ event, injector }) => {
+    const orderService = injector.get(OrderService);
+    return orderService.getOrderPayments(event.order.id);
+  })
+  .setTemplateVars(event => ({
+    order: event.order,
+    payments: event.data,
+  }))
+  // ...
 ```
 ```
-
 ### setMockEvent
 ### setMockEvent
 
 
-<MemberInfo kind="method" type={`(event: Omit&#60;Event, 'ctx' | 'data'&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`} />
+<MemberInfo kind="method" type={`(event: Omit&#60;Event, 'ctx' | 'data'&#62;) => <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;T, Event&#62;`}   />
 
 
 Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
 Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
 from this handler, which is useful when developing the email templates.
 from this handler, which is useful when developing the email templates.
 
 
+
 </div>
 </div>

+ 18 - 12
docs/docs/reference/core-plugins/email-plugin/template-loader.md

@@ -1,23 +1,22 @@
 ---
 ---
-title: 'TemplateLoader'
+title: "TemplateLoader"
 isDefaultIndex: false
 isDefaultIndex: false
 generated: true
 generated: true
 ---
 ---
-
 <!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
 <!-- 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 MemberInfo from '@site/src/components/MemberInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import GenerationInfo from '@site/src/components/GenerationInfo';
 import MemberDescription from '@site/src/components/MemberDescription';
 import MemberDescription from '@site/src/components/MemberDescription';
 
 
+
 ## TemplateLoader
 ## TemplateLoader
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/template-loader.ts" sourceLine="32" packageName="@vendure/email-plugin" />
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/template-loader.ts" sourceLine="32" packageName="@vendure/email-plugin" />
 
 
 Loads email templates based on the given request context, type and template name
 Loads email templates based on the given request context, type and template name
-and returns the template as a string.
+and return the template as a string.
 
 
-_Example_
+*Example*
 
 
 ```ts
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -47,19 +46,20 @@ interface TemplateLoader {
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`} />
+<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
 
 
 Load template and return it's content as a string
 Load template and return it's content as a string
-
 ### loadPartials
 ### loadPartials
 
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
 
 
 Load partials and return their contents.
 Load partials and return their contents.
 This method is only called during initialization, i.e. during server startup.
 This method is only called during initialization, i.e. during server startup.
 
 
+
 </div>
 </div>
 
 
+
 ## FileBasedTemplateLoader
 ## FileBasedTemplateLoader
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/file-based-template-loader.ts" sourceLine="17" packageName="@vendure/email-plugin" />
 <GenerationInfo sourceFile="packages/email-plugin/src/template-loader/file-based-template-loader.ts" sourceLine="17" packageName="@vendure/email-plugin" />
@@ -74,21 +74,27 @@ class FileBasedTemplateLoader implements TemplateLoader {
     loadPartials() => Promise<Partial[]>;
     loadPartials() => Promise<Partial[]>;
 }
 }
 ```
 ```
+* Implements: <code><a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a></code>
+
 
 
-- Implements: <code><a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a></code>
 
 
 <div className="members-wrapper">
 <div className="members-wrapper">
 
 
 ### constructor
 ### constructor
 
 
-<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`} />
+<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`}   />
+
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`} />
+<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
+
 
 
 ### loadPartials
 ### loadPartials
 
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+
+
+
 
 
 </div>
 </div>

+ 41 - 0
docs/docs/reference/dashboard/components/data-table-bulk-actions.md

@@ -0,0 +1,41 @@
+---
+title: "DataTableBulkActions"
+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';
+
+
+## BulkAction
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/data-table.ts" sourceLine="45" packageName="@vendure/dashboard" since="3.4.0" />
+
+**Status: Developer Preview**
+
+A bulk action is a component that will be rendered in the bulk actions dropdown.
+
+```ts title="Signature"
+type BulkAction = {
+    order?: number;
+    component: BulkActionComponent<any>;
+}
+```
+
+<div className="members-wrapper">
+
+### order
+
+<MemberInfo kind="property" type={`number`}   />
+
+
+### component
+
+<MemberInfo kind="property" type={`BulkActionComponent&#60;any&#62;`}   />
+
+
+
+
+</div>

+ 2 - 2
docs/docs/reference/dashboard/components/detail-page.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DetailPage
 ## DetailPage
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/detail-page.tsx" sourceLine="124" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/detail-page.tsx" sourceLine="152" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -32,7 +32,7 @@ Parameters
 
 
 ## DetailPageProps
 ## DetailPageProps
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/detail-page.tsx" sourceLine="39" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/detail-page.tsx" sourceLine="42" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 10 - 4
docs/docs/reference/dashboard/components/list-page.md

@@ -11,26 +11,26 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## ListPage
 ## ListPage
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/list-page.tsx" sourceLine="73" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/list-page.tsx" sourceLine="69" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 Auto-generates a list page with columns generated based on the provided query document fields.
 Auto-generates a list page with columns generated based on the provided query document fields.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function ListPage<T extends TypedDocumentNode<U, V>, U extends Record<string, any> = any, V extends ListQueryOptionsShape = ListQueryOptionsShape, AC extends AdditionalColumns<T> = AdditionalColumns<T>>(props: ListPageProps<T, U, V, AC>): void
+function ListPage<T extends TypedDocumentNode<U, V>, U extends Record<string, any> = any, V extends ListQueryOptionsShape = ListQueryOptionsShape, AC extends AdditionalColumns<T> = AdditionalColumns<T>>(props: Readonly<ListPageProps<T, U, V, AC>>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`<a href='/reference/dashboard/components/list-page#listpageprops'>ListPageProps</a>&#60;T, U, V, AC&#62;`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;<a href='/reference/dashboard/components/list-page#listpageprops'>ListPageProps</a>&#60;T, U, V, AC&#62;&#62;`} />
 
 
 
 
 
 
 ## ListPageProps
 ## ListPageProps
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/list-page.tsx" sourceLine="35" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/list-page.tsx" sourceLine="30" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -55,6 +55,7 @@ interface ListPageProps<T extends TypedDocumentNode<U, V>, U extends ListQuerySh
     rowActions?: RowAction<ListQueryFields<T>>[];
     rowActions?: RowAction<ListQueryFields<T>>[];
     transformData?: (data: any[]) => any[];
     transformData?: (data: any[]) => any[];
     setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
     setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
+    bulkActions?: BulkAction[];
 }
 }
 ```
 ```
 
 
@@ -145,6 +146,11 @@ interface ListPageProps<T extends TypedDocumentNode<U, V>, U extends ListQuerySh
 <MemberInfo kind="property" type={`(table: TableOptions&#60;any&#62;) =&#62; TableOptions&#60;any&#62;`}   />
 <MemberInfo kind="property" type={`(table: TableOptions&#60;any&#62;) =&#62; TableOptions&#60;any&#62;`}   />
 
 
 
 
+### bulkActions
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/components/data-table-bulk-actions#bulkaction'>BulkAction</a>[]`}   />
+
+
 
 
 
 
 </div>
 </div>

+ 12 - 9
docs/docs/reference/dashboard/components/page-action-bar.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PageActionBar
 ## PageActionBar
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="261" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="275" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -20,45 +20,48 @@ It should be used in conjunction with the <a href='/reference/dashboard/componen
 as direct children.
 as direct children.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageActionBar(props: { children: React.ReactNode }): void
+function PageActionBar(props: Readonly<{ children: React.ReactNode }>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{ children: React.ReactNode }&#62;`} />
 
 
 
 
 
 
 ## PageActionBarLeft
 ## PageActionBarLeft
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="287" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="297" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageActionBarLeft(props: { children: React.ReactNode }): void
+function PageActionBarLeft(props: Readonly<{ children: React.ReactNode }>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{ children: React.ReactNode }&#62;`} />
 
 
 
 
 
 
 ## PageActionBarRight
 ## PageActionBarRight
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="299" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="311" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageActionBarRight(props: { children: React.ReactNode }): void
+function PageActionBarRight(props: Readonly<{
+    children: React.ReactNode;
+    dropdownMenuItems?: InlineDropdownItem[];
+}>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{     children: React.ReactNode;     dropdownMenuItems?: InlineDropdownItem[]; }&#62;`} />
 
 

+ 11 - 11
docs/docs/reference/dashboard/components/page-block.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PageBlock
 ## PageBlock
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="351" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="407" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -20,19 +20,19 @@ It should be provided with a `column` prop to determine which column it should a
 to identify the block.
 to identify the block.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageBlock(props: PageBlockProps): void
+function PageBlock(props: Readonly<PageBlockProps>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`<a href='/reference/dashboard/components/page-block#pageblockprops'>PageBlockProps</a>`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;<a href='/reference/dashboard/components/page-block#pageblockprops'>PageBlockProps</a>&#62;`} />
 
 
 
 
 
 
 ## PageBlockProps
 ## PageBlockProps
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="328" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="384" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -86,7 +86,7 @@ type PageBlockProps = {
 
 
 ## FullWidthPageBlock
 ## FullWidthPageBlock
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="378" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="444" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -94,34 +94,34 @@ A component for displaying a block of content on a page that takes up the full w
 This should be used inside the <a href='/reference/dashboard/components/page-layout#pagelayout'>PageLayout</a> component.
 This should be used inside the <a href='/reference/dashboard/components/page-layout#pagelayout'>PageLayout</a> component.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function FullWidthPageBlock(props: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>): void
+function FullWidthPageBlock(props: Readonly<Pick<PageBlockProps, 'children' | 'className' | 'blockId'>>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`Pick&#60;<a href='/reference/dashboard/components/page-block#pageblockprops'>PageBlockProps</a>, 'children' | 'className' | 'blockId'&#62;`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;Pick&#60;<a href='/reference/dashboard/components/page-block#pageblockprops'>PageBlockProps</a>, 'children' | 'className' | 'blockId'&#62;&#62;`} />
 
 
 
 
 
 
 ## CustomFieldsPageBlock
 ## CustomFieldsPageBlock
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="400" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="469" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 A component for displaying an auto-generated form for custom fields on a page.
 A component for displaying an auto-generated form for custom fields on a page.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function CustomFieldsPageBlock(props: {
+function CustomFieldsPageBlock(props: Readonly<{
     column: 'main' | 'side';
     column: 'main' | 'side';
     entityType: string;
     entityType: string;
     control: Control<any, any>;
     control: Control<any, any>;
-}): void
+}>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`{     column: 'main' | 'side';     entityType: string;     control: Control&#60;any, any&#62;; }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{     column: 'main' | 'side';     entityType: string;     control: Control&#60;any, any&#62;; }&#62;`} />
 
 

+ 4 - 4
docs/docs/reference/dashboard/components/page-layout.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PageLayout
 ## PageLayout
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="157" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="170" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -19,19 +19,19 @@ This component governs the layout of the contents of a <a href='/reference/dashb
 It should contain all the <a href='/reference/dashboard/components/page-block#pageblock'>PageBlock</a> components that are to be displayed on the page.
 It should contain all the <a href='/reference/dashboard/components/page-block#pageblock'>PageBlock</a> components that are to be displayed on the page.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageLayout(props: PageLayoutProps): void
+function PageLayout(props: Readonly<PageLayoutProps>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`<a href='/reference/dashboard/components/page-layout#pagelayoutprops'>PageLayoutProps</a>`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;<a href='/reference/dashboard/components/page-layout#pagelayoutprops'>PageLayoutProps</a>&#62;`} />
 
 
 
 
 
 
 ## PageLayoutProps
 ## PageLayoutProps
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="127" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="140" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 3 - 3
docs/docs/reference/dashboard/components/page-title.md

@@ -11,18 +11,18 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PageTitle
 ## PageTitle
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="244" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="258" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 A component for displaying the title of a page. This should be used inside the <a href='/reference/dashboard/components/page#page'>Page</a> component.
 A component for displaying the title of a page. This should be used inside the <a href='/reference/dashboard/components/page#page'>Page</a> component.
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function PageTitle(props: { children: React.ReactNode }): void
+function PageTitle(props: Readonly<{ children: React.ReactNode }>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{ children: React.ReactNode }&#62;`} />
 
 

+ 3 - 3
docs/docs/reference/dashboard/components/page.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## Page
 ## Page
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="44" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx" sourceLine="48" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -25,11 +25,11 @@ The typical hierarchy of a page is as follows:
  - <a href='/reference/dashboard/components/page-layout#pagelayout'>PageLayout</a>
  - <a href='/reference/dashboard/components/page-layout#pagelayout'>PageLayout</a>
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function Page(props: PageProps): void
+function Page(props: Readonly<PageProps>): void
 ```
 ```
 Parameters
 Parameters
 
 
 ### props
 ### props
 
 
-<MemberInfo kind="parameter" type={`PageProps`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;PageProps&#62;`} />
 
 

+ 21 - 1
docs/docs/reference/dashboard/extensions/dashboard-action-bar-item.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DashboardActionBarItem
 ## DashboardActionBarItem
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="51" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/layout.ts" sourceLine="19" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -21,6 +21,7 @@ Allows you to define custom action bar items for any page in the dashboard.
 interface DashboardActionBarItem {
 interface DashboardActionBarItem {
     pageId: string;
     pageId: string;
     component: React.FunctionComponent<{ context: PageContextValue }>;
     component: React.FunctionComponent<{ context: PageContextValue }>;
+    type?: 'button' | 'dropdown';
     requiresPermission?: string | string[];
     requiresPermission?: string | string[];
 }
 }
 ```
 ```
@@ -37,6 +38,25 @@ The ID of the page where the action bar item should be displayed.
 <MemberInfo kind="property" type={`React.FunctionComponent&#60;{ context: PageContextValue }&#62;`}   />
 <MemberInfo kind="property" type={`React.FunctionComponent&#60;{ context: PageContextValue }&#62;`}   />
 
 
 A React component that will be rendered in the action bar.
 A React component that will be rendered in the action bar.
+### type
+
+<MemberInfo kind="property" type={`'button' | 'dropdown'`} default={`'button'`}   />
+
+The type of action bar item to display. Defaults to `button`.
+The 'dropdown' type is used to display the action bar item as a dropdown menu item.
+
+When using the dropdown type, use a suitable [dropdown item](https://ui.shadcn.com/docs/components/dropdown-menu)
+component, such as:
+
+```tsx
+import { DropdownMenuItem } from '@vendure/dashboard';
+
+// ...
+
+{
+  component: () => <DropdownMenuItem>My Item</DropdownMenuItem>
+}
+```
 ### requiresPermission
 ### requiresPermission
 
 
 <MemberInfo kind="property" type={`string | string[]`}   />
 <MemberInfo kind="property" type={`string | string[]`}   />

+ 80 - 0
docs/docs/reference/dashboard/extensions/dashboard-alert-definition.md

@@ -0,0 +1,80 @@
+---
+title: "DashboardAlertDefinition"
+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';
+
+
+## DashboardAlertDefinition
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/alerts.ts" sourceLine="10" packageName="@vendure/dashboard" since="3.3.0" />
+
+**Status: Developer Preview**
+
+Allows you to define custom alerts that can be displayed in the dashboard.
+
+```ts title="Signature"
+interface DashboardAlertDefinition<TResponse = any> {
+    id: string;
+    title: string | ((data: TResponse) => string);
+    description?: string | ((data: TResponse) => string);
+    severity: 'info' | 'warning' | 'error';
+    check: () => Promise<TResponse> | TResponse;
+    recheckInterval?: number;
+    shouldShow?: (data: TResponse) => boolean;
+    actions?: Array<{
+        label: string;
+        onClick: (data: TResponse) => void;
+    }>;
+}
+```
+
+<div className="members-wrapper">
+
+### id
+
+<MemberInfo kind="property" type={`string`}   />
+
+A unique identifier for the alert.
+### title
+
+<MemberInfo kind="property" type={`string | ((data: TResponse) =&#62; string)`}   />
+
+The title of the alert. Can be a string or a function that returns a string based on the response data.
+### description
+
+<MemberInfo kind="property" type={`string | ((data: TResponse) =&#62; string)`}   />
+
+The description of the alert. Can be a string or a function that returns a string based on the response data.
+### severity
+
+<MemberInfo kind="property" type={`'info' | 'warning' | 'error'`}   />
+
+The severity level of the alert.
+### check
+
+<MemberInfo kind="property" type={`() =&#62; Promise&#60;TResponse&#62; | TResponse`}   />
+
+A function that checks the condition and returns the response data.
+### recheckInterval
+
+<MemberInfo kind="property" type={`number`}   />
+
+The interval in milliseconds to recheck the condition.
+### shouldShow
+
+<MemberInfo kind="property" type={`(data: TResponse) =&#62; boolean`}   />
+
+A function that determines whether the alert should be shown based on the response data.
+### actions
+
+<MemberInfo kind="property" type={`Array&#60;{         label: string;         onClick: (data: TResponse) =&#62; void;     }&#62;`}   />
+
+Optional actions that can be performed when the alert is shown.
+
+
+</div>

+ 28 - 0
docs/docs/reference/dashboard/extensions/dashboard-base-widget-props.md

@@ -0,0 +1,28 @@
+---
+title: "DashboardBaseWidgetProps"
+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';
+
+
+## DashboardBaseWidgetProps
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/widgets.ts" sourceLine="12" packageName="@vendure/dashboard" since="3.3.0" />
+
+**Status: Developer Preview**
+
+Base props interface for dashboard widgets.
+
+```ts title="Signature"
+type DashboardBaseWidgetProps = PropsWithChildren<{
+    id: string;
+    title?: string;
+    description?: string;
+    config?: Record<string, unknown>;
+    actions?: React.ReactNode;
+}>
+```

+ 1 - 1
docs/docs/reference/dashboard/extensions/dashboard-custom-form-component.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DashboardCustomFormComponent
 ## DashboardCustomFormComponent
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="18" packageName="@vendure/dashboard" since="3.4.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/form-components.ts" sourceLine="12" packageName="@vendure/dashboard" since="3.4.0" />
 
 
 Allows you to define custom form components for custom fields in the dashboard.
 Allows you to define custom form components for custom fields in the dashboard.
 
 

+ 35 - 0
docs/docs/reference/dashboard/extensions/dashboard-custom-form-components.md

@@ -0,0 +1,35 @@
+---
+title: "DashboardCustomFormComponents"
+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';
+
+
+## DashboardCustomFormComponents
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/form-components.ts" sourceLine="25" packageName="@vendure/dashboard" since="3.4.0" />
+
+Interface for registering custom field components in the dashboard.
+For input and display components, use the co-located approach with detailForms.
+
+```ts title="Signature"
+interface DashboardCustomFormComponents {
+    customFields?: DashboardCustomFormComponent[];
+}
+```
+
+<div className="members-wrapper">
+
+### customFields
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-custom-form-component#dashboardcustomformcomponent'>DashboardCustomFormComponent</a>[]`}   />
+
+Custom form components for custom fields. These are used when rendering
+custom fields in forms.
+
+
+</div>

+ 41 - 0
docs/docs/reference/dashboard/extensions/dashboard-data-table-display-component.md

@@ -0,0 +1,41 @@
+---
+title: "DashboardDataTableDisplayComponent"
+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';
+
+
+## DashboardDataTableDisplayComponent
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/data-table.ts" sourceLine="12" packageName="@vendure/dashboard" since="3.4.0" />
+
+Allows you to define custom display components for specific columns in data tables.
+The pageId is already defined in the data table extension, so only the column name is needed.
+
+```ts title="Signature"
+interface DashboardDataTableDisplayComponent {
+    column: string;
+    component: React.ComponentType<{ value: any; [key: string]: any }>;
+}
+```
+
+<div className="members-wrapper">
+
+### column
+
+<MemberInfo kind="property" type={`string`}   />
+
+The name of the column where this display component should be used.
+### component
+
+<MemberInfo kind="property" type={`React.ComponentType&#60;{ value: any; [key: string]: any }&#62;`}   />
+
+The React component that will be rendered as the display.
+It should accept `value` and other standard display props.
+
+
+</div>

+ 61 - 0
docs/docs/reference/dashboard/extensions/dashboard-data-table-extension-definition.md

@@ -0,0 +1,61 @@
+---
+title: "DashboardDataTableExtensionDefinition"
+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';
+
+
+## DashboardDataTableExtensionDefinition
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/data-table.ts" sourceLine="59" packageName="@vendure/dashboard" since="3.4.0" />
+
+**Status: Developer Preview**
+
+This allows you to customize aspects of existing data tables in the dashboard.
+
+```ts title="Signature"
+interface DashboardDataTableExtensionDefinition {
+    pageId: string;
+    blockId?: string;
+    bulkActions?: BulkAction[];
+    extendListDocument?: string | DocumentNode | (() => DocumentNode | string);
+    displayComponents?: DashboardDataTableDisplayComponent[];
+}
+```
+
+<div className="members-wrapper">
+
+### pageId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The ID of the page where the data table is located, e.g. `'product-list'`, `'order-list'`.
+### blockId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The ID of the data table block. Defaults to `'list-table'`, which is the default blockId
+for the standard list pages. However, some other pages may use a different blockId,
+such as `'product-variants-table'` on the `'product-detail'` page.
+### bulkActions
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/components/data-table-bulk-actions#bulkaction'>BulkAction</a>[]`}   />
+
+An array of additional bulk actions that will be available on the data table.
+### extendListDocument
+
+<MemberInfo kind="property" type={`string | DocumentNode | (() =&#62; DocumentNode | string)`}   />
+
+Allows you to extend the list document for the data table.
+### displayComponents
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-data-table-display-component#dashboarddatatabledisplaycomponent'>DashboardDataTableDisplayComponent</a>[]`}   />
+
+Custom display components for specific columns in the data table.
+
+
+</div>

+ 47 - 0
docs/docs/reference/dashboard/extensions/dashboard-detail-form-display-component.md

@@ -0,0 +1,47 @@
+---
+title: "DashboardDetailFormDisplayComponent"
+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';
+
+
+## DashboardDetailFormDisplayComponent
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/detail-forms.ts" sourceLine="42" packageName="@vendure/dashboard" since="3.4.0" />
+
+Allows you to define custom display components for specific fields in detail forms.
+The pageId is already defined in the detail form extension, so only the blockId and field are needed.
+
+```ts title="Signature"
+interface DashboardDetailFormDisplayComponent {
+    blockId: string;
+    field: string;
+    component: DataDisplayComponent;
+}
+```
+
+<div className="members-wrapper">
+
+### blockId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The ID of the block where this display component should be used.
+### field
+
+<MemberInfo kind="property" type={`string`}   />
+
+The name of the field where this display component should be used.
+### component
+
+<MemberInfo kind="property" type={`DataDisplayComponent`}   />
+
+The React component that will be rendered as the display.
+It should accept `value` and other standard display props.
+
+
+</div>

+ 47 - 0
docs/docs/reference/dashboard/extensions/dashboard-detail-form-input-component.md

@@ -0,0 +1,47 @@
+---
+title: "DashboardDetailFormInputComponent"
+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';
+
+
+## DashboardDetailFormInputComponent
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/detail-forms.ts" sourceLine="15" packageName="@vendure/dashboard" since="3.4.0" />
+
+Allows you to define custom input components for specific fields in detail forms.
+The pageId is already defined in the detail form extension, so only the blockId and field are needed.
+
+```ts title="Signature"
+interface DashboardDetailFormInputComponent {
+    blockId: string;
+    field: string;
+    component: DataInputComponent;
+}
+```
+
+<div className="members-wrapper">
+
+### blockId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The ID of the block where this input component should be used.
+### field
+
+<MemberInfo kind="property" type={`string`}   />
+
+The name of the field where this input component should be used.
+### component
+
+<MemberInfo kind="property" type={`DataInputComponent`}   />
+
+The React component that will be rendered as the input.
+It should accept `value`, `onChange`, and other standard input props.
+
+
+</div>

+ 20 - 7
docs/docs/reference/dashboard/extensions/dashboard-extension.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DashboardExtension
 ## DashboardExtension
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="121" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="23" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -25,7 +25,9 @@ interface DashboardExtension {
     actionBarItems?: DashboardActionBarItem[];
     actionBarItems?: DashboardActionBarItem[];
     alerts?: DashboardAlertDefinition[];
     alerts?: DashboardAlertDefinition[];
     widgets?: DashboardWidgetDefinition[];
     widgets?: DashboardWidgetDefinition[];
-    customFormComponents?: DashboardCustomFormComponent[];
+    customFormComponents?: DashboardCustomFormComponents;
+    dataTables?: DashboardDataTableExtensionDefinition[];
+    detailForms?: DashboardDetailFormExtensionDefinition[];
 }
 }
 ```
 ```
 
 
@@ -53,20 +55,31 @@ Allows you to define custom page blocks for any page in the dashboard.
 Allows you to define custom action bar items for any page in the dashboard.
 Allows you to define custom action bar items for any page in the dashboard.
 ### alerts
 ### alerts
 
 
-<MemberInfo kind="property" type={`DashboardAlertDefinition[]`}   />
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-alert-definition#dashboardalertdefinition'>DashboardAlertDefinition</a>[]`}   />
 
 
-Not yet implemented
+Allows you to define custom alerts that can be displayed in the dashboard.
 ### widgets
 ### widgets
 
 
-<MemberInfo kind="property" type={`DashboardWidgetDefinition[]`}   />
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-widget-definition#dashboardwidgetdefinition'>DashboardWidgetDefinition</a>[]`}   />
 
 
 Allows you to define custom routes for the dashboard, which will render the
 Allows you to define custom routes for the dashboard, which will render the
 given components and optionally also add a nav menu item.
 given components and optionally also add a nav menu item.
 ### customFormComponents
 ### customFormComponents
 
 
-<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-custom-form-component#dashboardcustomformcomponent'>DashboardCustomFormComponent</a>[]`}   />
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-custom-form-components#dashboardcustomformcomponents'>DashboardCustomFormComponents</a>`}   />
+
+Unified registration for custom form components including custom field components,
+input components, and display components.
+### dataTables
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/extensions/dashboard-data-table-extension-definition#dashboarddatatableextensiondefinition'>DashboardDataTableExtensionDefinition</a>[]`}   />
+
+Allows you to customize aspects of existing data tables in the dashboard.
+### detailForms
+
+<MemberInfo kind="property" type={`DashboardDetailFormExtensionDefinition[]`}   />
+
 
 
-Allows you to define custom form components for custom fields in the dashboard.
 
 
 
 
 </div>
 </div>

+ 1 - 1
docs/docs/reference/dashboard/extensions/dashboard-page-block-definition.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## DashboardPageBlockDefinition
 ## DashboardPageBlockDefinition
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="104" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/layout.ts" sourceLine="87" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 65 - 0
docs/docs/reference/dashboard/extensions/dashboard-widget-definition.md

@@ -0,0 +1,65 @@
+---
+title: "DashboardWidgetDefinition"
+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';
+
+
+## DashboardWidgetDefinition
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/widgets.ts" sourceLine="66" packageName="@vendure/dashboard" since="3.3.0" />
+
+**Status: Developer Preview**
+
+Defines a dashboard widget that can be added to the dashboard.
+
+```ts title="Signature"
+type DashboardWidgetDefinition = {
+    id: string;
+    name: string;
+    component: React.ComponentType<DashboardBaseWidgetProps>;
+    defaultSize: { w: number; h: number; x?: number; y?: number };
+    minSize?: { w: number; h: number };
+    maxSize?: { w: number; h: number };
+}
+```
+
+<div className="members-wrapper">
+
+### id
+
+<MemberInfo kind="property" type={`string`}   />
+
+A unique identifier for the widget.
+### name
+
+<MemberInfo kind="property" type={`string`}   />
+
+The display name of the widget.
+### component
+
+<MemberInfo kind="property" type={`React.ComponentType&#60;<a href='/reference/dashboard/extensions/dashboard-base-widget-props#dashboardbasewidgetprops'>DashboardBaseWidgetProps</a>&#62;`}   />
+
+The React component that renders the widget.
+### defaultSize
+
+<MemberInfo kind="property" type={`{ w: number; h: number; x?: number; y?: number }`}   />
+
+The default size and position of the widget.
+### minSize
+
+<MemberInfo kind="property" type={`{ w: number; h: number }`}   />
+
+The minimum size constraints for the widget.
+### maxSize
+
+<MemberInfo kind="property" type={`{ w: number; h: number }`}   />
+
+The maximum size constraints for the widget.
+
+
+</div>

+ 58 - 0
docs/docs/reference/dashboard/extensions/dashboard-widget-instance.md

@@ -0,0 +1,58 @@
+---
+title: "DashboardWidgetInstance"
+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';
+
+
+## DashboardWidgetInstance
+
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/widgets.ts" sourceLine="29" packageName="@vendure/dashboard" since="3.3.0" />
+
+**Status: Developer Preview**
+
+Represents an instance of a dashboard widget with its layout and configuration.
+
+```ts title="Signature"
+type DashboardWidgetInstance = {
+    id: string;
+    widgetId: string;
+    layout: {
+        x: number;
+        y: number;
+        w: number;
+        h: number;
+    };
+    config?: Record<string, unknown>;
+}
+```
+
+<div className="members-wrapper">
+
+### id
+
+<MemberInfo kind="property" type={`string`}   />
+
+A unique identifier for the widget instance.
+### widgetId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The ID of the widget definition this instance is based on.
+### layout
+
+<MemberInfo kind="property" type={`{         x: number;         y: number;         w: number;         h: number;     }`}   />
+
+The layout configuration for the widget.
+### config
+
+<MemberInfo kind="property" type={`Record&#60;string, unknown&#62;`}   />
+
+Optional configuration data for the widget.
+
+
+</div>

+ 1 - 1
docs/docs/reference/dashboard/extensions/define-dashboard-extension.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## defineDashboardExtension
 ## defineDashboardExtension
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts" sourceLine="36" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts" sourceLine="37" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 1 - 1
docs/docs/reference/dashboard/extensions/page-block-location.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PageBlockLocation
 ## PageBlockLocation
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts" sourceLine="88" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/extension-api/types/layout.ts" sourceLine="71" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 2 - 2
docs/docs/reference/dashboard/hooks/use-auth.md

@@ -31,7 +31,7 @@ function useAuth(): void
 
 
 ```ts title="Signature"
 ```ts title="Signature"
 interface AuthContext {
 interface AuthContext {
-    status: 'authenticated' | 'verifying' | 'unauthenticated';
+    status: 'initial' | 'authenticated' | 'verifying' | 'unauthenticated';
     authenticationError?: string;
     authenticationError?: string;
     isAuthenticated: boolean;
     isAuthenticated: boolean;
     login: (username: string, password: string, onSuccess?: () => void) => void;
     login: (username: string, password: string, onSuccess?: () => void) => void;
@@ -45,7 +45,7 @@ interface AuthContext {
 
 
 ### status
 ### status
 
 
-<MemberInfo kind="property" type={`'authenticated' | 'verifying' | 'unauthenticated'`}   />
+<MemberInfo kind="property" type={`'initial' | 'authenticated' | 'verifying' | 'unauthenticated'`}   />
 
 
 
 
 ### authenticationError
 ### authenticationError

+ 14 - 6
docs/docs/reference/dashboard/hooks/use-detail-page.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## useDetailPage
 ## useDetailPage
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="224" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="239" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
@@ -77,7 +77,7 @@ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
 ```
 ```
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-function useDetailPage<T extends TypedDocumentNode<any, any>, C extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T> = keyof ResultOf<T>, VarNameUpdate extends keyof VariablesOf<U> = 'input', VarNameCreate extends keyof VariablesOf<C> = 'input'>(options: DetailPageOptions<T, C, U, EntityField, VarNameCreate, VarNameUpdate>): UseDetailPageResult<T, C, U, EntityField>
+function useDetailPage<T extends TypedDocumentNode<any, any>, C extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T> = keyof ResultOf<T>, VarNameUpdate extends keyof VariablesOf<U> = 'input', VarNameCreate extends keyof VariablesOf<C> = 'input'>(options: DetailPageOptions<T, C, U, EntityField, VarNameCreate, VarNameUpdate>): UseDetailPageResult<T, U, EntityField>
 ```
 ```
 Parameters
 Parameters
 
 
@@ -89,18 +89,19 @@ Parameters
 
 
 ## DetailPageOptions
 ## DetailPageOptions
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="39" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="46" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 ```ts title="Signature"
 ```ts title="Signature"
 interface DetailPageOptions<T extends TypedDocumentNode<any, any>, C extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T> = DetailEntityPath<T>, VarNameCreate extends keyof VariablesOf<C> = 'input', VarNameUpdate extends keyof VariablesOf<U> = 'input'> {
 interface DetailPageOptions<T extends TypedDocumentNode<any, any>, C extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T> = DetailEntityPath<T>, VarNameCreate extends keyof VariablesOf<C> = 'input', VarNameUpdate extends keyof VariablesOf<U> = 'input'> {
+    pageId?: string;
     queryDocument: T;
     queryDocument: T;
     entityField?: EntityField;
     entityField?: EntityField;
     params: {
     params: {
         id: string;
         id: string;
     };
     };
-    entityName: string;
+    entityName?: string;
     createDocument?: C;
     createDocument?: C;
     updateDocument?: U;
     updateDocument?: U;
     setValuesForUpdate: (entity: NonNullable<ResultOf<T>[EntityField]>) => VariablesOf<U>[VarNameUpdate];
     setValuesForUpdate: (entity: NonNullable<ResultOf<T>[EntityField]>) => VariablesOf<U>[VarNameUpdate];
@@ -113,6 +114,12 @@ interface DetailPageOptions<T extends TypedDocumentNode<any, any>, C extends Typ
 
 
 <div className="members-wrapper">
 <div className="members-wrapper">
 
 
+### pageId
+
+<MemberInfo kind="property" type={`string`}   />
+
+The page id. This is optional, but if provided, it will be used to
+identify the page when extending the detail page query
 ### queryDocument
 ### queryDocument
 
 
 <MemberInfo kind="property" type={`T`}   />
 <MemberInfo kind="property" type={`T`}   />
@@ -134,6 +141,7 @@ The parameters used to identify the entity.
 
 
 The entity type name for custom field configuration lookup.
 The entity type name for custom field configuration lookup.
 Required to filter out readonly custom fields before mutations.
 Required to filter out readonly custom fields before mutations.
+If not provided, the function will try to infer it from the query document.
 ### createDocument
 ### createDocument
 
 
 <MemberInfo kind="property" type={`C`}   />
 <MemberInfo kind="property" type={`C`}   />
@@ -176,12 +184,12 @@ The function to call when the update is successful.
 
 
 ## UseDetailPageResult
 ## UseDetailPageResult
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="141" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/framework/page/use-detail-page.ts" sourceLine="157" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-interface UseDetailPageResult<T extends TypedDocumentNode<any, any>, C extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T>> {
+interface UseDetailPageResult<T extends TypedDocumentNode<any, any>, U extends TypedDocumentNode<any, any>, EntityField extends keyof ResultOf<T>> {
     form: UseFormReturn<RemoveNullFields<VariablesOf<U>['input']>>;
     form: UseFormReturn<RemoveNullFields<VariablesOf<U>['input']>>;
     submitHandler: (event: FormEvent<HTMLFormElement>) => void;
     submitHandler: (event: FormEvent<HTMLFormElement>) => void;
     entity?: DetailPageEntity<T, EntityField>;
     entity?: DetailPageEntity<T, EntityField>;

+ 1 - 1
docs/docs/reference/dashboard/hooks/use-permissions.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## usePermissions
 ## usePermissions
 
 
-<GenerationInfo sourceFile="packages/dashboard/src/lib/hooks/use-permissions.ts" sourceLine="18" packageName="@vendure/dashboard" since="3.3.0" />
+<GenerationInfo sourceFile="packages/dashboard/src/lib/hooks/use-permissions.ts" sourceLine="19" packageName="@vendure/dashboard" since="3.3.0" />
 
 
 **Status: Developer Preview**
 **Status: Developer Preview**
 
 

+ 1 - 1
docs/docs/reference/typescript-api/configuration/merge-config.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## mergeConfig
 ## mergeConfig
 
 
-<GenerationInfo sourceFile="packages/core/src/config/merge-config.ts" sourceLine="30" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/merge-config.ts" sourceLine="32" packageName="@vendure/core" />
 
 
 Performs a deep merge of two VendureConfig objects. Unlike `Object.assign()` the `target` object is
 Performs a deep merge of two VendureConfig objects. Unlike `Object.assign()` the `target` object is
 not mutated, instead the function returns a new object which is the result of deeply merging the
 not mutated, instead the function returns a new object which is the result of deeply merging the

+ 1 - 1
lerna.json

@@ -1,6 +1,6 @@
 {
 {
     "packages": ["packages/*"],
     "packages": ["packages/*"],
-    "version": "3.3.6",
+    "version": "3.3.7",
     "npmClient": "npm",
     "npmClient": "npm",
     "command": {
     "command": {
         "version": {
         "version": {

+ 65 - 65
package-lock.json

@@ -50315,7 +50315,7 @@
         },
         },
         "packages/admin-ui": {
         "packages/admin-ui": {
             "name": "@vendure/admin-ui",
             "name": "@vendure/admin-ui",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@angular/animations": "^19.2.4",
                 "@angular/animations": "^19.2.4",
@@ -50338,7 +50338,7 @@
                 "@ng-select/ng-select": "^14.2.6",
                 "@ng-select/ng-select": "^14.2.6",
                 "@ngx-translate/core": "^16.0.4",
                 "@ngx-translate/core": "^16.0.4",
                 "@ngx-translate/http-loader": "^16.0.1",
                 "@ngx-translate/http-loader": "^16.0.1",
-                "@vendure/common": "3.3.6",
+                "@vendure/common": "3.3.7",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "@webcomponents/custom-elements": "^1.6.0",
                 "apollo-angular": "^10.0.3",
                 "apollo-angular": "^10.0.3",
                 "apollo-upload-client": "^18.0.1",
                 "apollo-upload-client": "^18.0.1",
@@ -50409,7 +50409,7 @@
         },
         },
         "packages/admin-ui-plugin": {
         "packages/admin-ui-plugin": {
             "name": "@vendure/admin-ui-plugin",
             "name": "@vendure/admin-ui-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "date-fns": "^2.30.0",
                 "date-fns": "^2.30.0",
@@ -50419,9 +50419,9 @@
             "devDependencies": {
             "devDependencies": {
                 "@types/express": "^5.0.1",
                 "@types/express": "^5.0.1",
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/admin-ui": "3.3.6",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/admin-ui": "3.3.7",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "express": "^5.1.0",
                 "express": "^5.1.0",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
@@ -50493,7 +50493,7 @@
         },
         },
         "packages/asset-server-plugin": {
         "packages/asset-server-plugin": {
             "name": "@vendure/asset-server-plugin",
             "name": "@vendure/asset-server-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "file-type": "^19.0.0",
                 "file-type": "^19.0.0",
@@ -50506,8 +50506,8 @@
                 "@types/express": "^5.0.1",
                 "@types/express": "^5.0.1",
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
                 "@types/node-fetch": "^2.6.11",
                 "@types/node-fetch": "^2.6.11",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "express": "^5.1.0",
                 "express": "^5.1.0",
                 "node-fetch": "^2.7.0",
                 "node-fetch": "^2.7.0",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
@@ -50585,11 +50585,11 @@
         },
         },
         "packages/cli": {
         "packages/cli": {
             "name": "@vendure/cli",
             "name": "@vendure/cli",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "3.3.6",
+                "@vendure/common": "3.3.7",
                 "change-case": "^4.1.2",
                 "change-case": "^4.1.2",
                 "commander": "^11.0.0",
                 "commander": "^11.0.0",
                 "dotenv": "^16.4.5",
                 "dotenv": "^16.4.5",
@@ -50603,7 +50603,7 @@
                 "vendure": "dist/cli.js"
                 "vendure": "dist/cli.js"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/core": "3.3.6",
+                "@vendure/core": "3.3.7",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
             },
             },
             "funding": {
             "funding": {
@@ -50621,7 +50621,7 @@
         },
         },
         "packages/common": {
         "packages/common": {
             "name": "@vendure/common",
             "name": "@vendure/common",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
             "devDependencies": {
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
@@ -50633,7 +50633,7 @@
         },
         },
         "packages/core": {
         "packages/core": {
             "name": "@vendure/core",
             "name": "@vendure/core",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@apollo/server": "^4.11.3",
                 "@apollo/server": "^4.11.3",
@@ -50645,7 +50645,7 @@
                 "@nestjs/platform-express": "^11.0.12",
                 "@nestjs/platform-express": "^11.0.12",
                 "@nestjs/terminus": "^11.0.0",
                 "@nestjs/terminus": "^11.0.0",
                 "@nestjs/typeorm": "^11.0.0",
                 "@nestjs/typeorm": "^11.0.0",
-                "@vendure/common": "3.3.6",
+                "@vendure/common": "3.3.7",
                 "bcrypt": "^5.1.1",
                 "bcrypt": "^5.1.1",
                 "body-parser": "^1.20.2",
                 "body-parser": "^1.20.2",
                 "cookie-session": "^2.1.0",
                 "cookie-session": "^2.1.0",
@@ -50816,11 +50816,11 @@
         },
         },
         "packages/create": {
         "packages/create": {
             "name": "@vendure/create",
             "name": "@vendure/create",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@clack/prompts": "^0.7.0",
                 "@clack/prompts": "^0.7.0",
-                "@vendure/common": "3.3.6",
+                "@vendure/common": "3.3.7",
                 "commander": "^11.0.0",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "cross-spawn": "^7.0.3",
                 "fs-extra": "^11.2.0",
                 "fs-extra": "^11.2.0",
@@ -50838,7 +50838,7 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
                 "@types/semver": "^7.5.8",
-                "@vendure/core": "3.3.6",
+                "@vendure/core": "3.3.7",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
                 "ts-node": "^10.9.2",
                 "ts-node": "^10.9.2",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
@@ -50903,7 +50903,7 @@
         },
         },
         "packages/dashboard": {
         "packages/dashboard": {
             "name": "@vendure/dashboard",
             "name": "@vendure/dashboard",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "dependencies": {
             "dependencies": {
                 "@dnd-kit/core": "^6.3.1",
                 "@dnd-kit/core": "^6.3.1",
                 "@dnd-kit/modifiers": "^9.0.0",
                 "@dnd-kit/modifiers": "^9.0.0",
@@ -50946,8 +50946,8 @@
                 "@types/react-dom": "^19.0.4",
                 "@types/react-dom": "^19.0.4",
                 "@types/react-grid-layout": "^1.3.5",
                 "@types/react-grid-layout": "^1.3.5",
                 "@uidotdev/usehooks": "^2.4.1",
                 "@uidotdev/usehooks": "^2.4.1",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "@vitejs/plugin-react": "^4.3.4",
                 "@vitejs/plugin-react": "^4.3.4",
                 "acorn": "^8.11.3",
                 "acorn": "^8.11.3",
                 "acorn-walk": "^8.3.2",
                 "acorn-walk": "^8.3.2",
@@ -51321,21 +51321,21 @@
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/dev-server": {
         "packages/dev-server": {
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@nestjs/axios": "^4.0.0",
                 "@nestjs/axios": "^4.0.0",
-                "@vendure/admin-ui-plugin": "3.3.6",
-                "@vendure/asset-server-plugin": "3.3.6",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
-                "@vendure/elasticsearch-plugin": "3.3.6",
-                "@vendure/email-plugin": "3.3.6",
+                "@vendure/admin-ui-plugin": "3.3.7",
+                "@vendure/asset-server-plugin": "3.3.7",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
+                "@vendure/elasticsearch-plugin": "3.3.7",
+                "@vendure/email-plugin": "3.3.7",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/testing": "3.3.6",
-                "@vendure/ui-devkit": "3.3.6",
+                "@vendure/testing": "3.3.7",
+                "@vendure/ui-devkit": "3.3.7",
                 "commander": "^12.0.0",
                 "commander": "^12.0.0",
                 "concurrently": "^8.2.2",
                 "concurrently": "^8.2.2",
                 "csv-stringify": "^6.4.6",
                 "csv-stringify": "^6.4.6",
@@ -51355,7 +51355,7 @@
         },
         },
         "packages/elasticsearch-plugin": {
         "packages/elasticsearch-plugin": {
             "name": "@vendure/elasticsearch-plugin",
             "name": "@vendure/elasticsearch-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@elastic/elasticsearch": "~7.9.1",
                 "@elastic/elasticsearch": "~7.9.1",
@@ -51363,8 +51363,8 @@
                 "fast-deep-equal": "^3.1.3"
                 "fast-deep-equal": "^3.1.3"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
             },
             },
@@ -51374,7 +51374,7 @@
         },
         },
         "packages/email-plugin": {
         "packages/email-plugin": {
             "name": "@vendure/email-plugin",
             "name": "@vendure/email-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@types/nodemailer": "^6.4.9",
                 "@types/nodemailer": "^6.4.9",
@@ -51390,8 +51390,8 @@
                 "@types/express": "^5.0.1",
                 "@types/express": "^5.0.1",
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
                 "@types/mjml": "^4.7.4",
                 "@types/mjml": "^4.7.4",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
             },
             },
@@ -51401,7 +51401,7 @@
         },
         },
         "packages/graphiql-plugin": {
         "packages/graphiql-plugin": {
             "name": "@vendure/graphiql-plugin",
             "name": "@vendure/graphiql-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "express": "^5.1.0"
                 "express": "^5.1.0"
@@ -51411,8 +51411,8 @@
                 "@types/express": "^5.0.0",
                 "@types/express": "^5.0.0",
                 "@types/react": "^19.0.0",
                 "@types/react": "^19.0.0",
                 "@types/react-dom": "^19.0.0",
                 "@types/react-dom": "^19.0.0",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "@vitejs/plugin-react": "^4.4.1",
                 "@vitejs/plugin-react": "^4.4.1",
                 "graphiql": "^4.0.2",
                 "graphiql": "^4.0.2",
                 "react": "^19.0.0",
                 "react": "^19.0.0",
@@ -51445,14 +51445,14 @@
         },
         },
         "packages/harden-plugin": {
         "packages/harden-plugin": {
             "name": "@vendure/harden-plugin",
             "name": "@vendure/harden-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "graphql-query-complexity": "^0.12.0"
                 "graphql-query-complexity": "^0.12.0"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6"
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7"
             },
             },
             "funding": {
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -51460,12 +51460,12 @@
         },
         },
         "packages/job-queue-plugin": {
         "packages/job-queue-plugin": {
             "name": "@vendure/job-queue-plugin",
             "name": "@vendure/job-queue-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
             "devDependencies": {
                 "@google-cloud/pubsub": "^2.8.0",
                 "@google-cloud/pubsub": "^2.8.0",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "bullmq": "^5.4.2",
                 "bullmq": "^5.4.2",
                 "ioredis": "^5.3.2",
                 "ioredis": "^5.3.2",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
@@ -51493,7 +51493,7 @@
         },
         },
         "packages/payments-plugin": {
         "packages/payments-plugin": {
             "name": "@vendure/payments-plugin",
             "name": "@vendure/payments-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "currency.js": "2.0.4"
                 "currency.js": "2.0.4"
@@ -51502,9 +51502,9 @@
                 "@mollie/api-client": "^3.7.0",
                 "@mollie/api-client": "^3.7.0",
                 "@types/braintree": "^3.3.11",
                 "@types/braintree": "^3.3.11",
                 "@types/localtunnel": "2.0.4",
                 "@types/localtunnel": "2.0.4",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
-                "@vendure/testing": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
+                "@vendure/testing": "3.3.7",
                 "braintree": "^3.22.0",
                 "braintree": "^3.22.0",
                 "localtunnel": "2.0.2",
                 "localtunnel": "2.0.2",
                 "nock": "^13.1.4",
                 "nock": "^13.1.4",
@@ -51534,12 +51534,12 @@
         },
         },
         "packages/sentry-plugin": {
         "packages/sentry-plugin": {
             "name": "@vendure/sentry-plugin",
             "name": "@vendure/sentry-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "devDependencies": {
             "devDependencies": {
                 "@sentry/node": "^7.106.1",
                 "@sentry/node": "^7.106.1",
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6"
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7"
             },
             },
             "funding": {
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -51550,14 +51550,14 @@
         },
         },
         "packages/stellate-plugin": {
         "packages/stellate-plugin": {
             "name": "@vendure/stellate-plugin",
             "name": "@vendure/stellate-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "node-fetch": "^2.7.0"
                 "node-fetch": "^2.7.0"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6"
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7"
             },
             },
             "funding": {
             "funding": {
                 "url": "https://github.com/sponsors/michaelbromley"
                 "url": "https://github.com/sponsors/michaelbromley"
@@ -51565,7 +51565,7 @@
         },
         },
         "packages/telemetry-plugin": {
         "packages/telemetry-plugin": {
             "name": "@vendure/telemetry-plugin",
             "name": "@vendure/telemetry-plugin",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@opentelemetry/api": "^1.9.0",
                 "@opentelemetry/api": "^1.9.0",
@@ -51579,8 +51579,8 @@
                 "javascript-stringify": "^2.1.0"
                 "javascript-stringify": "^2.1.0"
             },
             },
             "devDependencies": {
             "devDependencies": {
-                "@vendure/common": "3.3.6",
-                "@vendure/core": "3.3.6",
+                "@vendure/common": "3.3.7",
+                "@vendure/core": "3.3.7",
                 "typescript": "5.8.2"
                 "typescript": "5.8.2"
             },
             },
             "funding": {
             "funding": {
@@ -51589,11 +51589,11 @@
         },
         },
         "packages/testing": {
         "packages/testing": {
             "name": "@vendure/testing",
             "name": "@vendure/testing",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@graphql-typed-document-node/core": "^3.2.0",
                 "@graphql-typed-document-node/core": "^3.2.0",
-                "@vendure/common": "3.3.6",
+                "@vendure/common": "3.3.7",
                 "faker": "^4.1.0",
                 "faker": "^4.1.0",
                 "form-data": "^4.0.0",
                 "form-data": "^4.0.0",
                 "graphql": "^16.10.0",
                 "graphql": "^16.10.0",
@@ -51606,7 +51606,7 @@
                 "@types/mysql": "^2.15.26",
                 "@types/mysql": "^2.15.26",
                 "@types/node-fetch": "^2.6.4",
                 "@types/node-fetch": "^2.6.4",
                 "@types/pg": "^8.11.2",
                 "@types/pg": "^8.11.2",
-                "@vendure/core": "3.3.6",
+                "@vendure/core": "3.3.7",
                 "mysql": "^2.18.1",
                 "mysql": "^2.18.1",
                 "pg": "^8.11.3",
                 "pg": "^8.11.3",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",
@@ -51618,15 +51618,15 @@
         },
         },
         "packages/ui-devkit": {
         "packages/ui-devkit": {
             "name": "@vendure/ui-devkit",
             "name": "@vendure/ui-devkit",
-            "version": "3.3.6",
+            "version": "3.3.7",
             "license": "GPL-3.0-or-later",
             "license": "GPL-3.0-or-later",
             "dependencies": {
             "dependencies": {
                 "@angular-devkit/build-angular": "^19.2.5",
                 "@angular-devkit/build-angular": "^19.2.5",
                 "@angular/cli": "^19.2.5",
                 "@angular/cli": "^19.2.5",
                 "@angular/compiler": "^19.2.4",
                 "@angular/compiler": "^19.2.4",
                 "@angular/compiler-cli": "^19.2.4",
                 "@angular/compiler-cli": "^19.2.4",
-                "@vendure/admin-ui": "3.3.6",
-                "@vendure/common": "3.3.6",
+                "@vendure/admin-ui": "3.3.7",
+                "@vendure/common": "3.3.7",
                 "chalk": "^4.1.0",
                 "chalk": "^4.1.0",
                 "chokidar": "^3.6.0",
                 "chokidar": "^3.6.0",
                 "fs-extra": "^11.2.0",
                 "fs-extra": "^11.2.0",
@@ -51637,7 +51637,7 @@
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-node-resolve": "^15.2.3",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@rollup/plugin-terser": "^0.4.4",
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
-                "@vendure/core": "3.3.6",
+                "@vendure/core": "3.3.7",
                 "react": "^19.0.0",
                 "react": "^19.0.0",
                 "react-dom": "^19.0.0",
                 "react-dom": "^19.0.0",
                 "rimraf": "^5.0.5",
                 "rimraf": "^5.0.5",

+ 4 - 4
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/admin-ui-plugin",
     "name": "@vendure/admin-ui-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
     "files": [
     "files": [
@@ -25,9 +25,9 @@
     "devDependencies": {
     "devDependencies": {
         "@types/express": "^5.0.1",
         "@types/express": "^5.0.1",
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/admin-ui": "3.3.6",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/admin-ui": "3.3.7",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "express": "^5.1.0",
         "express": "^5.1.0",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",
         "typescript": "5.8.2"
         "typescript": "5.8.2"

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

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/admin-ui",
     "name": "@vendure/admin-ui",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
@@ -53,7 +53,7 @@
         "@ng-select/ng-select": "^14.2.6",
         "@ng-select/ng-select": "^14.2.6",
         "@ngx-translate/core": "^16.0.4",
         "@ngx-translate/core": "^16.0.4",
         "@ngx-translate/http-loader": "^16.0.1",
         "@ngx-translate/http-loader": "^16.0.1",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "@webcomponents/custom-elements": "^1.6.0",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^10.0.3",
         "apollo-angular": "^10.0.3",
         "apollo-upload-client": "^18.0.1",
         "apollo-upload-client": "^18.0.1",

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '3.3.6';
+export const ADMIN_UI_VERSION = '3.3.7';

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/asset-server-plugin",
     "name": "@vendure/asset-server-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
     "files": [
     "files": [
@@ -30,8 +30,8 @@
         "@types/express": "^5.0.1",
         "@types/express": "^5.0.1",
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
         "@types/node-fetch": "^2.6.11",
         "@types/node-fetch": "^2.6.11",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "express": "^5.1.0",
         "express": "^5.1.0",
         "node-fetch": "^2.7.0",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",

+ 3 - 3
packages/cli/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/cli",
     "name": "@vendure/cli",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "A modern, headless ecommerce framework",
     "description": "A modern, headless ecommerce framework",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
@@ -36,7 +36,7 @@
     ],
     ],
     "dependencies": {
     "dependencies": {
         "@clack/prompts": "^0.7.0",
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "change-case": "^4.1.2",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
         "commander": "^11.0.0",
         "dotenv": "^16.4.5",
         "dotenv": "^16.4.5",
@@ -47,7 +47,7 @@
         "tsconfig-paths": "^4.2.0"
         "tsconfig-paths": "^4.2.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/core": "3.3.6",
+        "@vendure/core": "3.3.7",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     }
     }
 }
 }

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/common",
     "name": "@vendure/common",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "index.js",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "repository": {
     "repository": {

+ 278 - 0
packages/core/e2e/custom-field-default-values.e2e-spec.ts

@@ -0,0 +1,278 @@
+import { createTestEnvironment } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+
+/**
+ * Tests for GitHub issue #3266: Custom field default values not applied when explicitly set to null
+ * https://github.com/vendure-ecommerce/vendure/issues/3266
+ */
+
+const customConfig = {
+    ...testConfig(),
+    customFields: {
+        Product: [
+            { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
+            { name: 'intWithDefault', type: 'int', defaultValue: 5 },
+            { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
+        ],
+        Customer: [
+            { name: 'stringWithDefault', type: 'string', defaultValue: 'customer-default' },
+            { name: 'intWithDefault', type: 'int', defaultValue: 100 },
+            { name: 'booleanWithDefault', type: 'boolean', defaultValue: false },
+        ],
+    },
+};
+
+describe('Custom field default values', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
+
+    const CREATE_CUSTOMER = gql`
+        mutation CreateCustomer($input: CreateCustomerInput!) {
+            createCustomer(input: $input) {
+                ... on Customer {
+                    id
+                    firstName
+                    lastName
+                    emailAddress
+                    customFields {
+                        stringWithDefault
+                        intWithDefault
+                        booleanWithDefault
+                    }
+                }
+                ... on ErrorResult {
+                    errorCode
+                    message
+                }
+            }
+        }
+    `;
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('translatable entity (Product)', () => {
+        it('should apply default values when creating product without custom fields', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product 1"
+                                    slug: "test-product-1"
+                                    description: "Test"
+                                }
+                            ]
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should apply default values when creating product with empty custom fields', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product 2"
+                                    slug: "test-product-2"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {}
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should apply default values when custom fields are explicitly set to null', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product Null"
+                                    slug: "test-product-null"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {
+                                stringWithDefault: null
+                                intWithDefault: null
+                                booleanWithDefault: null
+                            }
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            // This is the core issue: when custom fields are explicitly set to null,
+            // they should still get their default values
+            expect(createProduct.customFields.stringWithDefault).toBe('hello');
+            expect(createProduct.customFields.intWithDefault).toBe(5);
+            expect(createProduct.customFields.booleanWithDefault).toBe(true);
+        });
+
+        it('should not override explicitly provided values', async () => {
+            const { createProduct } = await adminClient.query(gql`
+                mutation {
+                    createProduct(
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: en
+                                    name: "Test Product Custom"
+                                    slug: "test-product-custom"
+                                    description: "Test"
+                                }
+                            ]
+                            customFields: {
+                                stringWithDefault: "custom value"
+                                intWithDefault: 999
+                                booleanWithDefault: false
+                            }
+                        }
+                    ) {
+                        id
+                        name
+                        customFields {
+                            stringWithDefault
+                            intWithDefault
+                            booleanWithDefault
+                        }
+                    }
+                }
+            `);
+
+            // When explicit values are provided, they should be used instead of defaults
+            expect(createProduct.customFields.stringWithDefault).toBe('custom value');
+            expect(createProduct.customFields.intWithDefault).toBe(999);
+            expect(createProduct.customFields.booleanWithDefault).toBe(false);
+        });
+    });
+
+    describe('non-translatable entity (Customer)', () => {
+        it('should apply default values when creating customer without custom fields', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'John',
+                    lastName: 'Doe',
+                    emailAddress: 'john.doe@example.com',
+                },
+            });
+
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should apply default values when creating customer with empty custom fields', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Jane',
+                    lastName: 'Smith',
+                    emailAddress: 'jane.smith@example.com',
+                    customFields: {},
+                },
+            });
+
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should apply default values when custom fields are explicitly set to null', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Bob',
+                    lastName: 'Johnson',
+                    emailAddress: 'bob.johnson@example.com',
+                    customFields: {
+                        stringWithDefault: null,
+                        intWithDefault: null,
+                        booleanWithDefault: null,
+                    },
+                },
+            });
+
+            // This should reproduce the issue for non-translatable entities
+            expect(createCustomer.customFields.stringWithDefault).toBe('customer-default');
+            expect(createCustomer.customFields.intWithDefault).toBe(100);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(false);
+        });
+
+        it('should not override explicitly provided values', async () => {
+            const { createCustomer } = await adminClient.query(CREATE_CUSTOMER, {
+                input: {
+                    firstName: 'Alice',
+                    lastName: 'Wilson',
+                    emailAddress: 'alice.wilson@example.com',
+                    customFields: {
+                        stringWithDefault: 'custom customer value',
+                        intWithDefault: 777,
+                        booleanWithDefault: true,
+                    },
+                },
+            });
+
+            // When explicit values are provided, they should be used instead of defaults
+            expect(createCustomer.customFields.stringWithDefault).toBe('custom customer value');
+            expect(createCustomer.customFields.intWithDefault).toBe(777);
+            expect(createCustomer.customFields.booleanWithDefault).toBe(true);
+        });
+    });
+});

+ 1 - 1
packages/core/e2e/list-query-builder.e2e-spec.ts

@@ -976,7 +976,7 @@ describe('ListQueryBuilder', () => {
                                 description: { contains: 'e' },
                                 description: { contains: 'e' },
                             },
                             },
                             {
                             {
-                                _or: [{ active: { eq: false } }, { ownerId: { eq: '10' } }],
+                                _or: [{ active: { eq: false } }, { ownerId: { eq: 'T_10' } }],
                             },
                             },
                         ],
                         ],
                     },
                     },

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/core",
     "name": "@vendure/core",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "A modern, headless ecommerce framework",
     "description": "A modern, headless ecommerce framework",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
@@ -49,7 +49,7 @@
         "@nestjs/platform-express": "^11.0.12",
         "@nestjs/platform-express": "^11.0.12",
         "@nestjs/terminus": "^11.0.0",
         "@nestjs/terminus": "^11.0.0",
         "@nestjs/typeorm": "^11.0.0",
         "@nestjs/typeorm": "^11.0.0",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "bcrypt": "^5.1.1",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "body-parser": "^1.20.2",
         "cookie-session": "^2.1.0",
         "cookie-session": "^2.1.0",

+ 3 - 2
packages/core/src/api/api.module.ts

@@ -11,10 +11,10 @@ import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-m
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { VENDURE_ADMIN_API_TYPE_PATHS, VENDURE_SHOP_API_TYPE_PATHS } from './constants';
 import { VENDURE_ADMIN_API_TYPE_PATHS, VENDURE_SHOP_API_TYPE_PATHS } from './constants';
 import { AuthGuard } from './middleware/auth-guard';
 import { AuthGuard } from './middleware/auth-guard';
+import { CustomFieldProcessingInterceptor } from './middleware/custom-field-processing-interceptor';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { IdInterceptor } from './middleware/id-interceptor';
 import { IdInterceptor } from './middleware/id-interceptor';
 import { TranslateErrorResultInterceptor } from './middleware/translate-error-result-interceptor';
 import { TranslateErrorResultInterceptor } from './middleware/translate-error-result-interceptor';
-import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fields-interceptor';
 
 
 /**
 /**
  * The ApiModule is responsible for the public API of the application. This is where requests
  * The ApiModule is responsible for the public API of the application. This is where requests
@@ -60,7 +60,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         },
         },
         {
         {
             provide: APP_INTERCEPTOR,
             provide: APP_INTERCEPTOR,
-            useClass: ValidateCustomFieldsInterceptor,
+            useClass: CustomFieldProcessingInterceptor,
         },
         },
         {
         {
             provide: APP_INTERCEPTOR,
             provide: APP_INTERCEPTOR,
@@ -74,6 +74,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
 })
 })
 export class ApiModule implements NestModule {
 export class ApiModule implements NestModule {
     constructor(private configService: ConfigService) {}
     constructor(private configService: ConfigService) {}
+
     async configure(consumer: MiddlewareConsumer) {
     async configure(consumer: MiddlewareConsumer) {
         const { adminApiPath, shopApiPath } = this.configService.apiOptions;
         const { adminApiPath, shopApiPath } = this.configService.apiOptions;
         const { uploadMaxFileSize } = this.configService.assetOptions;
         const { uploadMaxFileSize } = this.configService.assetOptions;

+ 22 - 1
packages/core/src/api/common/graphql-value-transformer.ts

@@ -187,12 +187,33 @@ export class GraphqlValueTransformer {
     private getChildrenTreeNodes(
     private getChildrenTreeNodes(
         inputType: GraphQLInputObjectType,
         inputType: GraphQLInputObjectType,
         parent: TypeTreeNode,
         parent: TypeTreeNode,
+        depth = 0,
     ): { [name: string]: TypeTreeNode } {
     ): { [name: string]: TypeTreeNode } {
+        if (depth > 3) return {};
+
         return Object.entries(inputType.getFields()).reduce(
         return Object.entries(inputType.getFields()).reduce(
             (result, [key, field]) => {
             (result, [key, field]) => {
                 const namedType = getNamedType(field.type);
                 const namedType = getNamedType(field.type);
                 if (namedType === parent.type) {
                 if (namedType === parent.type) {
-                    // prevent recursion-induced stack overflow
+                    // Allow _and/_or self-references in filter types, but limit depth to prevent infinite loops
+                    if (key === '_and' || key === '_or') {
+                        const selfRefChild: TypeTreeNode = {
+                            type: namedType,
+                            isList: this.isList(field.type),
+                            parent,
+                            fragmentRefs: [],
+                            children: {},
+                        };
+                        if (isInputObjectType(namedType)) {
+                            selfRefChild.children = this.getChildrenTreeNodes(
+                                namedType,
+                                selfRefChild,
+                                depth + 1,
+                            );
+                        }
+                        result[key] = selfRefChild;
+                        return result;
+                    }
                     return result;
                     return result;
                 }
                 }
                 const child: TypeTreeNode = {
                 const child: TypeTreeNode = {

+ 288 - 0
packages/core/src/api/middleware/custom-field-processing-interceptor.ts

@@ -0,0 +1,288 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { getGraphQlInputName } from '@vendure/common/lib/shared-utils';
+import {
+    getNamedType,
+    GraphQLSchema,
+    OperationDefinitionNode,
+    TypeInfo,
+    visit,
+    visitWithTypeInfo,
+} from 'graphql';
+
+import { Injector } from '../../common/injector';
+import { ConfigService } from '../../config/config.service';
+import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
+import { parseContext } from '../common/parse-context';
+import { internal_getRequestContext, RequestContext } from '../common/request-context';
+import { validateCustomFieldValue } from '../common/validate-custom-field-value';
+
+/**
+ * @description
+ * Unified interceptor that processes custom fields in GraphQL mutations by:
+ *
+ * 1. Applying default values when fields are explicitly set to null (create operations only)
+ * 2. Validating custom field values according to their constraints
+ *
+ * Uses native GraphQL utilities (visit, visitWithTypeInfo, getNamedType) for efficient
+ * AST traversal and type analysis.
+ */
+@Injectable()
+export class CustomFieldProcessingInterceptor implements NestInterceptor {
+    private readonly createInputsWithCustomFields = new Set<string>();
+    private readonly updateInputsWithCustomFields = new Set<string>();
+
+    constructor(
+        private readonly configService: ConfigService,
+        private readonly moduleRef: ModuleRef,
+    ) {
+        Object.keys(configService.customFields).forEach(entityName => {
+            this.createInputsWithCustomFields.add(`Create${entityName}Input`);
+            this.updateInputsWithCustomFields.add(`Update${entityName}Input`);
+        });
+        // Note: OrderLineCustomFieldsInput is handled separately since it's used in both
+        // create operations (addItemToOrder) and update operations (adjustOrderLine)
+    }
+
+    async intercept(context: ExecutionContext, next: CallHandler<any>) {
+        const parsedContext = parseContext(context);
+
+        if (!parsedContext.isGraphQL) {
+            return next.handle();
+        }
+
+        const { operation, schema } = parsedContext.info;
+        if (operation.operation === 'mutation') {
+            await this.processMutationCustomFields(context, operation, schema);
+        }
+
+        return next.handle();
+    }
+
+    private async processMutationCustomFields(
+        context: ExecutionContext,
+        operation: OperationDefinitionNode,
+        schema: GraphQLSchema,
+    ) {
+        const gqlExecutionContext = GqlExecutionContext.create(context);
+        const variables = gqlExecutionContext.getArgs();
+        const ctx = internal_getRequestContext(parseContext(context).req);
+        const injector = new Injector(this.moduleRef);
+
+        const inputTypeNames = this.getArgumentMap(operation, schema);
+
+        for (const [inputName, typeName] of Object.entries(inputTypeNames)) {
+            if (this.hasCustomFields(typeName) && variables[inputName]) {
+                await this.processInputVariables(typeName, variables[inputName], ctx, injector, operation);
+            }
+        }
+    }
+
+    private hasCustomFields(typeName: string): boolean {
+        return (
+            this.createInputsWithCustomFields.has(typeName) ||
+            this.updateInputsWithCustomFields.has(typeName) ||
+            typeName === 'OrderLineCustomFieldsInput'
+        );
+    }
+
+    private async processInputVariables(
+        typeName: string,
+        variableInput: any,
+        ctx: RequestContext,
+        injector: Injector,
+        operation: OperationDefinitionNode,
+    ) {
+        const inputVariables = Array.isArray(variableInput) ? variableInput : [variableInput];
+        const shouldApplyDefaults = this.shouldApplyDefaults(typeName, operation);
+
+        for (const inputVariable of inputVariables) {
+            if (shouldApplyDefaults) {
+                this.applyDefaultsToInput(typeName, inputVariable);
+            }
+            await this.validateInput(typeName, ctx, injector, inputVariable);
+        }
+    }
+
+    private shouldApplyDefaults(typeName: string, operation: OperationDefinitionNode): boolean {
+        // For regular create inputs, always apply defaults
+        if (this.createInputsWithCustomFields.has(typeName)) {
+            return true;
+        }
+
+        // For OrderLineCustomFieldsInput, check the actual mutation name
+        if (typeName === 'OrderLineCustomFieldsInput') {
+            return this.isOrderLineCreateOperation(operation);
+        }
+
+        // For update inputs, never apply defaults
+        return false;
+    }
+
+    private isOrderLineCreateOperation(operation: OperationDefinitionNode): boolean {
+        // Check if any field in the operation is a "create/add" operation for order lines
+        for (const selection of operation.selectionSet.selections) {
+            if (selection.kind === 'Field') {
+                const fieldName = selection.name.value;
+                // These mutations create new order lines, so should apply defaults
+                if (fieldName === 'addItemToOrder' || fieldName === 'addItemToDraftOrder') {
+                    return true;
+                }
+                // These mutations modify existing order lines, so should NOT apply defaults
+                if (fieldName === 'adjustOrderLine' || fieldName === 'adjustDraftOrderLine') {
+                    return false;
+                }
+            }
+        }
+        // Default to false for safety (don't apply defaults unless we're sure it's a create)
+        return false;
+    }
+
+    private getArgumentMap(
+        operation: OperationDefinitionNode,
+        schema: GraphQLSchema,
+    ): { [inputName: string]: string } {
+        const typeInfo = new TypeInfo(schema);
+        const map: { [inputName: string]: string } = {};
+
+        const visitor = {
+            enter(node: any) {
+                if (node.kind === 'Field') {
+                    const fieldDef = typeInfo.getFieldDef();
+                    if (fieldDef) {
+                        for (const arg of fieldDef.args) {
+                            map[arg.name] = getNamedType(arg.type).name;
+                        }
+                    }
+                }
+            },
+        };
+
+        visit(operation, visitWithTypeInfo(typeInfo, visitor));
+        return map;
+    }
+
+    private applyDefaultsToInput(typeName: string, variableValues: any) {
+        if (typeName === 'OrderLineCustomFieldsInput') {
+            this.applyDefaultsForOrderLine(variableValues);
+        } else {
+            this.applyDefaultsForEntity(typeName, variableValues);
+        }
+    }
+
+    private applyDefaultsForOrderLine(variableValues: any) {
+        const orderLineConfig = this.configService.customFields.OrderLine || [];
+        this.applyDefaultsToCustomFieldsObject(orderLineConfig, variableValues);
+    }
+
+    private applyDefaultsForEntity(typeName: string, variableValues: any) {
+        const entityName = this.getEntityNameFromInputType(typeName);
+        const customFieldConfig = this.configService.customFields[entityName];
+
+        if (!customFieldConfig) {
+            return;
+        }
+
+        this.applyDefaultsToDirectCustomFields(customFieldConfig, variableValues);
+        this.applyDefaultsToTranslationCustomFields(customFieldConfig, variableValues);
+    }
+
+    private applyDefaultsToDirectCustomFields(customFieldConfig: any[], variableValues: any) {
+        if (variableValues.customFields) {
+            this.applyDefaultsToCustomFieldsObject(customFieldConfig, variableValues.customFields);
+        }
+    }
+
+    private applyDefaultsToTranslationCustomFields(customFieldConfig: any[], variableValues: any) {
+        if (!variableValues.translations || !Array.isArray(variableValues.translations)) {
+            return;
+        }
+
+        for (const translation of variableValues.translations) {
+            if (translation.customFields) {
+                this.applyDefaultsToCustomFieldsObject(customFieldConfig, translation.customFields);
+            }
+        }
+    }
+
+    private applyDefaultsToCustomFieldsObject(customFieldConfig: any[], customFieldsObject: any) {
+        for (const config of customFieldConfig) {
+            const fieldName = getGraphQlInputName(config);
+            // Only apply default if the field is explicitly null and has a default value
+            if (customFieldsObject[fieldName] === null && config.defaultValue !== undefined) {
+                customFieldsObject[fieldName] = config.defaultValue;
+            }
+        }
+    }
+
+    private getEntityNameFromInputType(typeName: string): string {
+        // Remove "Create" or "Update" prefix and "Input" suffix
+        // e.g., "CreateProductInput" -> "Product", "UpdateCustomerInput" -> "Customer"
+        if (typeName.startsWith('Create')) {
+            return typeName.slice(6, -5); // Remove "Create" and "Input"
+        }
+        if (typeName.startsWith('Update')) {
+            return typeName.slice(6, -5); // Remove "Update" and "Input"
+        }
+        return typeName;
+    }
+
+    private async validateInput(
+        typeName: string,
+        ctx: RequestContext,
+        injector: Injector,
+        variableValues?: { [key: string]: any },
+    ) {
+        if (variableValues) {
+            const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
+            const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
+
+            if (typeName === 'OrderLineCustomFieldsInput') {
+                // special case needed to handle custom fields passed via addItemToOrder or adjustOrderLine
+                // mutations.
+                await this.validateCustomFieldsObject(
+                    this.configService.customFields.OrderLine,
+                    ctx,
+                    variableValues,
+                    injector,
+                );
+            }
+            if (variableValues.customFields) {
+                await this.validateCustomFieldsObject(
+                    customFieldConfig,
+                    ctx,
+                    variableValues.customFields,
+                    injector,
+                );
+            }
+            const translations = variableValues.translations;
+            if (Array.isArray(translations)) {
+                for (const translation of translations) {
+                    if (translation.customFields) {
+                        await this.validateCustomFieldsObject(
+                            customFieldConfig,
+                            ctx,
+                            translation.customFields,
+                            injector,
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    private async validateCustomFieldsObject(
+        customFieldConfig: CustomFieldConfig[],
+        ctx: RequestContext,
+        customFieldsObject: { [key: string]: any },
+        injector: Injector,
+    ) {
+        for (const [key, value] of Object.entries(customFieldsObject)) {
+            const config = customFieldConfig.find(c => getGraphQlInputName(c) === key);
+            if (config) {
+                await validateCustomFieldValue(config, value, injector, ctx);
+            }
+        }
+    }
+}

+ 0 - 172
packages/core/src/api/middleware/validate-custom-fields-interceptor.ts

@@ -1,172 +0,0 @@
-import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
-import { ModuleRef } from '@nestjs/core';
-import { GqlExecutionContext } from '@nestjs/graphql';
-import { getGraphQlInputName } from '@vendure/common/lib/shared-utils';
-import {
-    GraphQLInputType,
-    GraphQLList,
-    GraphQLNonNull,
-    GraphQLSchema,
-    OperationDefinitionNode,
-    TypeNode,
-} from 'graphql';
-
-import { Injector } from '../../common/injector';
-import { ConfigService } from '../../config/config.service';
-import { CustomFieldConfig, CustomFields } from '../../config/custom-field/custom-field-types';
-import { parseContext } from '../common/parse-context';
-import { internal_getRequestContext, RequestContext } from '../common/request-context';
-import { validateCustomFieldValue } from '../common/validate-custom-field-value';
-
-/**
- * This interceptor is responsible for enforcing the validation constraints defined for any CustomFields.
- * For example, if a custom 'int' field has a "min" value of 0, and a mutation attempts to set its value
- * to a negative integer, then that mutation will fail with an error.
- */
-@Injectable()
-export class ValidateCustomFieldsInterceptor implements NestInterceptor {
-    private readonly inputsWithCustomFields: Set<string>;
-
-    constructor(
-        private configService: ConfigService,
-        private moduleRef: ModuleRef,
-    ) {
-        this.inputsWithCustomFields = Object.keys(configService.customFields).reduce((inputs, entityName) => {
-            inputs.add(`Create${entityName}Input`);
-            inputs.add(`Update${entityName}Input`);
-            return inputs;
-        }, new Set<string>());
-        this.inputsWithCustomFields.add('OrderLineCustomFieldsInput');
-    }
-
-    async intercept(context: ExecutionContext, next: CallHandler<any>) {
-        const parsedContext = parseContext(context);
-        const injector = new Injector(this.moduleRef);
-        if (parsedContext.isGraphQL) {
-            const gqlExecutionContext = GqlExecutionContext.create(context);
-            const { operation, schema } = parsedContext.info;
-            const variables = gqlExecutionContext.getArgs();
-            const ctx = internal_getRequestContext(parsedContext.req);
-
-            if (operation.operation === 'mutation') {
-                const inputTypeNames = this.getArgumentMap(operation, schema);
-                for (const [inputName, typeName] of Object.entries(inputTypeNames)) {
-                    if (this.inputsWithCustomFields.has(typeName)) {
-                        if (variables[inputName]) {
-                            const inputVariables: Array<Record<string, any>> = Array.isArray(
-                                variables[inputName],
-                            )
-                                ? variables[inputName]
-                                : [variables[inputName]];
-
-                            for (const inputVariable of inputVariables) {
-                                await this.validateInput(typeName, ctx, injector, inputVariable);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        return next.handle();
-    }
-
-    private async validateInput(
-        typeName: string,
-        ctx: RequestContext,
-        injector: Injector,
-        variableValues?: { [key: string]: any },
-    ) {
-        if (variableValues) {
-            const entityName = typeName.replace(/(Create|Update)(.+)Input/, '$2');
-            const customFieldConfig = this.configService.customFields[entityName as keyof CustomFields];
-            if (typeName === 'OrderLineCustomFieldsInput') {
-                // special case needed to handle custom fields passed via addItemToOrder or adjustOrderLine
-                // mutations.
-                await this.validateCustomFieldsObject(
-                    this.configService.customFields.OrderLine,
-                    ctx,
-                    variableValues,
-                    injector,
-                );
-            }
-            if (variableValues.customFields) {
-                await this.validateCustomFieldsObject(
-                    customFieldConfig,
-                    ctx,
-                    variableValues.customFields,
-                    injector,
-                );
-            }
-            const translations = variableValues.translations;
-            if (Array.isArray(translations)) {
-                for (const translation of translations) {
-                    if (translation.customFields) {
-                        await this.validateCustomFieldsObject(
-                            customFieldConfig,
-                            ctx,
-                            translation.customFields,
-                            injector,
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    private async validateCustomFieldsObject(
-        customFieldConfig: CustomFieldConfig[],
-        ctx: RequestContext,
-        customFieldsObject: { [key: string]: any },
-        injector: Injector,
-    ) {
-        for (const [key, value] of Object.entries(customFieldsObject)) {
-            const config = customFieldConfig.find(c => getGraphQlInputName(c) === key);
-            if (config) {
-                await validateCustomFieldValue(config, value, injector, ctx);
-            }
-        }
-    }
-
-    private getArgumentMap(
-        operation: OperationDefinitionNode,
-        schema: GraphQLSchema,
-    ): { [inputName: string]: string } {
-        const mutationType = schema.getMutationType();
-        if (!mutationType) {
-            return {};
-        }
-        const map: { [inputName: string]: string } = {};
-
-        for (const selection of operation.selectionSet.selections) {
-            if (selection.kind === 'Field') {
-                const name = selection.name.value;
-
-                const inputType = mutationType.getFields()[name];
-                if (!inputType) continue;
-
-                for (const arg of inputType.args) {
-                    map[arg.name] = this.getInputTypeName(arg.type);
-                }
-            }
-        }
-        return map;
-    }
-
-    private getNamedTypeName(type: TypeNode): string {
-        if (type.kind === 'NonNullType' || type.kind === 'ListType') {
-            return this.getNamedTypeName(type.type);
-        } else {
-            return type.name.value;
-        }
-    }
-
-    private getInputTypeName(type: GraphQLInputType): string {
-        if (type instanceof GraphQLNonNull) {
-            return this.getInputTypeName(type.ofType);
-        }
-        if (type instanceof GraphQLList) {
-            return this.getInputTypeName(type.ofType);
-        }
-        return type.name;
-    }
-}

+ 51 - 2
packages/core/src/common/self-refreshing-cache.ts

@@ -1,10 +1,11 @@
-import { Json } from '@vendure/common/lib/shared-types';
-
 import { Logger } from '../config/logger/vendure-logger';
 import { Logger } from '../config/logger/vendure-logger';
 
 
 /**
 /**
  * @description
  * @description
  * A cache which automatically refreshes itself if the value is found to be stale.
  * A cache which automatically refreshes itself if the value is found to be stale.
+ *
+ * @docsCategory cache
+ * @docsPage SelfRefreshingCache
  */
  */
 export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
 export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
     /**
     /**
@@ -35,9 +36,31 @@ export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
     refresh(...args: RefreshArgs): Promise<V>;
     refresh(...args: RefreshArgs): Promise<V>;
 }
 }
 
 
+/**
+ * @description
+ * Configuration options for creating a {@link SelfRefreshingCache}.
+ *
+ * @docsCategory cache
+ * @docsPage SelfRefreshingCache
+ */
 export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
 export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
+    /**
+     * @description
+     * The name of the cache, used for logging purposes.
+     * e.g. `'MyService.cachedValue'`.
+     */
     name: string;
     name: string;
+    /**
+     * @description
+     * The time-to-live (ttl) in milliseconds for the cache. After this time, the value will be considered stale
+     * and will be refreshed the next time it is accessed.
+     */
     ttl: number;
     ttl: number;
+    /**
+     * @description
+     * The function which is used to refresh the value of the cache.
+     * This function should return a Promise which resolves to the new value.
+     */
     refresh: {
     refresh: {
         fn: (...args: RefreshArgs) => Promise<V>;
         fn: (...args: RefreshArgs) => Promise<V>;
         /**
         /**
@@ -46,6 +69,7 @@ export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
         defaultArgs: RefreshArgs;
         defaultArgs: RefreshArgs;
     };
     };
     /**
     /**
+     * @description
      * Intended for unit testing the SelfRefreshingCache only.
      * Intended for unit testing the SelfRefreshingCache only.
      * By default uses `() => new Date().getTime()`
      * By default uses `() => new Date().getTime()`
      */
      */
@@ -61,6 +85,31 @@ export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
  * From there, when the `.value` property is accessed, it will return a value from the cache, and if the
  * From there, when the `.value` property is accessed, it will return a value from the cache, and if the
  * value has expired, it will automatically run the `refreshFn` to update the value and then return the
  * value has expired, it will automatically run the `refreshFn` to update the value and then return the
  * fresh value.
  * fresh value.
+ *
+ * @example
+ * ```ts title="Example of creating a SelfRefreshingCache"
+ * import { createSelfRefreshingCache } from '@vendure/core';
+ *
+ * \@Injectable()
+ * export class PublicChannelService {
+ *   private publicChannel: SelfRefreshingCache<Channel, [RequestContext]>;
+ *
+ *   async init() {
+ *     this.publicChannel = await createSelfRefreshingCache<Channel, [RequestContext]>({
+ *      name: 'PublicChannelService.publicChannel',
+ *      ttl: 1000 * 60 * 60, // 1 hour
+ *      refresh: {
+ *        fn: async (ctx: RequestContext) => {
+ *         return this.channelService.getPublicChannel(ctx);
+ *       },
+ *      defaultArgs: [RequestContext.empty()],
+ *     },
+ *   });
+ * }
+ * ```
+ *
+ * @docsCategory cache
+ * @docsPage SelfRefreshingCache
  */
  */
 export async function createSelfRefreshingCache<V, RefreshArgs extends any[]>(
 export async function createSelfRefreshingCache<V, RefreshArgs extends any[]>(
     config: SelfRefreshingCacheConfig<V, RefreshArgs>,
     config: SelfRefreshingCacheConfig<V, RefreshArgs>,

+ 1 - 2
packages/core/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -1,14 +1,12 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { ID, Type } from '@vendure/common/lib/shared-types';
 import { ID, Type } from '@vendure/common/lib/shared-types';
-import { FindOneOptions } from 'typeorm';
 import { FindManyOptions } from 'typeorm/find-options/FindManyOptions';
 import { FindManyOptions } from 'typeorm/find-options/FindManyOptions';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { Translatable, TranslatedInput, Translation } from '../../../common/types/locale-types';
 import { Translatable, TranslatedInput, Translation } from '../../../common/types/locale-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { patchEntity } from '../utils/patch-entity';
 import { patchEntity } from '../utils/patch-entity';
 
 
 import { TranslationDiffer } from './translation-differ';
 import { TranslationDiffer } from './translation-differ';
@@ -67,6 +65,7 @@ export class TranslatableSaver {
         const { ctx, entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
         const { ctx, entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
 
 
         const entity = new entityType(input);
         const entity = new entityType(input);
+
         const translations: Array<Translation<T>> = [];
         const translations: Array<Translation<T>> = [];
 
 
         if (input.translations) {
         if (input.translations) {

+ 4 - 3
packages/core/src/service/services/order.service.ts

@@ -1568,12 +1568,13 @@ export class OrderService {
             .getMany();
             .getMany();
 
 
         for (const inputLine of input.lines) {
         for (const inputLine of input.lines) {
-            const existingFulfillmentLine = existingFulfillmentLines.find(l =>
+            const fulfillmentLinesForOrderLine = existingFulfillmentLines.filter(l =>
                 idsAreEqual(l.orderLineId, inputLine.orderLineId),
                 idsAreEqual(l.orderLineId, inputLine.orderLineId),
             );
             );
-            if (existingFulfillmentLine) {
+            if (fulfillmentLinesForOrderLine.length) {
+                const fulfilledQuantity = summate(fulfillmentLinesForOrderLine, 'quantity');
                 const unfulfilledQuantity =
                 const unfulfilledQuantity =
-                    existingFulfillmentLine.orderLine.quantity - existingFulfillmentLine.quantity;
+                    fulfillmentLinesForOrderLine[0].orderLine.quantity - fulfilledQuantity;
                 if (unfulfilledQuantity < inputLine.quantity) {
                 if (unfulfilledQuantity < inputLine.quantity) {
                     return true;
                     return true;
                 }
                 }

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/create",
     "name": "@vendure/create",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "bin": {
     "bin": {
         "create": "./index.js"
         "create": "./index.js"
@@ -31,14 +31,14 @@
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
         "@types/semver": "^7.5.8",
-        "@vendure/core": "3.3.6",
+        "@vendure/core": "3.3.7",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",
         "ts-node": "^10.9.2",
         "ts-node": "^10.9.2",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     },
     },
     "dependencies": {
     "dependencies": {
         "@clack/prompts": "^0.7.0",
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "commander": "^11.0.0",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "cross-spawn": "^7.0.3",
         "fs-extra": "^11.2.0",
         "fs-extra": "^11.2.0",

+ 3 - 3
packages/dashboard/package.json

@@ -1,7 +1,7 @@
 {
 {
     "name": "@vendure/dashboard",
     "name": "@vendure/dashboard",
     "private": false,
     "private": false,
-    "version": "3.3.6",
+    "version": "3.3.7",
     "type": "module",
     "type": "module",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
@@ -86,8 +86,8 @@
         "@types/react-dom": "^19.0.4",
         "@types/react-dom": "^19.0.4",
         "@types/react-grid-layout": "^1.3.5",
         "@types/react-grid-layout": "^1.3.5",
         "@uidotdev/usehooks": "^2.4.1",
         "@uidotdev/usehooks": "^2.4.1",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "@vitejs/plugin-react": "^4.3.4",
         "@vitejs/plugin-react": "^4.3.4",
         "acorn": "^8.11.3",
         "acorn": "^8.11.3",
         "acorn-walk": "^8.3.2",
         "acorn-walk": "^8.3.2",

+ 0 - 1
packages/dashboard/vite/tests/fixtures-path-alias/aliased-plugin/index.ts

@@ -1 +0,0 @@
-export * from './src/aliased.plugin';

+ 1 - 0
packages/dashboard/vite/tests/fixtures-path-alias/js-aliased/index.ts

@@ -0,0 +1 @@
+export * from './src/js-aliased.plugin';

+ 1 - 1
packages/dashboard/vite/tests/fixtures-path-alias/aliased-plugin/src/aliased.plugin.ts → packages/dashboard/vite/tests/fixtures-path-alias/js-aliased/src/js-aliased.plugin.ts

@@ -5,4 +5,4 @@ import { PluginCommonModule, VendurePlugin } from '@vendure/core';
     providers: [],
     providers: [],
     dashboard: './dashboard/index.tsx',
     dashboard: './dashboard/index.tsx',
 })
 })
-export class AliasedPlugin {}
+export class JsAliasedPlugin {}

+ 1 - 0
packages/dashboard/vite/tests/fixtures-path-alias/star-aliased/index.ts

@@ -0,0 +1 @@
+export * from './src/star-aliased.plugin';

+ 8 - 0
packages/dashboard/vite/tests/fixtures-path-alias/star-aliased/src/star-aliased.plugin.ts

@@ -0,0 +1,8 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [],
+    dashboard: './dashboard/index.tsx',
+})
+export class StarAliasedPlugin {}

+ 1 - 0
packages/dashboard/vite/tests/fixtures-path-alias/ts-aliased/index.ts

@@ -0,0 +1 @@
+export * from './src/ts-aliased.plugin';

+ 8 - 0
packages/dashboard/vite/tests/fixtures-path-alias/ts-aliased/src/ts-aliased.plugin.ts

@@ -0,0 +1,8 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [],
+    dashboard: './dashboard/index.tsx',
+})
+export class TsAliasedPlugin {}

+ 4 - 3
packages/dashboard/vite/tests/fixtures-path-alias/vendure-config.ts

@@ -1,7 +1,8 @@
+import { JsAliasedPlugin } from '@other/js-aliased';
+import { TsAliasedPlugin } from '@other/ts-aliased';
+import { StarAliasedPlugin } from '@plugins/star-aliased';
 import { VendureConfig } from '@vendure/core';
 import { VendureConfig } from '@vendure/core';
 
 
-import { AliasedPlugin } from './aliased-plugin';
-
 export const config: VendureConfig = {
 export const config: VendureConfig = {
     apiOptions: {
     apiOptions: {
         port: 3000,
         port: 3000,
@@ -15,5 +16,5 @@ export const config: VendureConfig = {
     paymentOptions: {
     paymentOptions: {
         paymentMethodHandlers: [],
         paymentMethodHandlers: [],
     },
     },
-    plugins: [AliasedPlugin],
+    plugins: [StarAliasedPlugin, TsAliasedPlugin, JsAliasedPlugin],
 };
 };

+ 35 - 7
packages/dashboard/vite/tests/path-alias.spec.ts

@@ -16,17 +16,45 @@ describe('detecting plugins using tsconfig path aliases', () => {
                 outputPath: tempDir,
                 outputPath: tempDir,
                 vendureConfigPath: join(__dirname, 'fixtures-path-alias', 'vendure-config.ts'),
                 vendureConfigPath: join(__dirname, 'fixtures-path-alias', 'vendure-config.ts'),
                 logger: process.env.LOG ? debugLogger : noopLogger,
                 logger: process.env.LOG ? debugLogger : noopLogger,
+                pathAdapter: {
+                    transformTsConfigPathMappings: ({ phase, patterns }) => {
+                        if (phase === 'loading') {
+                            return patterns.map(pattern => {
+                                return pattern.replace(/\/fixtures-path-alias/, '').replace(/.ts$/, '.js');
+                            });
+                        } else {
+                            return patterns;
+                        }
+                    },
+                },
             });
             });
 
 
-            expect(result.pluginInfo).toHaveLength(1);
-            expect(result.pluginInfo[0].name).toBe('AliasedPlugin');
-            expect(result.pluginInfo[0].dashboardEntryPath).toBe('./dashboard/index.tsx');
-            expect(result.pluginInfo[0].sourcePluginPath).toBe(
-                join(__dirname, 'fixtures-path-alias', 'aliased-plugin', 'src', 'aliased.plugin.ts'),
+            const plugins = result.pluginInfo.sort((a, b) => a.name.localeCompare(b.name));
+
+            expect(plugins).toHaveLength(3);
+
+            expect(plugins[0].name).toBe('JsAliasedPlugin');
+            expect(plugins[0].dashboardEntryPath).toBe('./dashboard/index.tsx');
+            expect(plugins[0].sourcePluginPath).toBe(
+                join(__dirname, 'fixtures-path-alias', 'js-aliased', 'src', 'js-aliased.plugin.ts'),
+            );
+            expect(plugins[0].pluginPath).toBe(join(tempDir, 'js-aliased', 'src', 'js-aliased.plugin.js'));
+
+            expect(plugins[1].name).toBe('StarAliasedPlugin');
+            expect(plugins[1].dashboardEntryPath).toBe('./dashboard/index.tsx');
+            expect(plugins[1].sourcePluginPath).toBe(
+                join(__dirname, 'fixtures-path-alias', 'star-aliased', 'src', 'star-aliased.plugin.ts'),
+            );
+            expect(plugins[1].pluginPath).toBe(
+                join(tempDir, 'star-aliased', 'src', 'star-aliased.plugin.js'),
             );
             );
-            expect(result.pluginInfo[0].pluginPath).toBe(
-                join(tempDir, 'aliased-plugin', 'src', 'aliased.plugin.js'),
+
+            expect(plugins[2].name).toBe('TsAliasedPlugin');
+            expect(plugins[2].dashboardEntryPath).toBe('./dashboard/index.tsx');
+            expect(plugins[2].sourcePluginPath).toBe(
+                join(__dirname, 'fixtures-path-alias', 'ts-aliased', 'src', 'ts-aliased.plugin.ts'),
             );
             );
+            expect(plugins[2].pluginPath).toBe(join(tempDir, 'ts-aliased', 'src', 'ts-aliased.plugin.js'));
         },
         },
         { timeout: 10_000 },
         { timeout: 10_000 },
     );
     );

+ 19 - 9
packages/dashboard/vite/tests/tsconfig.json

@@ -1,11 +1,21 @@
 {
 {
-    "compilerOptions": {
-        "module": "NodeNext",
-        "sourceMap": true,
-        "jsx": "react-jsx",
-        "paths": {
-            "@plugins/*": ["./fixtures-path-alias/*"]
-        }
-    },
-    "exclude": ["node_modules"]
+  "compilerOptions": {
+    "module": "NodeNext",
+    "sourceMap": true,
+    "jsx": "react-jsx",
+    "paths": {
+      "@plugins/*": [
+        "./fixtures-path-alias/*"
+      ],
+      "@other/ts-aliased": [
+        "./fixtures-path-alias/ts-aliased/index.ts"
+      ],
+      "@other/js-aliased": [
+        "./fixtures-path-alias/js-aliased/index.js"
+      ]
+    }
+  },
+  "exclude": [
+    "node_modules"
+  ]
 }
 }

+ 4 - 1
packages/dashboard/vite/utils/plugin-discovery.ts

@@ -232,8 +232,11 @@ export async function analyzeSourceFiles(
                     importPath + '.js',
                     importPath + '.js',
                     path.join(importPath, 'index.ts'),
                     path.join(importPath, 'index.ts'),
                     path.join(importPath, 'index.js'),
                     path.join(importPath, 'index.js'),
+                    importPath,
                 ];
                 ];
-
+                if (importPath.endsWith('.js')) {
+                    possiblePaths.push(importPath.replace(/.js$/, '.ts'));
+                }
                 // Try each possible path
                 // Try each possible path
                 let found = false;
                 let found = false;
                 for (const possiblePath of possiblePaths) {
                 for (const possiblePath of possiblePaths) {

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "dev-server",
     "name": "dev-server",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "index.js",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "private": true,
     "private": true,
@@ -19,17 +19,17 @@
     },
     },
     "dependencies": {
     "dependencies": {
         "@nestjs/axios": "^4.0.0",
         "@nestjs/axios": "^4.0.0",
-        "@vendure/admin-ui-plugin": "3.3.6",
-        "@vendure/asset-server-plugin": "3.3.6",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
-        "@vendure/elasticsearch-plugin": "3.3.6",
-        "@vendure/email-plugin": "3.3.6",
+        "@vendure/admin-ui-plugin": "3.3.7",
+        "@vendure/asset-server-plugin": "3.3.7",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
+        "@vendure/elasticsearch-plugin": "3.3.7",
+        "@vendure/email-plugin": "3.3.7",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/testing": "3.3.6",
-        "@vendure/ui-devkit": "3.3.6",
+        "@vendure/testing": "3.3.7",
+        "@vendure/ui-devkit": "3.3.7",
         "commander": "^12.0.0",
         "commander": "^12.0.0",
         "concurrently": "^8.2.2",
         "concurrently": "^8.2.2",
         "csv-stringify": "^6.4.6",
         "csv-stringify": "^6.4.6",

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/elasticsearch-plugin",
     "name": "@vendure/elasticsearch-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
@@ -30,8 +30,8 @@
         "fast-deep-equal": "^3.1.3"
         "fast-deep-equal": "^3.1.3"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     }
     }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/email-plugin",
     "name": "@vendure/email-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
@@ -38,8 +38,8 @@
         "@types/express": "^5.0.1",
         "@types/express": "^5.0.1",
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
         "@types/mjml": "^4.7.4",
         "@types/mjml": "^4.7.4",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     }
     }

+ 3 - 3
packages/graphiql-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/graphiql-plugin",
     "name": "@vendure/graphiql-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "dist/index.js",
     "main": "dist/index.js",
     "types": "dist/index.d.ts",
     "types": "dist/index.d.ts",
@@ -33,8 +33,8 @@
         "@types/express": "^5.0.0",
         "@types/express": "^5.0.0",
         "@types/react": "^19.0.0",
         "@types/react": "^19.0.0",
         "@types/react-dom": "^19.0.0",
         "@types/react-dom": "^19.0.0",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "@vitejs/plugin-react": "^4.4.1",
         "@vitejs/plugin-react": "^4.4.1",
         "graphiql": "^4.0.2",
         "graphiql": "^4.0.2",
         "react": "^19.0.0",
         "react": "^19.0.0",

+ 3 - 3
packages/harden-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/harden-plugin",
     "name": "@vendure/harden-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
@@ -25,7 +25,7 @@
         "graphql-query-complexity": "^0.12.0"
         "graphql-query-complexity": "^0.12.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6"
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7"
     }
     }
 }
 }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/job-queue-plugin",
     "name": "@vendure/job-queue-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "main": "package/index.js",
     "types": "package/index.d.ts",
     "types": "package/index.d.ts",
@@ -27,8 +27,8 @@
     },
     },
     "devDependencies": {
     "devDependencies": {
         "@google-cloud/pubsub": "^2.8.0",
         "@google-cloud/pubsub": "^2.8.0",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "bullmq": "^5.4.2",
         "bullmq": "^5.4.2",
         "ioredis": "^5.3.2",
         "ioredis": "^5.3.2",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",

+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/payments-plugin",
     "name": "@vendure/payments-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "main": "package/index.js",
     "types": "package/index.d.ts",
     "types": "package/index.d.ts",
@@ -50,9 +50,9 @@
         "@mollie/api-client": "^4.3.3",
         "@mollie/api-client": "^4.3.3",
         "@types/braintree": "^3.3.11",
         "@types/braintree": "^3.3.11",
         "@types/localtunnel": "2.0.4",
         "@types/localtunnel": "2.0.4",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
-        "@vendure/testing": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
+        "@vendure/testing": "3.3.7",
         "braintree": "^3.22.0",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",
         "nock": "^13.1.4",

+ 3 - 3
packages/sentry-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/sentry-plugin",
     "name": "@vendure/sentry-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
@@ -26,7 +26,7 @@
     },
     },
     "devDependencies": {
     "devDependencies": {
         "@sentry/node": "^7.106.1",
         "@sentry/node": "^7.106.1",
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6"
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7"
     }
     }
 }
 }

+ 3 - 3
packages/stellate-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/stellate-plugin",
     "name": "@vendure/stellate-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "types": "lib/index.d.ts",
@@ -25,7 +25,7 @@
         "node-fetch": "^2.7.0"
         "node-fetch": "^2.7.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6"
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7"
     }
     }
 }
 }

+ 3 - 3
packages/telemetry-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/telemetry-plugin",
     "name": "@vendure/telemetry-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "Telemetry for Vendure",
     "description": "Telemetry for Vendure",
     "repository": {
     "repository": {
         "type": "git",
         "type": "git",
@@ -45,8 +45,8 @@
         "javascript-stringify": "^2.1.0"
         "javascript-stringify": "^2.1.0"
     },
     },
     "devDependencies": {
     "devDependencies": {
-        "@vendure/common": "3.3.6",
-        "@vendure/core": "3.3.6",
+        "@vendure/common": "3.3.7",
+        "@vendure/core": "3.3.7",
         "typescript": "5.8.2"
         "typescript": "5.8.2"
     }
     }
 }
 }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/testing",
     "name": "@vendure/testing",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "End-to-end testing tools for Vendure projects",
     "description": "End-to-end testing tools for Vendure projects",
     "keywords": [
     "keywords": [
         "vendure",
         "vendure",
@@ -38,7 +38,7 @@
     },
     },
     "dependencies": {
     "dependencies": {
         "@graphql-typed-document-node/core": "^3.2.0",
         "@graphql-typed-document-node/core": "^3.2.0",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "faker": "^4.1.0",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "form-data": "^4.0.0",
         "graphql": "^16.10.0",
         "graphql": "^16.10.0",
@@ -51,7 +51,7 @@
         "@types/mysql": "^2.15.26",
         "@types/mysql": "^2.15.26",
         "@types/node-fetch": "^2.6.4",
         "@types/node-fetch": "^2.6.4",
         "@types/pg": "^8.11.2",
         "@types/pg": "^8.11.2",
-        "@vendure/core": "3.3.6",
+        "@vendure/core": "3.3.7",
         "mysql": "^2.18.1",
         "mysql": "^2.18.1",
         "pg": "^8.11.3",
         "pg": "^8.11.3",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/ui-devkit",
     "name": "@vendure/ui-devkit",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "A library for authoring Vendure Admin UI extensions",
     "description": "A library for authoring Vendure Admin UI extensions",
     "keywords": [
     "keywords": [
         "vendure",
         "vendure",
@@ -40,8 +40,8 @@
         "@angular/cli": "^19.2.5",
         "@angular/cli": "^19.2.5",
         "@angular/compiler": "^19.2.4",
         "@angular/compiler": "^19.2.4",
         "@angular/compiler-cli": "^19.2.4",
         "@angular/compiler-cli": "^19.2.4",
-        "@vendure/admin-ui": "3.3.6",
-        "@vendure/common": "3.3.6",
+        "@vendure/admin-ui": "3.3.7",
+        "@vendure/common": "3.3.7",
         "chalk": "^4.1.0",
         "chalk": "^4.1.0",
         "chokidar": "^3.6.0",
         "chokidar": "^3.6.0",
         "fs-extra": "^11.2.0",
         "fs-extra": "^11.2.0",
@@ -52,7 +52,7 @@
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
         "@rollup/plugin-terser": "^0.4.4",
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/core": "3.3.6",
+        "@vendure/core": "3.3.7",
         "react": "^19.0.0",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "react-dom": "^19.0.0",
         "rimraf": "^5.0.5",
         "rimraf": "^5.0.5",

+ 1 - 0
scripts/codegen/generate-graphql-types.ts

@@ -21,6 +21,7 @@ const specFileToIgnore = [
     'custom-field-relations.e2e-spec',
     'custom-field-relations.e2e-spec',
     'custom-field-struct.e2e-spec',
     'custom-field-struct.e2e-spec',
     'custom-field-permissions.e2e-spec',
     'custom-field-permissions.e2e-spec',
+    'custom-field-default-values.e2e-spec',
     'order-item-price-calculation-strategy.e2e-spec',
     'order-item-price-calculation-strategy.e2e-spec',
     'list-query-builder.e2e-spec',
     'list-query-builder.e2e-spec',
     'shop-order.e2e-spec',
     'shop-order.e2e-spec',