1
0
Эх сурвалжийг харах

Merge branch 'master' into minor

Michael Bromley 6 сар өмнө
parent
commit
22cb1ba3ef
76 өөрчлөгдсөн 1668 нэмэгдсэн , 497 устгасан
  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>
 
 

+ 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
 requirements, you can create a custom session cache strategy and set it via the `authOptions.sessionCacheStrategy`
 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
 
-<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
 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
 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';
 
+
 ## 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.
 
 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.
 
-_Example_
+*Example*
 
 ```ts
 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
@@ -40,16 +39,16 @@ also to locate the directory of the email template files. So in the example abov
 
 ## 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.
 
-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
 `loadTemplate()` method can use that to locate the correct template file.
 
-_Example_
+*Example*
 
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -57,18 +56,18 @@ import { readFileSync } from 'fs';
 import path from 'path';
 
 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({
-    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:
         </mj-text>
 
-        <--! your custom email layout goes here -->
+        <!-- your custom email layout goes here -->
     </mj-column>
 </mj-section>
 
@@ -130,75 +129,87 @@ import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { quoteRequestedHandler } from './plugins/quote-plugin';
 
 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">
 
 ### 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
 
-<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.
 Multiple filter functions may be defined.
-
 ### 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.
 
 The recipient can be a plain email address: `'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>'`
-
 ### 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.
-
 ### 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
 and subject line for interpolation.
-
 ### 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
 setTemplateVars() method.
-
 ### 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
 setTemplateVars() method.
-
 ### 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".
-
 ### 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
 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.
 - 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>,
-  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
   `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
 
-<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
 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
 errors may result if using the DefaultJobQueuePlugin with certain DBs such as MySQL/MariaDB.
 
-_Example_
+*Example*
 
 ```ts
 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
 
-<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
 templates for channels or languageCodes other than the default.
-
 ### 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.
 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
 added to the `event` as the `data` property.
 
-_Example_
+*Example*
 
 ```ts
 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
 
-<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
 from this handler, which is useful when developing the email templates.
 
+
 </div>

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

@@ -1,23 +1,22 @@
 ---
-title: 'TemplateLoader'
+title: "TemplateLoader"
 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';
 
+
 ## TemplateLoader
 
 <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
-and returns the template as a string.
+and return the template as a string.
 
-_Example_
+*Example*
 
 ```ts
 import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
@@ -47,19 +46,20 @@ interface TemplateLoader {
 
 ### 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
-
 ### loadPartials
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
 
 Load partials and return their contents.
 This method is only called during initialization, i.e. during server startup.
 
+
 </div>
 
+
 ## FileBasedTemplateLoader
 
 <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[]>;
 }
 ```
+* 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">
 
 ### constructor
 
-<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`} />
+<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`}   />
+
 
 ### 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
 
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`} />
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+
+
+
 
 </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
 
-<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**
 
@@ -32,7 +32,7 @@ Parameters
 
 ## 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**
 

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

@@ -11,26 +11,26 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 
 Auto-generates a list page with columns generated based on the provided query document fields.
 
 ```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
 
 ### 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
 
-<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**
 
@@ -55,6 +55,7 @@ interface ListPageProps<T extends TypedDocumentNode<U, V>, U extends ListQuerySh
     rowActions?: RowAction<ListQueryFields<T>>[];
     transformData?: (data: any[]) => 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;`}   />
 
 
+### bulkActions
+
+<MemberInfo kind="property" type={`<a href='/reference/dashboard/components/data-table-bulk-actions#bulkaction'>BulkAction</a>[]`}   />
+
+
 
 
 </div>

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 
@@ -20,45 +20,48 @@ It should be used in conjunction with the <a href='/reference/dashboard/componen
 as direct children.
 
 ```ts title="Signature"
-function PageActionBar(props: { children: React.ReactNode }): void
+function PageActionBar(props: Readonly<{ children: React.ReactNode }>): void
 ```
 Parameters
 
 ### props
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{ children: React.ReactNode }&#62;`} />
 
 
 
 ## 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**
 
 ```ts title="Signature"
-function PageActionBarLeft(props: { children: React.ReactNode }): void
+function PageActionBarLeft(props: Readonly<{ children: React.ReactNode }>): void
 ```
 Parameters
 
 ### props
 
-<MemberInfo kind="parameter" type={`{ children: React.ReactNode }`} />
+<MemberInfo kind="parameter" type={`Readonly&#60;{ children: React.ReactNode }&#62;`} />
 
 
 
 ## 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**
 
 ```ts title="Signature"
-function PageActionBarRight(props: { children: React.ReactNode }): void
+function PageActionBarRight(props: Readonly<{
+    children: React.ReactNode;
+    dropdownMenuItems?: InlineDropdownItem[];
+}>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
@@ -20,19 +20,19 @@ It should be provided with a `column` prop to determine which column it should a
 to identify the block.
 
 ```ts title="Signature"
-function PageBlock(props: PageBlockProps): void
+function PageBlock(props: Readonly<PageBlockProps>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
@@ -86,7 +86,7 @@ type PageBlockProps = {
 
 ## 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**
 
@@ -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.
 
 ```ts title="Signature"
-function FullWidthPageBlock(props: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>): void
+function FullWidthPageBlock(props: Readonly<Pick<PageBlockProps, 'children' | 'className' | 'blockId'>>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
 A component for displaying an auto-generated form for custom fields on a page.
 
 ```ts title="Signature"
-function CustomFieldsPageBlock(props: {
+function CustomFieldsPageBlock(props: Readonly<{
     column: 'main' | 'side';
     entityType: string;
     control: Control<any, any>;
-}): void
+}>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
@@ -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.
 
 ```ts title="Signature"
-function PageLayout(props: PageLayoutProps): void
+function PageLayout(props: Readonly<PageLayoutProps>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 

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

@@ -11,18 +11,18 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 
 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"
-function PageTitle(props: { children: React.ReactNode }): void
+function PageTitle(props: Readonly<{ children: React.ReactNode }>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
@@ -25,11 +25,11 @@ The typical hierarchy of a page is as follows:
  - <a href='/reference/dashboard/components/page-layout#pagelayout'>PageLayout</a>
 
 ```ts title="Signature"
-function Page(props: PageProps): void
+function Page(props: Readonly<PageProps>): void
 ```
 Parameters
 
 ### 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
 
-<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**
 
@@ -21,6 +21,7 @@ Allows you to define custom action bar items for any page in the dashboard.
 interface DashboardActionBarItem {
     pageId: string;
     component: React.FunctionComponent<{ context: PageContextValue }>;
+    type?: 'button' | 'dropdown';
     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;`}   />
 
 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
 
 <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
 
-<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.
 

+ 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
 
-<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**
 
@@ -25,7 +25,9 @@ interface DashboardExtension {
     actionBarItems?: DashboardActionBarItem[];
     alerts?: DashboardAlertDefinition[];
     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.
 ### 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
 
-<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
 given components and optionally also add a nav menu item.
 ### 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>

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 

+ 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
 
-<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**
 

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 

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

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

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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**
 
@@ -77,7 +77,7 @@ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
 ```
 
 ```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
 
@@ -89,18 +89,19 @@ Parameters
 
 ## 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**
 
 ```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'> {
+    pageId?: string;
     queryDocument: T;
     entityField?: EntityField;
     params: {
         id: string;
     };
-    entityName: string;
+    entityName?: string;
     createDocument?: C;
     updateDocument?: U;
     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">
 
+### 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
 
 <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.
 Required to filter out readonly custom fields before mutations.
+If not provided, the function will try to infer it from the query document.
 ### createDocument
 
 <MemberInfo kind="property" type={`C`}   />
@@ -176,12 +184,12 @@ The function to call when the update is successful.
 
 ## 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**
 
 ```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']>>;
     submitHandler: (event: FormEvent<HTMLFormElement>) => void;
     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
 
-<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**
 

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## 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
 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/*"],
-    "version": "3.3.6",
+    "version": "3.3.7",
     "npmClient": "npm",
     "command": {
         "version": {

+ 65 - 65
package-lock.json

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

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -25,9 +25,9 @@
     "devDependencies": {
         "@types/express": "^5.0.1",
         "@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",
         "rimraf": "^5.0.5",
         "typescript": "5.8.2"

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "repository": {
         "type": "git",
@@ -53,7 +53,7 @@
         "@ng-select/ng-select": "^14.2.6",
         "@ngx-translate/core": "^16.0.4",
         "@ngx-translate/http-loader": "^16.0.1",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^10.0.3",
         "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.
-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",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -30,8 +30,8 @@
         "@types/express": "^5.0.1",
         "@types/fs-extra": "^11.0.4",
         "@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",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",

+ 3 - 3
packages/cli/package.json

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

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/common",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "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' },
                             },
                             {
-                                _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",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -49,7 +49,7 @@
         "@nestjs/platform-express": "^11.0.12",
         "@nestjs/terminus": "^11.0.0",
         "@nestjs/typeorm": "^11.0.0",
-        "@vendure/common": "3.3.6",
+        "@vendure/common": "3.3.7",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "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 { VENDURE_ADMIN_API_TYPE_PATHS, VENDURE_SHOP_API_TYPE_PATHS } from './constants';
 import { AuthGuard } from './middleware/auth-guard';
+import { CustomFieldProcessingInterceptor } from './middleware/custom-field-processing-interceptor';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { IdInterceptor } from './middleware/id-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
@@ -60,7 +60,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         },
         {
             provide: APP_INTERCEPTOR,
-            useClass: ValidateCustomFieldsInterceptor,
+            useClass: CustomFieldProcessingInterceptor,
         },
         {
             provide: APP_INTERCEPTOR,
@@ -74,6 +74,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
 })
 export class ApiModule implements NestModule {
     constructor(private configService: ConfigService) {}
+
     async configure(consumer: MiddlewareConsumer) {
         const { adminApiPath, shopApiPath } = this.configService.apiOptions;
         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(
         inputType: GraphQLInputObjectType,
         parent: TypeTreeNode,
+        depth = 0,
     ): { [name: string]: TypeTreeNode } {
+        if (depth > 3) return {};
+
         return Object.entries(inputType.getFields()).reduce(
             (result, [key, field]) => {
                 const namedType = getNamedType(field.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;
                 }
                 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';
 
 /**
  * @description
  * 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[] = []> {
     /**
@@ -35,9 +36,31 @@ export interface SelfRefreshingCache<V, RefreshArgs extends any[] = []> {
     refresh(...args: RefreshArgs): Promise<V>;
 }
 
+/**
+ * @description
+ * Configuration options for creating a {@link SelfRefreshingCache}.
+ *
+ * @docsCategory cache
+ * @docsPage SelfRefreshingCache
+ */
 export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
+    /**
+     * @description
+     * The name of the cache, used for logging purposes.
+     * e.g. `'MyService.cachedValue'`.
+     */
     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;
+    /**
+     * @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: {
         fn: (...args: RefreshArgs) => Promise<V>;
         /**
@@ -46,6 +69,7 @@ export interface SelfRefreshingCacheConfig<V, RefreshArgs extends any[]> {
         defaultArgs: RefreshArgs;
     };
     /**
+     * @description
      * Intended for unit testing the SelfRefreshingCache only.
      * 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
  * value has expired, it will automatically run the `refreshFn` to update the value and then return the
  * 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[]>(
     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 { omit } from '@vendure/common/lib/omit';
 import { ID, Type } from '@vendure/common/lib/shared-types';
-import { FindOneOptions } from 'typeorm';
 import { FindManyOptions } from 'typeorm/find-options/FindManyOptions';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { Translatable, TranslatedInput, Translation } from '../../../common/types/locale-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 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 { TranslationDiffer } from './translation-differ';
@@ -67,6 +65,7 @@ export class TranslatableSaver {
         const { ctx, entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
 
         const entity = new entityType(input);
+
         const translations: Array<Translation<T>> = [];
 
         if (input.translations) {

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

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

+ 3 - 3
packages/create/package.json

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

+ 3 - 3
packages/dashboard/package.json

@@ -1,7 +1,7 @@
 {
     "name": "@vendure/dashboard",
     "private": false,
-    "version": "3.3.6",
+    "version": "3.3.7",
     "type": "module",
     "repository": {
         "type": "git",
@@ -86,8 +86,8 @@
         "@types/react-dom": "^19.0.4",
         "@types/react-grid-layout": "^1.3.5",
         "@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",
         "acorn": "^8.11.3",
         "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: [],
     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 { AliasedPlugin } from './aliased-plugin';
-
 export const config: VendureConfig = {
     apiOptions: {
         port: 3000,
@@ -15,5 +16,5 @@ export const config: VendureConfig = {
     paymentOptions: {
         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,
                 vendureConfigPath: join(__dirname, 'fixtures-path-alias', 'vendure-config.ts'),
                 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 },
     );

+ 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',
                     path.join(importPath, 'index.ts'),
                     path.join(importPath, 'index.js'),
+                    importPath,
                 ];
-
+                if (importPath.endsWith('.js')) {
+                    possiblePaths.push(importPath.replace(/.js$/, '.ts'));
+                }
                 // Try each possible path
                 let found = false;
                 for (const possiblePath of possiblePaths) {

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

@@ -1,6 +1,6 @@
 {
     "name": "dev-server",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "main": "index.js",
     "license": "GPL-3.0-or-later",
     "private": true,
@@ -19,17 +19,17 @@
     },
     "dependencies": {
         "@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"
     },
     "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",
         "concurrently": "^8.2.2",
         "csv-stringify": "^6.4.6",

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/elasticsearch-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -30,8 +30,8 @@
         "fast-deep-equal": "^3.1.3"
     },
     "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",
         "typescript": "5.8.2"
     }

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/email-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -38,8 +38,8 @@
         "@types/express": "^5.0.1",
         "@types/fs-extra": "^11.0.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",
         "typescript": "5.8.2"
     }

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/graphiql-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "dist/index.js",
     "types": "dist/index.d.ts",
@@ -33,8 +33,8 @@
         "@types/express": "^5.0.0",
         "@types/react": "^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",
         "graphiql": "^4.0.2",
         "react": "^19.0.0",

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/harden-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -25,7 +25,7 @@
         "graphql-query-complexity": "^0.12.0"
     },
     "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",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -27,8 +27,8 @@
     },
     "devDependencies": {
         "@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",
         "ioredis": "^5.3.2",
         "rimraf": "^5.0.5",

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -50,9 +50,9 @@
         "@mollie/api-client": "^4.3.3",
         "@types/braintree": "^3.3.11",
         "@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",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/sentry-plugin",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -26,7 +26,7 @@
     },
     "devDependencies": {
         "@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",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "license": "GPL-3.0-or-later",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -25,7 +25,7 @@
         "node-fetch": "^2.7.0"
     },
     "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",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "Telemetry for Vendure",
     "repository": {
         "type": "git",
@@ -45,8 +45,8 @@
         "javascript-stringify": "^2.1.0"
     },
     "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"
     }
 }

+ 3 - 3
packages/testing/package.json

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

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/ui-devkit",
-    "version": "3.3.6",
+    "version": "3.3.7",
     "description": "A library for authoring Vendure Admin UI extensions",
     "keywords": [
         "vendure",
@@ -40,8 +40,8 @@
         "@angular/cli": "^19.2.5",
         "@angular/compiler": "^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",
         "chokidar": "^3.6.0",
         "fs-extra": "^11.2.0",
@@ -52,7 +52,7 @@
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/core": "3.3.6",
+        "@vendure/core": "3.3.7",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "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-struct.e2e-spec',
     'custom-field-permissions.e2e-spec',
+    'custom-field-default-values.e2e-spec',
     'order-item-price-calculation-strategy.e2e-spec',
     'list-query-builder.e2e-spec',
     'shop-order.e2e-spec',