Просмотр исходного кода

Merge branch 'master' into minor

Michael Bromley 1 год назад
Родитель
Сommit
5db4c6645d
70 измененных файлов с 717 добавлено и 521 удалено
  1. 9 5
      docs/docs/guides/developer-guide/testing/index.md
  2. 4 3
      docs/docs/guides/extending-the-admin-ui/getting-started/index.md
  3. 1 1
      docs/docs/reference/core-plugins/email-plugin/email-event-handler-with-async-data.md
  4. 1 2
      docs/docs/reference/core-plugins/email-plugin/email-event-handler.md
  5. 19 19
      docs/docs/reference/core-plugins/email-plugin/email-plugin-options.md
  6. 43 5
      docs/docs/reference/core-plugins/email-plugin/email-plugin-types.md
  7. 5 5
      docs/docs/reference/core-plugins/email-plugin/index.md
  8. 2 2
      docs/docs/reference/core-plugins/email-plugin/template-loader.md
  9. 1 1
      docs/docs/reference/core-plugins/email-plugin/transport-options.md
  10. 1 1
      docs/docs/reference/typescript-api/assets/asset-options.md
  11. 2 3
      docs/docs/reference/typescript-api/auth/auth-options.md
  12. 4 4
      docs/docs/reference/typescript-api/auth/external-authentication-service.md
  13. 1 1
      docs/docs/reference/typescript-api/auth/superadmin-credentials.md
  14. 2 2
      docs/docs/reference/typescript-api/common/admin-ui/admin-ui-config.md
  15. 2 2
      docs/docs/reference/typescript-api/common/json-compatible.md
  16. 9 9
      docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md
  17. 1 1
      docs/docs/reference/typescript-api/configuration/entity-options.md
  18. 1 1
      docs/docs/reference/typescript-api/configuration/runtime-vendure-config.md
  19. 1 1
      docs/docs/reference/typescript-api/configuration/system-options.md
  20. 1 1
      docs/docs/reference/typescript-api/configuration/vendure-config.md
  21. 1 1
      docs/docs/reference/typescript-api/custom-fields/custom-field-type.md
  22. 1 1
      docs/docs/reference/typescript-api/import-export/import-export-options.md
  23. 1 1
      docs/docs/reference/typescript-api/job-queue/job-queue-options.md
  24. 1 1
      docs/docs/reference/typescript-api/orders/order-options.md
  25. 1 1
      docs/docs/reference/typescript-api/payment/payment-options.md
  26. 1 1
      docs/docs/reference/typescript-api/plugin/vendure-plugin-metadata.md
  27. 1 1
      docs/docs/reference/typescript-api/products-stock/catalog-options.md
  28. 1 1
      docs/docs/reference/typescript-api/promotions/promotion-options.md
  29. 3 3
      docs/docs/reference/typescript-api/services/collection-service.md
  30. 1 1
      docs/docs/reference/typescript-api/shipping/shipping-options.md
  31. 1 1
      docs/docs/reference/typescript-api/tax/tax-options.md
  32. 0 4
      docs/docs/user-guide/catalog/index.md
  33. 2 3
      docs/docs/user-guide/catalog/products.md
  34. 1 6
      docs/docs/user-guide/index.md
  35. 1 1
      docs/docs/user-guide/localization/index.md
  36. 2 6
      docs/docs/user-guide/orders/draft-orders.md
  37. 4 4
      docs/docs/user-guide/promotions/index.md
  38. 1 1
      docs/docs/user-guide/settings/countries-zones.md
  39. 2 2
      docs/docs/user-guide/settings/global-settings.md
  40. 1 1
      docs/docs/user-guide/settings/taxes.md
  41. 7 3
      docs/docusaurus.config.js
  42. 32 0
      docs/sidebars.js
  43. 64 0
      license/signatures/version1/cla.json
  44. 0 26
      package-lock.json
  45. 4 5
      packages/admin-ui/src/lib/catalog/src/components/create-product-variant-dialog/create-product-variant-dialog.component.ts
  46. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss
  47. 2 2
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.spec.ts
  48. 0 3
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.ts
  49. 8 8
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  50. 160 160
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  51. 7 7
      packages/common/src/shared-types.ts
  52. 1 0
      packages/core/src/api/resolvers/shop/shop-order.resolver.ts
  53. 2 3
      packages/core/src/config/vendure-config.ts
  54. 6 1
      packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts
  55. 4 4
      packages/core/src/service/helpers/external-authentication/external-authentication.service.ts
  56. 142 93
      packages/core/src/service/services/collection.service.ts
  57. 1 0
      packages/core/src/service/services/stock-movement.service.ts
  58. 0 2
      packages/create/package.json
  59. 20 7
      packages/create/src/create-vendure-app.ts
  60. 1 1
      packages/create/src/gather-user-responses.ts
  61. 3 3
      packages/create/src/helpers.ts
  62. 1 0
      packages/create/templates/.env.hbs
  63. 1 0
      packages/create/templates/environment.d.hbs
  64. 6 5
      packages/create/templates/vendure-config.hbs
  65. 2 2
      packages/dev-server/dev-config.ts
  66. 0 1
      packages/email-plugin/src/handler/event-handler.ts
  67. 77 68
      packages/email-plugin/src/plugin.spec.ts
  68. 5 5
      packages/email-plugin/src/plugin.ts
  69. 22 1
      packages/email-plugin/src/types.ts
  70. 1 0
      packages/payments-plugin/src/mollie/mollie.service.ts

+ 9 - 5
docs/docs/guides/developer-guide/testing/index.md

@@ -26,18 +26,22 @@ For a working example of a Vendure plugin with e2e testing, see the [real-world-
   - `@swc/core`
   - `@swc/core`
   - `unplugin-swc`
   - `unplugin-swc`
 
 
+```sh
+npm install --save-dev @vendure/testing vitest graphql-tag @swc/core unplugin-swc
+```
+
 ### Configure Vitest
 ### Configure Vitest
 
 
-Create a `vitest.config.js` file in the root of your project:
+Create a `vitest.config.mts` file in the root of your project:
 
 
-```ts
+```ts title="vitest.config.mts"
 import path from 'path';
 import path from 'path';
 import swc from 'unplugin-swc';
 import swc from 'unplugin-swc';
 import { defineConfig } from 'vitest/config';
 import { defineConfig } from 'vitest/config';
 
 
 export default defineConfig({
 export default defineConfig({
     test: {
     test: {
-        include: '**/*.e2e-spec.ts',
+        include: ['**/*.e2e-spec.ts'],
         typecheck: {
         typecheck: {
             tsconfig: path.join(__dirname, 'tsconfig.e2e.json'),
             tsconfig: path.join(__dirname, 'tsconfig.e2e.json'),
         },
         },
@@ -60,9 +64,9 @@ export default defineConfig({
 
 
 and a `tsconfig.e2e.json` tsconfig file for the tests:
 and a `tsconfig.e2e.json` tsconfig file for the tests:
 
 
-```json
+```json title="tsconfig.e2e.json"
 {
 {
-  "extends": "../tsconfig.json",
+  "extends": "./tsconfig.json",
   "compilerOptions": {
   "compilerOptions": {
     "types": ["node"],
     "types": ["node"],
     "lib": ["es2015"],
     "lib": ["es2015"],

+ 4 - 3
docs/docs/guides/extending-the-admin-ui/getting-started/index.md

@@ -357,12 +357,13 @@ export const config: VendureConfig = {
 ```
 ```
 
 
 :::info
 :::info
-To compile the angular app ahead of time (for production) and copy the dist folder to Vendure's output dist folder, include the following commands in your packages.json scripts:
+To compile the angular app ahead of time (for production) and copy the dist folder to
+Vendure's output dist folder, include the following commands in your package.json scripts:
 
 
 ```json
 ```json
 {
 {
     "scripts": {
     "scripts": {
-        "copy": "npx copyfiles -u 1 'src/__admin-ui/dist/**/*' dist",
+        "copy": "npx copyfiles -u 1 'admin-ui/dist/**/*' dist",
         "build": "tsc && yarn copy",
         "build": "tsc && yarn copy",
         "build:admin": "rimraf admin-ui && npx ts-node src/compile-admin-ui.ts"
         "build:admin": "rimraf admin-ui && npx ts-node src/compile-admin-ui.ts"
     }
     }
@@ -370,7 +371,7 @@ To compile the angular app ahead of time (for production) and copy the dist fold
 ```
 ```
 
 
 "build:admin" will remove the admin-ui folder and run the compileUiExtensions function to generate the admin-ui Angular app.
 "build:admin" will remove the admin-ui folder and run the compileUiExtensions function to generate the admin-ui Angular app.
-Make sure to install copyfiles before running the "copy" command:
+Make sure to install `copyfiles` before running the "copy" command:
 
 
 <Tabs>
 <Tabs>
 <TabItem value="npm" label="npm" default>
 <TabItem value="npm" label="npm" default>

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

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

+ 1 - 2
docs/docs/reference/core-plugins/email-plugin/email-event-handler.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## EmailEventHandler
 ## EmailEventHandler
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="136" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/handler/event-handler.ts" sourceLine="135" packageName="@vendure/email-plugin" />
 
 
 The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 The EmailEventHandler defines how the EmailPlugin will respond to a given event.
 
 
@@ -119,7 +119,6 @@ const config: VendureConfig = {
   plugins: [
   plugins: [
     EmailPlugin.init({
     EmailPlugin.init({
       handler: [...defaultEmailHandlers, quoteRequestedHandler],
       handler: [...defaultEmailHandlers, quoteRequestedHandler],
-      templatePath: path.join(__dirname, 'vendure/email/templates'),
       // ... etc
       // ... etc
     }),
     }),
   ],
   ],

+ 19 - 19
docs/docs/reference/core-plugins/email-plugin/email-plugin-options.md

@@ -19,11 +19,11 @@ Configuration for the EmailPlugin.
 interface EmailPluginOptions {
 interface EmailPluginOptions {
     templatePath?: string;
     templatePath?: string;
     templateLoader?: TemplateLoader;
     templateLoader?: TemplateLoader;
-    transport:
-        | EmailTransportOptions
-        | ((
-              injector?: Injector,
-              ctx?: RequestContext,
+    transport:
+        | EmailTransportOptions
+        | ((
+              injector?: Injector,
+              ctx?: RequestContext,
           ) => EmailTransportOptions | Promise<EmailTransportOptions>);
           ) => EmailTransportOptions | Promise<EmailTransportOptions>);
     handlers: Array<EmailEventHandler<string, any>>;
     handlers: Array<EmailEventHandler<string, any>>;
     globalTemplateVars?: { [key: string]: any } | GlobalTemplateVarsFn;
     globalTemplateVars?: { [key: string]: any } | GlobalTemplateVarsFn;
@@ -38,44 +38,44 @@ interface EmailPluginOptions {
 
 
 <MemberInfo kind="property" type={`string`}   />
 <MemberInfo kind="property" type={`string`}   />
 
 
-The path to the location of the email templates. In a default Vendure installation,
+The path to the location of the email templates. In a default Vendure installation,
 the templates are installed to `<project root>/vendure/email/templates`.
 the templates are installed to `<project root>/vendure/email/templates`.
 ### templateLoader
 ### templateLoader
 
 
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a>`}  since="2.0.0"  />
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a>`}  since="2.0.0"  />
 
 
-An optional TemplateLoader which can be used to load templates from a custom location or async service.
+An optional TemplateLoader which can be used to load templates from a custom location or async service.
 The default uses the FileBasedTemplateLoader which loads templates from `<project root>/vendure/email/templates`
 The default uses the FileBasedTemplateLoader which loads templates from `<project root>/vendure/email/templates`
 ### transport
 ### transport
 
 
-<MemberInfo kind="property" type={`| <a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a>         | ((               injector?: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,               ctx?: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,           ) =&#62; <a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a> | Promise&#60;<a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a>&#62;)`}   />
+<MemberInfo kind="property" type={`| <a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a>
         | ((
               injector?: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
               ctx?: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
           ) =&#62; <a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a> | Promise&#60;<a href='/reference/core-plugins/email-plugin/transport-options#emailtransportoptions'>EmailTransportOptions</a>&#62;)`}   />
 
 
 Configures how the emails are sent.
 Configures how the emails are sent.
 ### handlers
 ### handlers
 
 
 <MemberInfo kind="property" type={`Array&#60;<a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;string, any&#62;&#62;`}   />
 <MemberInfo kind="property" type={`Array&#60;<a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>&#60;string, any&#62;&#62;`}   />
 
 
-An array of <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>s which define which Vendure events will trigger
+An array of <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>s which define which Vendure events will trigger
 emails, and how those emails are generated.
 emails, and how those emails are generated.
 ### globalTemplateVars
 ### globalTemplateVars
 
 
 <MemberInfo kind="property" type={`{ [key: string]: any } | <a href='/reference/core-plugins/email-plugin/email-plugin-options#globaltemplatevarsfn'>GlobalTemplateVarsFn</a>`}   />
 <MemberInfo kind="property" type={`{ [key: string]: any } | <a href='/reference/core-plugins/email-plugin/email-plugin-options#globaltemplatevarsfn'>GlobalTemplateVarsFn</a>`}   />
 
 
-An object containing variables which are made available to all templates. For example,
-the storefront URL could be defined here and then used in the "email address verification"
-email. Use the GlobalTemplateVarsFn if you need to retrieve variables from Vendure or
+An object containing variables which are made available to all templates. For example,
+the storefront URL could be defined here and then used in the "email address verification"
+email. Use the GlobalTemplateVarsFn if you need to retrieve variables from Vendure or
 plugin services.
 plugin services.
 ### emailSender
 ### emailSender
 
 
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/email-sender#emailsender'>EmailSender</a>`} default={`<a href='/reference/core-plugins/email-plugin/email-sender#nodemaileremailsender'>NodemailerEmailSender</a>`}   />
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/email-sender#emailsender'>EmailSender</a>`} default={`<a href='/reference/core-plugins/email-plugin/email-sender#nodemaileremailsender'>NodemailerEmailSender</a>`}   />
 
 
-An optional allowed EmailSender, used to allow custom implementations of the send functionality
+An optional allowed EmailSender, used to allow custom implementations of the send functionality
 while still utilizing the existing emailPlugin functionality.
 while still utilizing the existing emailPlugin functionality.
 ### emailGenerator
 ### emailGenerator
 
 
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/email-generator#emailgenerator'>EmailGenerator</a>`} default={`<a href='/reference/core-plugins/email-plugin/email-generator#handlebarsmjmlgenerator'>HandlebarsMjmlGenerator</a>`}   />
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/email-plugin/email-generator#emailgenerator'>EmailGenerator</a>`} default={`<a href='/reference/core-plugins/email-plugin/email-generator#handlebarsmjmlgenerator'>HandlebarsMjmlGenerator</a>`}   />
 
 
-An optional allowed EmailGenerator, used to allow custom email generation functionality to
+An optional allowed EmailGenerator, used to allow custom email generation functionality to
 better match with custom email sending functionality.
 better match with custom email sending functionality.
 
 
 
 
@@ -86,8 +86,8 @@ better match with custom email sending functionality.
 
 
 <GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="64" packageName="@vendure/email-plugin" since="2.3.0" />
 <GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="64" packageName="@vendure/email-plugin" since="2.3.0" />
 
 
-Allows you to dynamically load the "globalTemplateVars" key async and access Vendure services
-to create the object. This is not a requirement. You can also specify a simple static object if your
+Allows you to dynamically load the "globalTemplateVars" key async and access Vendure services
+to create the object. This is not a requirement. You can also specify a simple static object if your
 projects doesn't need to access async or dynamic values.
 projects doesn't need to access async or dynamic values.
 
 
 *Example*
 *Example*
@@ -112,9 +112,9 @@ EmailPlugin.init({
 ```
 ```
 
 
 ```ts title="Signature"
 ```ts title="Signature"
-type GlobalTemplateVarsFn = (
-    ctx: RequestContext,
-    injector: Injector,
+type GlobalTemplateVarsFn = (
+    ctx: RequestContext,
+    injector: Injector,
 ) => Promise<{ [key: string]: any }>
 ) => Promise<{ [key: string]: any }>
 ```
 ```
 
 

+ 43 - 5
docs/docs/reference/core-plugins/email-plugin/email-plugin-types.md

@@ -128,9 +128,47 @@ type EmailAttachment = Omit<Attachment, 'raw'> & { path?: string }
 ```
 ```
 
 
 
 
+## LoadTemplateInput
+
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="401" packageName="@vendure/email-plugin" />
+
+The object passed to the <a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a> `loadTemplate()` method.
+
+```ts title="Signature"
+interface LoadTemplateInput {
+    type: string;
+    templateName: string;
+    templateVars: any;
+}
+```
+
+<div className="members-wrapper">
+
+### type
+
+<MemberInfo kind="property" type={`string`}   />
+
+The type corresponds to the string passed to the EmailEventListener constructor.
+### templateName
+
+<MemberInfo kind="property" type={`string`}   />
+
+The template name is specified by the EmailEventHander's call to
+the `addTemplate()` method, and will default to `body.hbs`
+### templateVars
+
+<MemberInfo kind="property" type={`any`}   />
+
+The variables defined by the globalTemplateVars as well as any variables defined in the
+EmailEventHandler's `setTemplateVars()` method.
+
+
+</div>
+
+
 ## SetTemplateVarsFn
 ## SetTemplateVarsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="413" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="434" packageName="@vendure/email-plugin" />
 
 
 A function used to define template variables available to email templates.
 A function used to define template variables available to email templates.
 See <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>.setTemplateVars().
 See <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>.setTemplateVars().
@@ -145,7 +183,7 @@ type SetTemplateVarsFn<Event> = (
 
 
 ## SetAttachmentsFn
 ## SetAttachmentsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="427" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="448" packageName="@vendure/email-plugin" />
 
 
 A function used to define attachments to be sent with the email.
 A function used to define attachments to be sent with the email.
 See https://nodemailer.com/message/attachments/ for more information about
 See https://nodemailer.com/message/attachments/ for more information about
@@ -158,7 +196,7 @@ type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<Ema
 
 
 ## SetSubjectFn
 ## SetSubjectFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="435" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="456" packageName="@vendure/email-plugin" />
 
 
 A function used to define the subject to be sent with the email.
 A function used to define the subject to be sent with the email.
 
 
@@ -173,7 +211,7 @@ type SetSubjectFn<Event> = (
 
 
 ## OptionalAddressFields
 ## OptionalAddressFields
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="449" packageName="@vendure/email-plugin" since="1.1.0" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="470" packageName="@vendure/email-plugin" since="1.1.0" />
 
 
 Optional address-related fields for sending the email.
 Optional address-related fields for sending the email.
 
 
@@ -209,7 +247,7 @@ An email address that will appear on the _Reply-To:_ field
 
 
 ## SetOptionalAddressFieldsFn
 ## SetOptionalAddressFieldsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="475" packageName="@vendure/email-plugin" since="1.1.0" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="496" packageName="@vendure/email-plugin" since="1.1.0" />
 
 
 A function used to set the <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a>.
 A function used to set the <a href='/reference/core-plugins/email-plugin/email-plugin-types#optionaladdressfields'>OptionalAddressFields</a>.
 
 

+ 5 - 5
docs/docs/reference/core-plugins/email-plugin/index.md

@@ -39,14 +39,14 @@ or
 *Example*
 *Example*
 
 
 ```ts
 ```ts
-import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
+import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
 
 
 const config: VendureConfig = {
 const config: VendureConfig = {
   // Add an instance of the plugin to the plugins array
   // Add an instance of the plugin to the plugins array
   plugins: [
   plugins: [
     EmailPlugin.init({
     EmailPlugin.init({
       handler: defaultEmailHandlers,
       handler: defaultEmailHandlers,
-      templatePath: path.join(__dirname, 'static/email/templates'),
+      templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
       transport: {
       transport: {
         type: 'smtp',
         type: 'smtp',
         host: 'smtp.example.com',
         host: 'smtp.example.com',
@@ -207,13 +207,13 @@ channel aware transport settings.
 *Example*
 *Example*
 
 
 ```ts
 ```ts
-import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
+import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
 import { MyTransportService } from './transport.services.ts';
 import { MyTransportService } from './transport.services.ts';
 const config: VendureConfig = {
 const config: VendureConfig = {
   plugins: [
   plugins: [
     EmailPlugin.init({
     EmailPlugin.init({
       handler: defaultEmailHandlers,
       handler: defaultEmailHandlers,
-      templatePath: path.join(__dirname, 'static/email/templates'),
+      templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
       transport: (injector, ctx) => {
       transport: (injector, ctx) => {
         if (ctx) {
         if (ctx) {
           return injector.get(MyTransportService).getSettings(ctx);
           return injector.get(MyTransportService).getSettings(ctx);
@@ -241,7 +241,7 @@ EmailPlugin.init({
   devMode: true,
   devMode: true,
   route: 'mailbox',
   route: 'mailbox',
   handler: defaultEmailHandlers,
   handler: defaultEmailHandlers,
-  templatePath: path.join(__dirname, 'vendure/email/templates'),
+  templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
   outputPath: path.join(__dirname, 'test-emails'),
   outputPath: path.join(__dirname, 'test-emails'),
 })
 })
 ```
 ```

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

@@ -46,7 +46,7 @@ interface TemplateLoader {
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: LoadTemplateInput) => Promise&#60;string&#62;`}   />
+<MemberInfo kind="method" type={`(injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
 
 
 Load template and return it's content as a string
 Load template and return it's content as a string
 ### loadPartials
 ### loadPartials
@@ -87,7 +87,7 @@ class FileBasedTemplateLoader implements TemplateLoader {
 
 
 ### loadTemplate
 ### loadTemplate
 
 
-<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: LoadTemplateInput) => Promise&#60;string&#62;`}   />
+<MemberInfo kind="method" type={`(_injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, _ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, { type, templateName }: <a href='/reference/core-plugins/email-plugin/email-plugin-types#loadtemplateinput'>LoadTemplateInput</a>) => Promise&#60;string&#62;`}   />
 
 
 
 
 ### loadPartials
 ### loadPartials

+ 1 - 1
docs/docs/reference/core-plugins/email-plugin/transport-options.md

@@ -86,7 +86,7 @@ See [Nodemailers's SES docs](https://nodemailer.com/transports/ses/) for more de
   plugins: [
   plugins: [
     EmailPlugin.init({
     EmailPlugin.init({
       handler: defaultEmailHandlers,
       handler: defaultEmailHandlers,
-      templatePath: path.join(__dirname, 'static/email/templates'),
+      templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
       transport: {
       transport: {
         type: 'ses',
         type: 'ses',
         SES: { ses, aws: { SendRawEmailCommand } },
         SES: { ses, aws: { SendRawEmailCommand } },

+ 1 - 1
docs/docs/reference/typescript-api/assets/asset-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## AssetOptions
 ## AssetOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="628" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="627" packageName="@vendure/core" />
 
 
 The AssetOptions define how assets (images and other files) are named and stored, and how preview images are generated.
 The AssetOptions define how assets (images and other files) are named and stored, and how preview images are generated.
 
 

+ 2 - 3
docs/docs/reference/typescript-api/auth/auth-options.md

@@ -100,9 +100,8 @@ taken from the database.
 
 
 Determines whether new User accounts require verification of their email address.
 Determines whether new User accounts require verification of their email address.
 
 
-If set to "true", when registering via the `registerCustomerAccount` mutation, one should *not* set the
-`password` property - doing so will result in an error. Instead, the password is set at a later stage
-(once the email with the verification token has been opened) via the `verifyCustomerAccount` mutation.
+If set to "true", the customer will be required to verify their email address using a verification token
+they receive in their email. See the `registerCustomerAccount` mutation for more details on the verification behavior.
 ### verificationTokenDuration
 ### verificationTokenDuration
 
 
 <MemberInfo kind="property" type={`string | number`} default={`'7d'`}   />
 <MemberInfo kind="property" type={`string | number`} default={`'7d'`}   />

+ 4 - 4
docs/docs/reference/typescript-api/auth/external-authentication-service.md

@@ -24,10 +24,10 @@ class ExternalAuthenticationService {
     createCustomerAndUser(ctx: RequestContext, config: {
     createCustomerAndUser(ctx: RequestContext, config: {
             strategy: string;
             strategy: string;
             externalIdentifier: string;
             externalIdentifier: string;
-            verified: boolean;
             emailAddress: string;
             emailAddress: string;
-            firstName?: string;
-            lastName?: string;
+            firstName: string;
+            lastName: string;
+            verified?: boolean;
         }) => Promise<User>;
         }) => Promise<User>;
     createAdministratorAndUser(ctx: RequestContext, config: {
     createAdministratorAndUser(ctx: RequestContext, config: {
             strategy: string;
             strategy: string;
@@ -67,7 +67,7 @@ Looks up a User based on their identifier from an external authentication
 provider, ensuring this User is associated with an Administrator account.
 provider, ensuring this User is associated with an Administrator account.
 ### createCustomerAndUser
 ### createCustomerAndUser
 
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, config: {
             strategy: string;
             externalIdentifier: string;
             verified: boolean;
             emailAddress: string;
             firstName?: string;
             lastName?: string;
         }) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a>&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, config: {
             strategy: string;
             externalIdentifier: string;
             emailAddress: string;
             firstName: string;
             lastName: string;
             verified?: boolean;
         }) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a>&#62;`}   />
 
 
 If a customer has been successfully authenticated by an external authentication provider, yet cannot
 If a customer has been successfully authenticated by an external authentication provider, yet cannot
 be found using `findCustomerUser`, then we need to create a new User and
 be found using `findCustomerUser`, then we need to create a new User and

+ 1 - 1
docs/docs/reference/typescript-api/auth/superadmin-credentials.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## SuperadminCredentials
 ## SuperadminCredentials
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="804" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="803" packageName="@vendure/core" />
 
 
 These credentials will be used to create the Superadmin user & administrator
 These credentials will be used to create the Superadmin user & administrator
 when Vendure first bootstraps.
 when Vendure first bootstraps.

+ 2 - 2
docs/docs/reference/typescript-api/common/admin-ui/admin-ui-config.md

@@ -42,14 +42,14 @@ interface AdminUiConfig {
 
 
 ### apiHost
 ### apiHost
 
 
-<MemberInfo kind="property" type={`string | 'auto'`} default={`'http://localhost'`}   />
+<MemberInfo kind="property" type={`string | 'auto'`} default={`'auto'`}   />
 
 
 The hostname of the Vendure server which the admin UI will be making API calls
 The hostname of the Vendure server which the admin UI will be making API calls
 to. If set to "auto", the Admin UI app will determine the hostname from the
 to. If set to "auto", the Admin UI app will determine the hostname from the
 current location (i.e. `window.location.hostname`).
 current location (i.e. `window.location.hostname`).
 ### apiPort
 ### apiPort
 
 
-<MemberInfo kind="property" type={`number | 'auto'`} default={`3000`}   />
+<MemberInfo kind="property" type={`number | 'auto'`} default={`'auto'`}   />
 
 
 The port of the Vendure server which the admin UI will be making API calls
 The port of the Vendure server which the admin UI will be making API calls
 to. If set to "auto", the Admin UI app will determine the port from the
 to. If set to "auto", the Admin UI app will determine the port from the

+ 2 - 2
docs/docs/reference/typescript-api/common/json-compatible.md

@@ -21,7 +21,7 @@ type JsonCompatible<T> = {
     [P in keyof T]: T[P] extends Json
     [P in keyof T]: T[P] extends Json
         ? T[P]
         ? T[P]
         : Pick<T, P> extends Required<Pick<T, P>>
         : Pick<T, P> extends Required<Pick<T, P>>
-        ? never
-        : JsonCompatible<T[P]>;
+          ? never
+          : JsonCompatible<T[P]>;
 }
 }
 ```
 ```

+ 9 - 9
docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md

@@ -29,15 +29,15 @@ type DefaultFormConfigHash = {
     'product-selector-form-input': Record<string, never>;
     'product-selector-form-input': Record<string, never>;
     'relation-form-input': Record<string, never>;
     'relation-form-input': Record<string, never>;
     'rich-text-form-input': Record<string, never>;
     'rich-text-form-input': Record<string, never>;
-    'select-form-input': {
-        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
+    'select-form-input': {
+        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
     };
     };
     'text-form-input': { prefix?: string; suffix?: string };
     'text-form-input': { prefix?: string; suffix?: string };
-    'textarea-form-input': {
-        spellcheck?: boolean;
+    'textarea-form-input': {
+        spellcheck?: boolean;
     };
     };
-    'product-multi-form-input': {
-        selectionMode?: 'product' | 'variant';
+    'product-multi-form-input': {
+        selectionMode?: 'product' | 'variant';
     };
     };
     'combination-mode-form-input': Record<string, never>;
     'combination-mode-form-input': Record<string, never>;
 }
 }
@@ -107,7 +107,7 @@ type DefaultFormConfigHash = {
 
 
 ### 'select-form-input'
 ### 'select-form-input'
 
 
-<MemberInfo kind="property" type={`{
         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;
     }`}   />
+<MemberInfo kind="property" type={`{         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;     }`}   />
 
 
 
 
 ### 'text-form-input'
 ### 'text-form-input'
@@ -117,12 +117,12 @@ type DefaultFormConfigHash = {
 
 
 ### 'textarea-form-input'
 ### 'textarea-form-input'
 
 
-<MemberInfo kind="property" type={`{
         spellcheck?: boolean;
     }`}   />
+<MemberInfo kind="property" type={`{         spellcheck?: boolean;     }`}   />
 
 
 
 
 ### 'product-multi-form-input'
 ### 'product-multi-form-input'
 
 
-<MemberInfo kind="property" type={`{
         selectionMode?: 'product' | 'variant';
     }`}   />
+<MemberInfo kind="property" type={`{         selectionMode?: 'product' | 'variant';     }`}   />
 
 
 
 
 ### 'combination-mode-form-input'
 ### 'combination-mode-form-input'

+ 1 - 1
docs/docs/reference/typescript-api/configuration/entity-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## EntityOptions
 ## EntityOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="954" packageName="@vendure/core" since="1.3.0" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="953" packageName="@vendure/core" since="1.3.0" />
 
 
 Options relating to the internal handling of entities.
 Options relating to the internal handling of entities.
 
 

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## RuntimeVendureConfig
 ## RuntimeVendureConfig
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1201" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1200" packageName="@vendure/core" />
 
 
 This interface represents the VendureConfig object available at run-time, i.e. the user-supplied
 This interface represents the VendureConfig object available at run-time, i.e. the user-supplied
 config values have been merged with the <a href='/reference/typescript-api/configuration/default-config#defaultconfig'>defaultConfig</a> values.
 config values have been merged with the <a href='/reference/typescript-api/configuration/default-config#defaultconfig'>defaultConfig</a> values.

+ 1 - 1
docs/docs/reference/typescript-api/configuration/system-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## SystemOptions
 ## SystemOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1043" packageName="@vendure/core" since="1.6.0" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1042" packageName="@vendure/core" since="1.6.0" />
 
 
 Options relating to system functions.
 Options relating to system functions.
 
 

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

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## VendureConfig
 ## VendureConfig
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1071" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1070" packageName="@vendure/core" />
 
 
 All possible configuration options are defined by the
 All possible configuration options are defined by the
 [`VendureConfig`](https://github.com/vendure-ecommerce/vendure/blob/master/server/src/config/vendure-config.ts) interface.
 [`VendureConfig`](https://github.com/vendure-ecommerce/vendure/blob/master/server/src/config/vendure-config.ts) interface.

+ 1 - 1
docs/docs/reference/typescript-api/custom-fields/custom-field-type.md

@@ -28,7 +28,7 @@ boolean      | tinyint (m), bool (p), boolean (s)    | Boolean
 datetime     | datetime (m,s), timestamp (p)         | DateTime
 datetime     | datetime (m,s), timestamp (p)         | DateTime
 relation     | many-to-one / many-to-many relation   | As specified in config
 relation     | many-to-one / many-to-many relation   | As specified in config
 
 
-Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#configuration-options)
+Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#custom-field-config-properties)
 are available for that custom field.
 are available for that custom field.
 
 
 ```ts title="Signature"
 ```ts title="Signature"

+ 1 - 1
docs/docs/reference/typescript-api/import-export/import-export-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## ImportExportOptions
 ## ImportExportOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="889" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="888" packageName="@vendure/core" />
 
 
 Options related to importing & exporting data.
 Options related to importing & exporting data.
 
 

+ 1 - 1
docs/docs/reference/typescript-api/job-queue/job-queue-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## JobQueueOptions
 ## JobQueueOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="913" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="912" packageName="@vendure/core" />
 
 
 Options related to the built-in job queue.
 Options related to the built-in job queue.
 
 

+ 1 - 1
docs/docs/reference/typescript-api/orders/order-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## OrderOptions
 ## OrderOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="483" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="482" packageName="@vendure/core" />
 
 
 
 
 
 

+ 1 - 1
docs/docs/reference/typescript-api/payment/payment-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PaymentOptions
 ## PaymentOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="826" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="825" packageName="@vendure/core" />
 
 
 Defines payment-related options in the <a href='/reference/typescript-api/configuration/vendure-config#vendureconfig'>VendureConfig</a>.
 Defines payment-related options in the <a href='/reference/typescript-api/configuration/vendure-config#vendureconfig'>VendureConfig</a>.
 
 

+ 1 - 1
docs/docs/reference/typescript-api/plugin/vendure-plugin-metadata.md

@@ -71,7 +71,7 @@ To effectively disable this check for a plugin, you can use an overly-permissive
 *Example*
 *Example*
 
 
 ```ts
 ```ts
-compatibility: '^2.0.0'
+compatibility: '^3.0.0'
 ```
 ```
 
 
 
 

+ 1 - 1
docs/docs/reference/typescript-api/products-stock/catalog-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## CatalogOptions
 ## CatalogOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="675" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="674" packageName="@vendure/core" />
 
 
 Options related to products and collections.
 Options related to products and collections.
 
 

+ 1 - 1
docs/docs/reference/typescript-api/promotions/promotion-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## PromotionOptions
 ## PromotionOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="737" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="736" packageName="@vendure/core" />
 
 
 
 
 
 

+ 3 - 3
docs/docs/reference/typescript-api/services/collection-service.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## CollectionService
 ## CollectionService
 
 
-<GenerationInfo sourceFile="packages/core/src/service/services/collection.service.ts" sourceLine="67" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/collection.service.ts" sourceLine="72" packageName="@vendure/core" />
 
 
 Contains methods relating to <a href='/reference/typescript-api/entities/collection#collection'>Collection</a> entities.
 Contains methods relating to <a href='/reference/typescript-api/entities/collection#collection'>Collection</a> entities.
 
 
@@ -25,7 +25,7 @@ class CollectionService implements OnModuleInit {
     getAvailableFilters(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getAvailableFilters(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getParent(ctx: RequestContext, collectionId: ID) => Promise<Collection | undefined>;
     getParent(ctx: RequestContext, collectionId: ID) => Promise<Collection | undefined>;
     getChildren(ctx: RequestContext, collectionId: ID) => Promise<Collection[]>;
     getChildren(ctx: RequestContext, collectionId: ID) => Promise<Collection[]>;
-    getBreadcrumbs(ctx: RequestContext, collection: Collection) => Promise<Array<{ name: string; id: ID }>>;
+    getBreadcrumbs(ctx: RequestContext, collection: Collection) => Promise<Array<{ name: string; id: ID, slug: string }>>;
     getCollectionsByProductId(ctx: RequestContext, productId: ID, publicOnly: boolean) => Promise<Array<Translated<Collection>>>;
     getCollectionsByProductId(ctx: RequestContext, productId: ID, publicOnly: boolean) => Promise<Array<Translated<Collection>>>;
     getDescendants(ctx: RequestContext, rootId: ID, maxDepth: number = Number.MAX_SAFE_INTEGER) => Promise<Array<Translated<Collection>>>;
     getDescendants(ctx: RequestContext, rootId: ID, maxDepth: number = Number.MAX_SAFE_INTEGER) => Promise<Array<Translated<Collection>>>;
     getAncestors(collectionId: ID) => Promise<Collection[]>;
     getAncestors(collectionId: ID) => Promise<Collection[]>;
@@ -89,7 +89,7 @@ Returns all configured CollectionFilters, as specified by the <a href='/referenc
 Returns all child Collections of the Collection with the given id.
 Returns all child Collections of the Collection with the given id.
 ### getBreadcrumbs
 ### getBreadcrumbs
 
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, collection: <a href='/reference/typescript-api/entities/collection#collection'>Collection</a>) => Promise&#60;Array&#60;{ name: string; id: <a href='/reference/typescript-api/common/id#id'>ID</a> }&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, collection: <a href='/reference/typescript-api/entities/collection#collection'>Collection</a>) => Promise&#60;Array&#60;{ name: string; id: <a href='/reference/typescript-api/common/id#id'>ID</a>, slug: string }&#62;&#62;`}   />
 
 
 Returns an array of name/id pairs representing all ancestor Collections up
 Returns an array of name/id pairs representing all ancestor Collections up
 to the Root Collection.
 to the Root Collection.

+ 1 - 1
docs/docs/reference/typescript-api/shipping/shipping-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## ShippingOptions
 ## ShippingOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="753" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="752" packageName="@vendure/core" />
 
 
 
 
 
 

+ 1 - 1
docs/docs/reference/typescript-api/tax/tax-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## TaxOptions
 ## TaxOptions
 
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="866" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="865" packageName="@vendure/core" />
 
 
 
 
 
 

+ 0 - 4
docs/docs/user-guide/catalog/index.md

@@ -1,4 +0,0 @@
----
-title: "Catalog"
-weight: 1
----

+ 2 - 3
docs/docs/user-guide/catalog/products.md

@@ -1,6 +1,5 @@
 ---
 ---
 title: "Products"
 title: "Products"
-weight: 0
 ---
 ---
 
 
 # Products
 # Products
@@ -21,7 +20,7 @@ In the diagram above you'll notice that it is the ProductVariants which have an
 
 
 ## Tracking Inventory
 ## Tracking Inventory
 
 
-Vendure can track the stock levels of each of your ProductVariants. This is done by setting the "track inventory" option to "track" (or "inherit from global settings" if the [global setting]({{< relref "/user-guide/settings/global-settings" >}}) is set to track).
+Vendure can track the stock levels of each of your ProductVariants. This is done by setting the "track inventory" option to "track" (or "inherit from global settings" if the [global setting](/user-guide/settings/global-settings) is set to track).
 
 
 ![./screen-inventory.webp](./screen-inventory.webp)
 ![./screen-inventory.webp](./screen-inventory.webp)
 
 
@@ -33,4 +32,4 @@ When tracking inventory:
 
 
 ### Back orders
 ### Back orders
 
 
-Back orders can be enabled by setting a **negative value** as the "Out-of-stock threshold". This can be done via [global settings]({{< relref "/user-guide/settings/global-settings" >}}) or on a per-variant basis.
+Back orders can be enabled by setting a **negative value** as the "Out-of-stock threshold". This can be done via [global settings](/user-guide/settings/global-settings) or on a per-variant basis.

+ 1 - 6
docs/docs/user-guide/index.md

@@ -1,9 +1,4 @@
----
-title: "Administrator Guide"
-weight: 1
----
- 
-# Administrator Guide
+# Vendure User Guide
 
 
 This section is for store owners and staff who are charged with running a Vendure-based store.
 This section is for store owners and staff who are charged with running a Vendure-based store.
 
 

+ 1 - 1
docs/docs/user-guide/localization/index.md

@@ -29,7 +29,7 @@ Vendure supports **admin-facing** (Admin API and Admin UI) localization by allow
 
 
 ## How to enable languages
 ## How to enable languages
 
 
-To select the set of languages you wish to create translations for, set them in the [global settings]({{< relref "/user-guide/settings/global-settings" >}}).
+To select the set of languages you wish to create translations for, set them in the [global settings](/user-guide/settings/global-settings).
 
 
 Once more than one language is enabled, you will see a language switcher appear when editing the object types listed above.
 Once more than one language is enabled, you will see a language switcher appear when editing the object types listed above.
 
 

+ 2 - 6
docs/docs/user-guide/orders/draft-orders.md

@@ -4,10 +4,6 @@ title: "Draft Orders"
 
 
 # Draft Orders
 # Draft Orders
 
 
-{{% alert "warning" %}}
-Note: Draft Orders are available from Vendure v1.8+
-{{% /alert %}}
-
 Draft Orders are used when an Administrator would like to manually create an order via the Admin UI. For example, this can be useful when:
 Draft Orders are used when an Administrator would like to manually create an order via the Admin UI. For example, this can be useful when:
 
 
 - A customer phones up to place an order
 - A customer phones up to place an order
@@ -26,7 +22,7 @@ From there you can:
 
 
 Once ready, click the **"Complete draft"** button to convert this Order from a Draft into a regular Order. At this stage the order can be paid for, and you can manually record the payment details.
 Once ready, click the **"Complete draft"** button to convert this Order from a Draft into a regular Order. At this stage the order can be paid for, and you can manually record the payment details.
 
 
-{{% alert "primary" %}}
+:::note
 Note: Draft Orders do not appear in a Customer's order history in the storefront (Shop API) while still
 Note: Draft Orders do not appear in a Customer's order history in the storefront (Shop API) while still
 in the "Draft" state.
 in the "Draft" state.
-{{% /alert %}}
+:::

+ 4 - 4
docs/docs/user-guide/promotions/index.md

@@ -16,8 +16,8 @@ A condition defines the criteria that must be met for the Promotion to be activa
 
 
 * If the order total is at least $X
 * If the order total is at least $X
 * Buy at least X of a certain product
 * Buy at least X of a certain product
-* But at least X of any product with the specified [FacetValues]({{< relref "/user-guide/catalog/facets" >}})
-* If the customer is a member of the specified [Customer Group]({{< relref "/user-guide/customers" >}}#customer-groups)
+* But at least X of any product with the specified [FacetValues](/user-guide/catalog/facets)
+* If the customer is a member of the specified [Customer Group](/user-guide/customers#customer-groups)
 
 
 Vendure allows completely custom conditions to be defined by your developers, implementing the specific logic needed by your business.
 Vendure allows completely custom conditions to be defined by your developers, implementing the specific logic needed by your business.
 
 
@@ -26,9 +26,9 @@ Vendure allows completely custom conditions to be defined by your developers, im
 A coupon code can be any text which will activate a Promotion. A coupon code can be used in conjunction with conditions if desired.
 A coupon code can be any text which will activate a Promotion. A coupon code can be used in conjunction with conditions if desired.
 
 
 
 
-{{< alert "primary" >}}
+:::note
 Note: Promotions **must** have either a **coupon code** _or_ **at least 1 condition** defined.
 Note: Promotions **must** have either a **coupon code** _or_ **at least 1 condition** defined.
-{{< /alert >}}
+:::
 
 
 ## Promotion Actions
 ## Promotion Actions
 
 

+ 1 - 1
docs/docs/user-guide/settings/countries-zones.md

@@ -8,5 +8,5 @@ title: "Countries & Zones"
 
 
 By default, Vendure includes all countries in the list, but you are free to remove or disable any that you don't need.
 By default, Vendure includes all countries in the list, but you are free to remove or disable any that you don't need.
 
 
-**Zones** provide a way to group countries. Zones are used mainly for defining [tax rates]({{< relref "/user-guide/settings/taxes" >}}) and can also be used in shipping calculations.
+**Zones** provide a way to group countries. Zones are used mainly for defining [tax rates](/user-guide/settings/taxes) and can also be used in shipping calculations.
 
 

+ 2 - 2
docs/docs/user-guide/settings/global-settings.md

@@ -8,5 +8,5 @@ The global settings allow you to define certain configurations that affect _all_
 
 
 * **Available languages** defines which languages you wish to make available for translations. When more than one language has been enabled, you will see the language switcher appear when viewing translatable objects such as products, collections, facets and shipping methods.
 * **Available languages** defines which languages you wish to make available for translations. When more than one language has been enabled, you will see the language switcher appear when viewing translatable objects such as products, collections, facets and shipping methods.
   ![./screen-translations.webp](./screen-translations.webp)
   ![./screen-translations.webp](./screen-translations.webp)
-* **Global out-of-stock threshold** sets the stock level at which a product variant is considered to be out of stock. Using a negative value enables backorder support. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/user-guide/catalog/products" >}}#tracking-inventory) guide).
-* **Track inventory by default** sets whether stock levels should be tracked. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/user-guide/catalog/products" >}}#tracking-inventory) guide).
+* **Global out-of-stock threshold** sets the stock level at which a product variant is considered to be out of stock. Using a negative value enables backorder support. This setting can be overridden by individual product variants (see the [tracking inventory](/user-guide/catalog/products#tracking-inventory) guide).
+* **Track inventory by default** sets whether stock levels should be tracked. This setting can be overridden by individual product variants (see the [tracking inventory](/user-guide/catalog/products#tracking-inventory) guide).

+ 1 - 1
docs/docs/user-guide/settings/taxes.md

@@ -29,4 +29,4 @@ Tax rates set the rate of tax for a given **tax category** destined for a partic
 
 
 ## Tax Compliance
 ## Tax Compliance
 
 
-Please note that tax compliance is a complex topic that varies significantly between countries. Vendure does not (and cannot) offer a complete out-of-the-box tax solution which is guaranteed to be compliant with your use-case. What we strive to do is to provide a very flexible set of tools that your developers can use to tailor tax calculations exactly to your needs. These are covered in the [Developer's guide to taxes]({{< relref "/guides/developer-guide/taxes" >}}). 
+Please note that tax compliance is a complex topic that varies significantly between countries. Vendure does not (and cannot) offer a complete out-of-the-box tax solution which is guaranteed to be compliant with your use-case. What we strive to do is to provide a very flexible set of tools that your developers can use to tailor tax calculations exactly to your needs. These are covered in the [Developer's guide to taxes](/guides/core-concepts/taxes/). 

+ 7 - 3
docs/docusaurus.config.js

@@ -45,9 +45,7 @@ const config = {
                         keywords: ['cli'],
                         keywords: ['cli'],
                         extendDefaults: true,
                         extendDefaults: true,
                     },
                     },
-                    exclude: [
-                        'user-guide/**/*'
-                    ]
+                    // exclude: ['user-guide/**/*'],
                 },
                 },
                 blog: false,
                 blog: false,
                 theme: {
                 theme: {
@@ -92,6 +90,12 @@ const config = {
                         position: 'left',
                         position: 'left',
                         label: 'Reference',
                         label: 'Reference',
                     },
                     },
+                    {
+                        type: 'docSidebar',
+                        sidebarId: 'userGuideSidebar',
+                        position: 'left',
+                        label: 'User Guide',
+                    },
                     {
                     {
                         href: 'https://vendure.io',
                         href: 'https://vendure.io',
                         label: 'vendure.io',
                         label: 'vendure.io',

+ 32 - 0
docs/sidebars.js

@@ -315,6 +315,38 @@ const sidebars = {
             },
             },
         },
         },
     ],
     ],
+    userGuideSidebar: [
+        {
+            type: 'doc',
+            id: 'user-guide/index',
+            className: 'reference-index',
+        },
+        {
+            type: 'category',
+            label: 'Catalog',
+            items: [{ type: 'autogenerated', dirName: 'user-guide/catalog' }],
+        },
+        {
+            type: 'category',
+            label: 'Orders',
+            items: [{ type: 'autogenerated', dirName: 'user-guide/orders' }],
+        },
+        {
+            type: 'category',
+            label: 'Customers',
+            items: [{ type: 'autogenerated', dirName: 'user-guide/customers' }],
+        },
+        {
+            type: 'category',
+            label: 'Promotions',
+            items: [{ type: 'autogenerated', dirName: 'user-guide/promotions' }],
+        },
+        {
+            type: 'category',
+            label: 'Settings',
+            items: [{ type: 'autogenerated', dirName: 'user-guide/settings' }],
+        },
+    ],
 };
 };
 
 
 module.exports = sidebars;
 module.exports = sidebars;

+ 64 - 0
license/signatures/version1/cla.json

@@ -39,6 +39,70 @@
       "created_at": "2024-07-19T06:53:24Z",
       "created_at": "2024-07-19T06:53:24Z",
       "repoId": 136938012,
       "repoId": 136938012,
       "pullRequestNo": 2964
       "pullRequestNo": 2964
+    },
+    {
+      "name": "williamrijksen",
+      "id": 1991582,
+      "comment_id": 2250303402,
+      "created_at": "2024-07-25T13:17:48Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2972
+    },
+    {
+      "name": "monrostar",
+      "id": 13255191,
+      "comment_id": 2255645619,
+      "created_at": "2024-07-29T11:09:26Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2978
+    },
+    {
+      "name": "jacobfrantz1",
+      "id": 69358280,
+      "comment_id": 2258961522,
+      "created_at": "2024-07-30T18:31:00Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2982
+    },
+    {
+      "name": "arrrrny",
+      "id": 48218623,
+      "comment_id": 2262149965,
+      "created_at": "2024-08-01T06:25:53Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2987
+    },
+    {
+      "name": "casperiv0",
+      "id": 53900565,
+      "comment_id": 2267440518,
+      "created_at": "2024-08-04T08:52:46Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2993
+    },
+    {
+      "name": "DanielBiegler",
+      "id": 14810858,
+      "comment_id": 2267490248,
+      "created_at": "2024-08-04T10:20:47Z",
+      "repoId": 136938012,
+      "pullRequestNo": 2994
+    },
+    {
+      "name": "dfernandesbsolus",
+      "id": 96054351,
+      "comment_id": 2277331192,
+      "created_at": "2024-08-09T07:30:28Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3002
+    },
+    {
+      "name": "Dominic-Preap",
+      "id": 14802170,
+      "comment_id": 2281685611,
+      "created_at": "2024-08-10T13:11:42Z",
+      "repoId": 136938012,
+      "pullRequestNo": 3007
     }
     }
   ]
   ]
 }
 }

+ 0 - 26
package-lock.json

@@ -9893,11 +9893,6 @@
             "dev": true,
             "dev": true,
             "license": "MIT"
             "license": "MIT"
         },
         },
-        "node_modules/@types/detect-port": {
-            "version": "1.3.5",
-            "dev": true,
-            "license": "MIT"
-        },
         "node_modules/@types/duplexify": {
         "node_modules/@types/duplexify": {
             "version": "3.6.4",
             "version": "3.6.4",
             "dev": true,
             "dev": true,
@@ -11588,13 +11583,6 @@
             "dev": true,
             "dev": true,
             "license": "MIT"
             "license": "MIT"
         },
         },
-        "node_modules/address": {
-            "version": "1.2.2",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 10.0.0"
-            }
-        },
         "node_modules/adjust-sourcemap-loader": {
         "node_modules/adjust-sourcemap-loader": {
             "version": "4.0.0",
             "version": "4.0.0",
             "license": "MIT",
             "license": "MIT",
@@ -15031,18 +15019,6 @@
             "version": "2.1.0",
             "version": "2.1.0",
             "license": "MIT"
             "license": "MIT"
         },
         },
-        "node_modules/detect-port": {
-            "version": "1.5.1",
-            "license": "MIT",
-            "dependencies": {
-                "address": "^1.0.1",
-                "debug": "4"
-            },
-            "bin": {
-                "detect": "bin/detect-port.js",
-                "detect-port": "bin/detect-port.js"
-            }
-        },
         "node_modules/dev-server": {
         "node_modules/dev-server": {
             "resolved": "packages/dev-server",
             "resolved": "packages/dev-server",
             "link": true
             "link": true
@@ -32329,7 +32305,6 @@
                 "@vendure/common": "^3.0.0",
                 "@vendure/common": "^3.0.0",
                 "commander": "^11.0.0",
                 "commander": "^11.0.0",
                 "cross-spawn": "^7.0.3",
                 "cross-spawn": "^7.0.3",
-                "detect-port": "^1.5.1",
                 "fs-extra": "^11.2.0",
                 "fs-extra": "^11.2.0",
                 "handlebars": "^4.7.8",
                 "handlebars": "^4.7.8",
                 "picocolors": "^1.0.0",
                 "picocolors": "^1.0.0",
@@ -32341,7 +32316,6 @@
             },
             },
             "devDependencies": {
             "devDependencies": {
                 "@types/cross-spawn": "^6.0.6",
                 "@types/cross-spawn": "^6.0.6",
-                "@types/detect-port": "^1.3.5",
                 "@types/fs-extra": "^11.0.4",
                 "@types/fs-extra": "^11.0.4",
                 "@types/handlebars": "^4.1.0",
                 "@types/handlebars": "^4.1.0",
                 "@types/semver": "^7.5.8",
                 "@types/semver": "^7.5.8",

+ 4 - 5
packages/admin-ui/src/lib/catalog/src/components/create-product-variant-dialog/create-product-variant-dialog.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { FormBuilder, FormControl, FormGroup, FormRecord, Validators } from '@angular/forms';
+import { FormBuilder, FormControl, FormRecord, Validators } from '@angular/forms';
 import {
 import {
     CreateProductVariantInput,
     CreateProductVariantInput,
     CurrencyCode,
     CurrencyCode,
@@ -7,7 +7,6 @@ import {
     GetProductVariantOptionsQuery,
     GetProductVariantOptionsQuery,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { combineLatest } from 'rxjs';
 
 
 @Component({
 @Component({
     selector: 'vdr-create-product-variant-dialog',
     selector: 'vdr-create-product-variant-dialog',
@@ -20,7 +19,7 @@ export class CreateProductVariantDialogComponent implements Dialog<CreateProduct
     product: NonNullable<GetProductVariantOptionsQuery['product']>;
     product: NonNullable<GetProductVariantOptionsQuery['product']>;
     form = this.formBuilder.group({
     form = this.formBuilder.group({
         name: ['', Validators.required],
         name: ['', Validators.required],
-        sku: ['', Validators.required],
+        sku: [''],
         price: [''],
         price: [''],
         options: this.formBuilder.record<string>({}),
         options: this.formBuilder.record<string>({}),
     });
     });
@@ -67,14 +66,14 @@ export class CreateProductVariantDialogComponent implements Dialog<CreateProduct
 
 
     confirm() {
     confirm() {
         const { name, sku, options, price } = this.form.value;
         const { name, sku, options, price } = this.form.value;
-        if (!name || !sku || !options || price == null) {
+        if (!name || !options || price == null) {
             return;
             return;
         }
         }
 
 
         const optionIds = Object.values(options).filter(notNullOrUndefined);
         const optionIds = Object.values(options).filter(notNullOrUndefined);
         this.resolveWith({
         this.resolveWith({
             productId: this.product.id,
             productId: this.product.id,
-            sku,
+            sku: sku || '',
             price: Number(price),
             price: Number(price),
             optionIds,
             optionIds,
             translations: [
             translations: [

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss

@@ -53,7 +53,7 @@ vdr-action-bar clr-toggle-wrapper {
 
 
 .channel-assignment {
 .channel-assignment {
     flex-wrap: wrap;
     flex-wrap: wrap;
-    max-height: 144px;
+    min-height: 24px;
 }
 }
 
 
 .pagination-row {
 .pagination-row {

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.spec.ts

@@ -23,11 +23,11 @@ describe('LocaleDatePipe', () => {
 
 
     it('medium format German', () => {
     it('medium format German', () => {
         const pipe = new LocaleDatePipe();
         const pipe = new LocaleDatePipe();
-        expect(pipe.transform(testDate, 'medium', LanguageCode.de)).toBe('12. Jan. 2021, 9:12:42 AM');
+        expect(pipe.transform(testDate, 'medium', LanguageCode.de)).toBe('12. Jan. 2021, 09:12:42');
     });
     });
 
 
     it('medium format Chinese', () => {
     it('medium format Chinese', () => {
         const pipe = new LocaleDatePipe();
         const pipe = new LocaleDatePipe();
-        expect(pipe.transform(testDate, 'medium', LanguageCode.zh)).toBe('2021年1月12日 上午9:12:42');
+        expect(pipe.transform(testDate, 'medium', LanguageCode.zh)).toBe('2021年1月12日 09:12:42');
     });
     });
 });
 });

+ 0 - 3
packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.ts

@@ -47,14 +47,12 @@ export class LocaleDatePipe extends LocaleBasePipe implements PipeTransform {
                     hour: 'numeric',
                     hour: 'numeric',
                     minute: 'numeric',
                     minute: 'numeric',
                     second: 'numeric',
                     second: 'numeric',
-                    hour12: true,
                 };
                 };
             case 'mediumTime':
             case 'mediumTime':
                 return {
                 return {
                     hour: 'numeric',
                     hour: 'numeric',
                     minute: 'numeric',
                     minute: 'numeric',
                     second: 'numeric',
                     second: 'numeric',
-                    hour12: true,
                 };
                 };
             case 'longDate':
             case 'longDate':
                 return {
                 return {
@@ -75,7 +73,6 @@ export class LocaleDatePipe extends LocaleBasePipe implements PipeTransform {
                     year: '2-digit',
                     year: '2-digit',
                     hour: 'numeric',
                     hour: 'numeric',
                     minute: 'numeric',
                     minute: 'numeric',
-                    hour12: true,
                 };
                 };
             default:
             default:
                 return;
                 return;

+ 8 - 8
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -28,7 +28,7 @@
     "administrators": "Administradores",
     "administrators": "Administradores",
     "assets": "Imagens",
     "assets": "Imagens",
     "channels": "Canais",
     "channels": "Canais",
-    "collections": "Categorias",
+    "collections": "Coleções",
     "countries": "Países",
     "countries": "Países",
     "customer-groups": "Grupos de cliente",
     "customer-groups": "Grupos de cliente",
     "customers": "Clientes",
     "customers": "Clientes",
@@ -76,7 +76,7 @@
     "cannot-create-variants-without-options": "As variantes do produto não podem ser criadas até que um grupo de opções com pelo menos duas opções de produtos tenha sido definidas",
     "cannot-create-variants-without-options": "As variantes do produto não podem ser criadas até que um grupo de opções com pelo menos duas opções de produtos tenha sido definidas",
     "channel-price-preview": "Visualizar preço do canal",
     "channel-price-preview": "Visualizar preço do canal",
     "collection": "Coleçāo",
     "collection": "Coleçāo",
-    "collection-contents": "Conteúdo da categoria",
+    "collection-contents": "Conteúdo da coleção",
     "collections": "Coleções",
     "collections": "Coleções",
     "confirm-bulk-delete-products": "Excluir {count} produtos?",
     "confirm-bulk-delete-products": "Excluir {count} produtos?",
     "confirm-cancel": "Cancelar?",
     "confirm-cancel": "Cancelar?",
@@ -91,7 +91,7 @@
     "confirm-deletion-of-unused-variants-title": "Excluir variantes de produtos obsoletos?",
     "confirm-deletion-of-unused-variants-title": "Excluir variantes de produtos obsoletos?",
     "create-draft-order": "Criar rascunho de pedido",
     "create-draft-order": "Criar rascunho de pedido",
     "create-facet-value": "Criar novo valor para etiqueta",
     "create-facet-value": "Criar novo valor para etiqueta",
-    "create-new-collection": "Criar nova categoria",
+    "create-new-collection": "Criar nova coleção",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-product": "Novo produto",
     "create-new-product": "Novo produto",
     "create-new-stock-location": "Criar nova localização de estoque",
     "create-new-stock-location": "Criar nova localização de estoque",
@@ -274,7 +274,7 @@
     "name": "Nome",
     "name": "Nome",
     "no-alerts": "Sem alertas",
     "no-alerts": "Sem alertas",
     "no-bulk-actions-available": "Nenhuma ação em massa disponível",
     "no-bulk-actions-available": "Nenhuma ação em massa disponível",
-    "no-channel-selected": "",
+    "no-channel-selected": "Nenhum canal selecionado",
     "no-results": "Sem resultados",
     "no-results": "Sem resultados",
     "not-applicable": "Não aplicável",
     "not-applicable": "Não aplicável",
     "not-set": "Não configurado",
     "not-set": "Não configurado",
@@ -480,10 +480,10 @@
     "image-title": "Título",
     "image-title": "Título",
     "insert-image": "Inserir imagem",
     "insert-image": "Inserir imagem",
     "link-href": "Link de referência",
     "link-href": "Link de referência",
-    "link-target": "",
+    "link-target": "Escolha onde abrir o link:",
     "link-title": "Título do link",
     "link-title": "Título do link",
     "remove-link": "Remover",
     "remove-link": "Remover",
-    "set-link": "Setar link"
+    "set-link": "Definir link"
   },
   },
   "error": {
   "error": {
     "403-forbidden": "No momento, você não está autorizado a acessar \"{ path }\". Você não tem permissão ou sua sessão expirou.",
     "403-forbidden": "No momento, você não está autorizado a acessar \"{ path }\". Você não tem permissão ou sua sessão expirou.",
@@ -514,7 +514,7 @@
     "assets": "Imagens",
     "assets": "Imagens",
     "catalog": "Catálogo",
     "catalog": "Catálogo",
     "channels": "Canais",
     "channels": "Canais",
-    "collections": "Categorias",
+    "collections": "Coleções",
     "countries": "Países",
     "countries": "Países",
     "customer-groups": "Grupos de clientes",
     "customer-groups": "Grupos de clientes",
     "customers": "Clientes",
     "customers": "Clientes",
@@ -675,7 +675,7 @@
     "set-billing-address": "Definir endereço de faturamento",
     "set-billing-address": "Definir endereço de faturamento",
     "set-coupon-codes": "Definir cupons de desconto",
     "set-coupon-codes": "Definir cupons de desconto",
     "set-customer-for-order": "Definir cliente",
     "set-customer-for-order": "Definir cliente",
-    "set-customer-success": "",
+    "set-customer-success": "Cliente definido com sucesso",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",
     "set-shipping-address": "Definir endereço de entrega",
     "set-shipping-address": "Definir endereço de entrega",
     "set-shipping-method": "Definir método de envio",
     "set-shipping-method": "Definir método de envio",

+ 160 - 160
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -28,7 +28,7 @@
     "administrators": "Administradores",
     "administrators": "Administradores",
     "assets": "Imagens",
     "assets": "Imagens",
     "channels": "Canais",
     "channels": "Canais",
-    "collections": "Categorias",
+    "collections": "Colecções",
     "countries": "Países",
     "countries": "Países",
     "customer-groups": "Grupos de cliente",
     "customer-groups": "Grupos de cliente",
     "customers": "Clientes",
     "customers": "Clientes",
@@ -45,8 +45,8 @@
     "profile": "Perfil",
     "profile": "Perfil",
     "promotions": "Promoções",
     "promotions": "Promoções",
     "roles": "Regras",
     "roles": "Regras",
-    "seller-orders": "",
-    "sellers": "",
+    "seller-orders": "Encomendas de vendedores",
+    "sellers": "Vendedores",
     "shipping-methods": "Métodos de envio",
     "shipping-methods": "Métodos de envio",
     "stock-locations": "Localizações de stock",
     "stock-locations": "Localizações de stock",
     "system-status": "Estado do sistema",
     "system-status": "Estado do sistema",
@@ -58,12 +58,12 @@
     "add-facet-value": "Adicionar novo valor",
     "add-facet-value": "Adicionar novo valor",
     "add-facets": "Adicionar etiqueta",
     "add-facets": "Adicionar etiqueta",
     "add-option": "Adicionar opção",
     "add-option": "Adicionar opção",
-    "add-price-in-another-currency": "",
-    "add-stock-location": "",
-    "add-stock-to-location": "",
-    "asset": "",
-    "asset-preview-links": "",
-    "assets": "",
+    "add-price-in-another-currency": "Adicionar preço em outra moeda",
+    "add-stock-location": "Adicionar Localização de stock",
+    "add-stock-to-location": "Adicionar Stock a localização",
+    "asset": "Imagem",
+    "asset-preview-links": "Links de pré vizualização de imagens",
+    "assets": "Imagens",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-named-channel": "Atribuir a { channelCode }",
     "assign-to-named-channel": "Atribuir a { channelCode }",
@@ -73,74 +73,74 @@
     "auto-update-product-variant-name": "Actualizar automaticamente os nomes das variantes do produto",
     "auto-update-product-variant-name": "Actualizar automaticamente os nomes das variantes do produto",
     "calculated-price": "Preço calculado",
     "calculated-price": "Preço calculado",
     "calculated-price-tooltip": "Existe um cálculo de preço personalizado configurado que modifica o preço definido acima:",
     "calculated-price-tooltip": "Existe um cálculo de preço personalizado configurado que modifica o preço definido acima:",
-    "cannot-create-variants-without-options": "",
+    "cannot-create-variants-without-options": "Não é possível criar variantes do produto sem opções",
     "channel-price-preview": "Visualizar preço do canal",
     "channel-price-preview": "Visualizar preço do canal",
-    "collection": "",
-    "collection-contents": "Conteúdo da categoria",
-    "collections": "",
-    "confirm-bulk-delete-products": "",
-    "confirm-cancel": "",
+    "collection": "Colecção",
+    "collection-contents": "Conteúdo da colecção",
+    "collections": "Colecções",
+    "confirm-bulk-delete-products": "Confirmar apagar produtos em massa",
+    "confirm-cancel": "Confirmar cancelamento",
     "confirm-delete-assets": "Eliminar {count} {count, plural, one {imagem} other {imagens}}?",
     "confirm-delete-assets": "Eliminar {count} {count, plural, one {imagem} other {imagens}}?",
     "confirm-delete-facet-value": "Eliminar valor da etiqueta?",
     "confirm-delete-facet-value": "Eliminar valor da etiqueta?",
     "confirm-delete-product": "Eliminar produto?",
     "confirm-delete-product": "Eliminar produto?",
-    "confirm-delete-product-option": "",
-    "confirm-delete-product-option-group": "",
-    "confirm-delete-product-option-group-body": "",
+    "confirm-delete-product-option": "Eliminar opção de produto?",
+    "confirm-delete-product-option-group": "Eliminar grupo de opção de produto?",
+    "confirm-delete-product-option-group-body": "Eliminar grupo de opção de produto?",
     "confirm-delete-product-variant": "Eliminar variante do produto?",
     "confirm-delete-product-variant": "Eliminar variante do produto?",
     "confirm-deletion-of-unused-variants-body": "As variantes listadas abaixo estão obsoletas e serão eliminadas devido à adição de novas opções.",
     "confirm-deletion-of-unused-variants-body": "As variantes listadas abaixo estão obsoletas e serão eliminadas devido à adição de novas opções.",
     "confirm-deletion-of-unused-variants-title": "Eliminar as variantes obsoletas?",
     "confirm-deletion-of-unused-variants-title": "Eliminar as variantes obsoletas?",
-    "create-draft-order": "",
-    "create-facet-value": "",
-    "create-new-collection": "Criar nova categoria",
+    "create-draft-order": "Criar ordem rascunho",
+    "create-facet-value": "Criar valor de imagem",
+    "create-new-collection": "Criar nova colecção",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-product": "Novo produto",
     "create-new-product": "Novo produto",
-    "create-new-stock-location": "",
-    "create-product-option-group": "",
-    "create-product-variant": "",
-    "default-currency": "",
-    "do-not-inherit-filters": "",
+    "create-new-stock-location": "Criar nova localização de stock",
+    "create-product-option-group": "Criar grupo de opção de produto",
+    "create-product-variant": "Criar variante de produto",
+    "default-currency": "Moeda padrão",
+    "do-not-inherit-filters": "Não herdar filtros",
     "drop-files-to-upload": "Colocar ficheiros para enviar",
     "drop-files-to-upload": "Colocar ficheiros para enviar",
-    "duplicate-collections": "Duplicar coleções",
+    "duplicate-collections": "Duplicar colecções",
     "duplicate-facets": "Duplicar facetas",
     "duplicate-facets": "Duplicar facetas",
     "duplicate-products": "Duplicar produtos",
     "duplicate-products": "Duplicar produtos",
-    "edit-facet-values": "",
-    "edit-options": "",
-    "facet": "",
-    "facet-value-not-available": "",
+    "edit-facet-values": "Editar etiquetas",
+    "edit-options": "Editar Opções",
+    "facet": "Etiqueta",
+    "facet-value-not-available": "Valor da etiqueta não disponível",
     "facet-values": "Valor da Etiqueta",
     "facet-values": "Valor da Etiqueta",
-    "facets": "",
+    "facets": "Etiquetas",
     "filter-by-name": "Filtrar por nome",
     "filter-by-name": "Filtrar por nome",
-    "filter-inheritance": "",
+    "filter-inheritance": "Herdar filtros",
     "filters": "Filtros",
     "filters": "Filtros",
-    "inherit-filters-from-parent": "",
-    "live-preview-contents": "",
+    "inherit-filters-from-parent": "Herdar filtros do produto pai",
+    "live-preview-contents": "Pré vizualizar conteúdos",
     "manage-variants": "Gerir variações",
     "manage-variants": "Gerir variações",
-    "move-collection-to": "",
-    "move-collections": "",
-    "move-collections-success": "",
+    "move-collection-to": "Mover colecção para",
+    "move-collections": "Mover Colecções",
+    "move-collections-success": "Colecções movidas com sucesso",
     "move-down": "Mover para baixo",
     "move-down": "Mover para baixo",
     "move-to": "Mover para",
     "move-to": "Mover para",
     "move-up": "Mover para cima",
     "move-up": "Mover para cima",
-    "name": "",
+    "name": "Nome",
     "no-channel-selected": "Nenhum canal seleccionado",
     "no-channel-selected": "Nenhum canal seleccionado",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-selection": "Nenhuma imagem seleccionada",
     "no-selection": "Nenhuma imagem seleccionada",
-    "no-stock-locations-available-on-current-channel": "",
-    "notify-bulk-delete-products-success": "",
-    "notify-remove-facets-from-channel-success": "",
+    "no-stock-locations-available-on-current-channel": "Nenhuma localização de stock disponível neste canal",
+    "notify-bulk-delete-products-success": "Produtos em bulk apagados com sucesso",
+    "notify-remove-facets-from-channel-success": "Etiquetas removidas do canal com sucesso",
     "notify-remove-product-from-channel-error": "Não foi possível remover o produto do canal",
     "notify-remove-product-from-channel-error": "Não foi possível remover o produto do canal",
     "notify-remove-product-from-channel-success": "Produto removido do canal com sucesso",
     "notify-remove-product-from-channel-success": "Produto removido do canal com sucesso",
     "notify-remove-variant-from-channel-error": "Erro ao remover a variante do canal",
     "notify-remove-variant-from-channel-error": "Erro ao remover a variante do canal",
     "notify-remove-variant-from-channel-success": "Variante removida do canal com sucesso",
     "notify-remove-variant-from-channel-success": "Variante removida do canal com sucesso",
-    "number-of-variants": "",
+    "number-of-variants": "Quantidade de variantes",
     "option": "Opção",
     "option": "Opção",
     "option-name": "Nome da opção",
     "option-name": "Nome da opção",
     "option-values": "Valor da opção",
     "option-values": "Valor da opção",
     "out-of-stock-threshold": "Limite para fora de estoque",
     "out-of-stock-threshold": "Limite para fora de estoque",
     "out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes.",
     "out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes.",
-    "page-description-options-editor": "",
+    "page-description-options-editor": "Editar opções de descrição de página",
     "price": "Preço",
     "price": "Preço",
-    "price-and-tax": "",
+    "price-and-tax": "Preço e impostos",
     "price-conversion-factor": "Factor de conversão de preço",
     "price-conversion-factor": "Factor de conversão de preço",
     "price-in-channel": "Preço no canal { channel }",
     "price-in-channel": "Preço no canal { channel }",
     "price-includes-tax-at": "Inclui { rate }% de imposto",
     "price-includes-tax-at": "Inclui { rate }% de imposto",
@@ -153,7 +153,7 @@
     "product-variants": "Variações do produto",
     "product-variants": "Variações do produto",
     "products": "Produtos",
     "products": "Produtos",
     "public": "Público",
     "public": "Público",
-    "quick-jump-placeholder": "",
+    "quick-jump-placeholder": "Avanço rápido",
     "rebuild-search-index": "Reconstruir índice de pesquisa",
     "rebuild-search-index": "Reconstruir índice de pesquisa",
     "reindex-error": "Ocorreu um erro ao reconstruir o índice de pesquisa",
     "reindex-error": "Ocorreu um erro ao reconstruir o índice de pesquisa",
     "reindex-successful": "{count, plural, one {Variante do produto indexada} other {{count} variantes de produtos indexadas}} em {time}ms",
     "reindex-successful": "{count, plural, one {Variante do produto indexada} other {{count} variantes de produtos indexadas}} em {time}ms",
@@ -162,9 +162,9 @@
     "remove-option": "Eliminar opção",
     "remove-option": "Eliminar opção",
     "remove-product-from-channel": "Eliminar produto do canal",
     "remove-product-from-channel": "Eliminar produto do canal",
     "remove-product-variant-from-channel": "Remover variante do canal?",
     "remove-product-variant-from-channel": "Remover variante do canal?",
-    "reorder-collection": "",
-    "root-collection": "",
-    "run-pending-search-index-updates": "",
+    "reorder-collection": "Reordenar colecção",
+    "root-collection": "Voltar atrás na colecçáo",
+    "run-pending-search-index-updates": "Fazer update a indexações de pesquisa pendentes",
     "running-search-index-updates": "A executar {count, plural, one {1 actualização} other {{count} actualizações}}",
     "running-search-index-updates": "A executar {count, plural, one {1 actualização} other {{count} actualizações}}",
     "search-asset-name-or-tag": "Pesquisar pelo nome ou tag",
     "search-asset-name-or-tag": "Pesquisar pelo nome ou tag",
     "search-for-term": "Pesquisar termo",
     "search-for-term": "Pesquisar termo",
@@ -175,7 +175,7 @@
     "slug": "Slug",
     "slug": "Slug",
     "slug-pattern-error": "Slug inválido",
     "slug-pattern-error": "Slug inválido",
     "stock-allocated": "Estoque reservado",
     "stock-allocated": "Estoque reservado",
-    "stock-levels": "",
+    "stock-levels": "Níveis de Stock",
     "stock-location": "Localização de stock",
     "stock-location": "Localização de stock",
     "stock-locations": "Localizações de stock",
     "stock-locations": "Localizações de stock",
     "stock-on-hand": "Estoque",
     "stock-on-hand": "Estoque",
@@ -190,41 +190,41 @@
     "use-global-value": "Utilizar configuração global",
     "use-global-value": "Utilizar configuração global",
     "values": "Valores",
     "values": "Valores",
     "variant": "Variante",
     "variant": "Variante",
-    "variant-count": "",
+    "variant-count": "Número de variantes",
     "view-contents": "Visualizar conteúdo",
     "view-contents": "Visualizar conteúdo",
     "visibility": "Visibilidade"
     "visibility": "Visibilidade"
   },
   },
   "common": {
   "common": {
     "ID": "ID",
     "ID": "ID",
-    "add-filter": "",
+    "add-filter": "Adicionar filtro",
     "add-item-to-list": "Adicionar item à lista",
     "add-item-to-list": "Adicionar item à lista",
     "add-note": "Adicionar nota",
     "add-note": "Adicionar nota",
-    "apply": "",
-    "assign-to-channel": "",
+    "apply": "Aplicar",
+    "assign-to-channel": "Adicionar a canal",
     "assign-to-channels": "Atribuir a {count, plural, one {canal} other {canais}}",
     "assign-to-channels": "Atribuir a {count, plural, one {canal} other {canais}}",
-    "available-currencies": "",
+    "available-currencies": "Moedas disponíveis",
     "available-languages": "Idiomas disponíveis",
     "available-languages": "Idiomas disponíveis",
-    "boolean-and": "",
-    "boolean-false": "",
-    "boolean-or": "",
-    "boolean-true": "",
-    "breadcrumb": "",
+    "boolean-and": "e",
+    "boolean-false": "falso",
+    "boolean-or": "ou",
+    "boolean-true": "verdadeiro",
+    "breadcrumb": "breadcrump",
     "browser-default": "Navegador padrão",
     "browser-default": "Navegador padrão",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
     "cancel-navigation": "Continuar a editar",
     "cancel-navigation": "Continuar a editar",
     "change-selection": "Alterar seleccionados",
     "change-selection": "Alterar seleccionados",
     "channel": "Canal",
     "channel": "Canal",
     "channels": "Canais",
     "channels": "Canais",
-    "clear-selection": "",
+    "clear-selection": "Apagar seleção",
     "code": "Código",
     "code": "Código",
     "collapse-entries": "Recolher entradas",
     "collapse-entries": "Recolher entradas",
     "confirm": "Confirmar",
     "confirm": "Confirmar",
-    "confirm-bulk-assign-to-channel": "",
-    "confirm-bulk-delete": "",
-    "confirm-bulk-remove-from-channel": "",
+    "confirm-bulk-assign-to-channel": "Confirmar atribuição em bulk a canal",
+    "confirm-bulk-delete": "Confimar apagar em bulk",
+    "confirm-bulk-remove-from-channel": "Confirmar apagar em bulk de canal",
     "confirm-delete-note": "Eliminar nota?",
     "confirm-delete-note": "Eliminar nota?",
     "confirm-navigation": "Descartar modificações?",
     "confirm-navigation": "Descartar modificações?",
-    "contents": "",
+    "contents": "Conteúdos",
     "create": "Adicionar",
     "create": "Adicionar",
     "created-at": "Adicionado em",
     "created-at": "Adicionado em",
     "custom-fields": "Campos customizados",
     "custom-fields": "Campos customizados",
@@ -244,97 +244,97 @@
     "edit-field": "Editar campo",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
     "edit-note": "Editar nota",
     "enabled": "Activo",
     "enabled": "Activo",
-    "end-date": "",
+    "end-date": "Data de fim",
     "expand-entries": "Expandir entradas",
     "expand-entries": "Expandir entradas",
     "extension-running-in-separate-window": "A extensão está a ser executada em uma janela separada",
     "extension-running-in-separate-window": "A extensão está a ser executada em uma janela separada",
     "filter": "Filtro",
     "filter": "Filtro",
     "filter-preset-name": "Nome de configuração predefinida de filtro",
     "filter-preset-name": "Nome de configuração predefinida de filtro",
-    "force-delete": "",
-    "force-remove": "",
+    "force-delete": "Forçar apagar",
+    "force-remove": "Forçar remover",
     "general": "Geral",
     "general": "Geral",
     "guest": "Convidado",
     "guest": "Convidado",
-    "id": "",
-    "image": "",
+    "id": "ID",
+    "image": "Imagem",
     "items-per-page-option": "{ count } por página",
     "items-per-page-option": "{ count } por página",
-    "items-selected-count": "",
-    "keep-editing": "",
+    "items-selected-count": "Itens seleccionados",
+    "keep-editing": "Continuar edição",
     "language": "Idioma",
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "launch-extension": "Iniciar extensão",
-    "list-items-and-n-more": "",
+    "list-items-and-n-more": "Listar itens",
     "live-update": "Actualização em tempo real",
     "live-update": "Actualização em tempo real",
     "locale": "Localidade",
     "locale": "Localidade",
     "log-out": "Sair",
     "log-out": "Sair",
     "login": "Entrar",
     "login": "Entrar",
-    "login-image-title": "",
-    "login-title": "",
+    "login-image-title": "Titulo de imagem de login",
+    "login-title": "Titulo de login",
     "manage-tags": "Gerir tags",
     "manage-tags": "Gerir tags",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
     "medium-date": "Data média",
     "medium-date": "Data média",
     "more": "Mais...",
     "more": "Mais...",
     "name": "Nome",
     "name": "Nome",
-    "no-alerts": "",
-    "no-bulk-actions-available": "",
-    "no-channel-selected": "",
+    "no-alerts": "sem alertas",
+    "no-bulk-actions-available": "nenhuma acção em bulk disponível",
+    "no-channel-selected": "Nenhum canal selecionado",
     "no-results": "Nenhum resultado encontrado",
     "no-results": "Nenhum resultado encontrado",
-    "not-applicable": "",
+    "not-applicable": "Não aplicável",
     "not-set": "Não configurado",
     "not-set": "Não configurado",
-    "notify-assign-to-channel-success-with-count": "",
-    "notify-bulk-update-success": "",
+    "notify-assign-to-channel-success-with-count": "Notificar atribuição a canal com contagem",
+    "notify-bulk-update-success": "Notificar bulk update com sucesso",
     "notify-create-error": "Ocorreu um erro. Não foi possível criar { entity }",
     "notify-create-error": "Ocorreu um erro. Não foi possível criar { entity }",
     "notify-create-success": "Novo(a) { entity } adicionado(a)",
     "notify-create-success": "Novo(a) { entity } adicionado(a)",
     "notify-delete-error": "Ocorreu um erro, não foi possível eliminar { entity }",
     "notify-delete-error": "Ocorreu um erro, não foi possível eliminar { entity }",
-    "notify-delete-error-with-count": "",
+    "notify-delete-error-with-count": "Notificar erro ao apagar com contagem",
     "notify-delete-success": "{ entity } excluído(a)",
     "notify-delete-success": "{ entity } excluído(a)",
-    "notify-delete-success-with-count": "",
+    "notify-delete-success-with-count": "Notificar apagado com sucesso com contagem",
     "notify-duplicate-error": "Não foi possível duplicar { name } devido a um erro: { error }",
     "notify-duplicate-error": "Não foi possível duplicar { name } devido a um erro: { error }",
     "notify-duplicate-error-excess": "Não foi possível duplicar { count } {count, plural, one {item} other {itens}} adicionais devido a erros",
     "notify-duplicate-error-excess": "Não foi possível duplicar { count } {count, plural, one {item} other {itens}} adicionais devido a erros",
     "notify-duplicate-success": "Duplicação realizada com sucesso {count, plural, one {1 item} other {{count} itens}}: { names }",
     "notify-duplicate-success": "Duplicação realizada com sucesso {count, plural, one {1 item} other {{count} itens}}: { names }",
-    "notify-remove-from-channel-success-with-count": "",
+    "notify-remove-from-channel-success-with-count": "Notificar removido do canal com sucesso com contagem",
     "notify-save-changes-error": "Ocorreu um erro. Não foi possível guardar as alterações",
     "notify-save-changes-error": "Ocorreu um erro. Não foi possível guardar as alterações",
     "notify-saved-changes": "Alterações guardadas",
     "notify-saved-changes": "Alterações guardadas",
     "notify-update-error": "Ocorreu um erro. Não foi possível actualizar a entidade { entity }",
     "notify-update-error": "Ocorreu um erro. Não foi possível actualizar a entidade { entity }",
     "notify-update-success": "Entidade ({ entity }) actualizada com sucesso",
     "notify-update-success": "Entidade ({ entity }) actualizada com sucesso",
     "notify-updated-tags-success": "Tags actualizadas com sucesso",
     "notify-updated-tags-success": "Tags actualizadas com sucesso",
-    "okay": "",
-    "operator-contains": "",
-    "operator-eq": "",
-    "operator-gt": "",
-    "operator-lt": "",
-    "operator-not-contains": "",
-    "operator-not-eq": "",
-    "operator-notContains": "",
-    "operator-regex": "",
+    "okay": "Ok",
+    "operator-contains": "Contém",
+    "operator-eq": "Igual",
+    "operator-gt": "Maior que",
+    "operator-lt": "Menor que",
+    "operator-not-contains": "Não contém",
+    "operator-not-eq": "Diferente de",
+    "operator-notContains": "Não Contêm",
+    "operator-regex": "Expressão regular",
     "password": "Palavra passe",
     "password": "Palavra passe",
-    "position": "",
+    "position": "Posição",
     "price": "Preço",
     "price": "Preço",
     "price-with-tax": "Preço com impostos",
     "price-with-tax": "Preço com impostos",
     "private": "Privado",
     "private": "Privado",
     "public": "Público",
     "public": "Público",
     "remember-me": "Lembre-se de mim",
     "remember-me": "Lembre-se de mim",
     "remove": "Eliminar",
     "remove": "Eliminar",
-    "remove-from-channel": "",
+    "remove-from-channel": "Remover do canal",
     "remove-item-from-list": "Remover item da lista",
     "remove-item-from-list": "Remover item da lista",
     "rename-filter-preset": "Renomear predefinição",
     "rename-filter-preset": "Renomear predefinição",
-    "reset-columns": "",
+    "reset-columns": "Apagar colunas",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
     "sample-formatting": "Formatação de amostra",
     "sample-formatting": "Formatação de amostra",
     "save-filter-preset": "Guardar como predefinição",
     "save-filter-preset": "Guardar como predefinição",
-    "search-and-filter-list": "",
-    "search-by-name": "",
+    "search-and-filter-list": "Pesquisa e lista de filtros",
+    "search-by-name": "Pesquisar por nome",
     "select": "Seleccione...",
     "select": "Seleccione...",
     "select-display-language": "Seleccionar idioma",
     "select-display-language": "Seleccionar idioma",
-    "select-items-with-count": "",
-    "select-products": "",
-    "select-relation-id": "",
-    "select-table-columns": "",
+    "select-items-with-count": "Selecionar itens com contagem",
+    "select-products": "Selecionar produtos",
+    "select-relation-id": "Seçecionar relacionamento da ID",
+    "select-table-columns": "Selecionar tabelas comuns",
     "select-today": "Seleccione a data de hoje",
     "select-today": "Seleccione a data de hoje",
-    "select-variants": "",
-    "seller": "",
+    "select-variants": "Selecionar variantes",
+    "seller": "Vendedor",
     "set-language": "Definir idioma",
     "set-language": "Definir idioma",
     "short-date": "Data abreviada",
     "short-date": "Data abreviada",
-    "slug": "",
-    "start-date": "",
-    "status": "",
+    "slug": "Slug",
+    "start-date": "Data de início",
+    "status": "Estado",
     "tags": "Tags",
     "tags": "Tags",
     "theme": "Tema",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",
@@ -343,11 +343,11 @@
     "update": "Actualização",
     "update": "Actualização",
     "updated-at": "Actualizado em",
     "updated-at": "Actualizado em",
     "username": "Nome do utilizador",
     "username": "Nome do utilizador",
-    "value": "",
-    "view-contents": "",
+    "value": "Valor",
+    "view-contents": "Ver conteúdos",
     "view-next-month": "Visualizar próximo mês",
     "view-next-month": "Visualizar próximo mês",
     "view-previous-month": "Visualizar mês anterior",
     "view-previous-month": "Visualizar mês anterior",
-    "visibility": "",
+    "visibility": "visibilidade",
     "with-selected": "Com seleccionado..."
     "with-selected": "Com seleccionado..."
   },
   },
   "customer": {
   "customer": {
@@ -359,18 +359,18 @@
     "add-customers-to-group-with-name": "Clientes adicionados ao grupo \"{ groupName }\"",
     "add-customers-to-group-with-name": "Clientes adicionados ao grupo \"{ groupName }\"",
     "addresses": "Moradas",
     "addresses": "Moradas",
     "city": "Cidade",
     "city": "Cidade",
-    "company": "",
+    "company": "Empresa",
     "confirm-remove-customer-from-group": "Eliminar cliente do grupo?",
     "confirm-remove-customer-from-group": "Eliminar cliente do grupo?",
     "country": "País",
     "country": "País",
     "create-customer-group": "Criar grupo de cliente",
     "create-customer-group": "Criar grupo de cliente",
     "create-new-address": "Criar nova morada",
     "create-new-address": "Criar nova morada",
     "create-new-customer": "Criar novo cliente",
     "create-new-customer": "Criar novo cliente",
     "create-new-customer-group": "Criar novo grupo de cliente",
     "create-new-customer-group": "Criar novo grupo de cliente",
-    "customer": "",
-    "customer-group": "",
+    "customer": "Cliente",
+    "customer-group": "Grupo de cliente",
     "customer-groups": "Grupos de cliente",
     "customer-groups": "Grupos de cliente",
     "customer-history": "Histórico de cliente",
     "customer-history": "Histórico de cliente",
-    "customers": "",
+    "customers": "Clientes",
     "default-billing-address": "Morada de cobrança padrão",
     "default-billing-address": "Morada de cobrança padrão",
     "default-shipping-address": "Morada de entrega padrão",
     "default-shipping-address": "Morada de entrega padrão",
     "email-address": "E-mail",
     "email-address": "E-mail",
@@ -423,10 +423,10 @@
   "dashboard": {
   "dashboard": {
     "add-widget": "Adicionar widget",
     "add-widget": "Adicionar widget",
     "latest-orders": "Últimas encomendas",
     "latest-orders": "Últimas encomendas",
-    "metric-average-order-value": "",
-    "metric-number-of-orders": "",
-    "metric-order-total-value": "",
-    "metrics": "",
+    "metric-average-order-value": "Valor médio de encomenda",
+    "metric-number-of-orders": "Número de encomendas",
+    "metric-order-total-value": "Valor total de encomendas",
+    "metrics": "Métricas",
     "orders-summary": "Resumo de encomendas",
     "orders-summary": "Resumo de encomendas",
     "remove-widget": "Remover widget",
     "remove-widget": "Remover widget",
     "thisMonth": "Este mês",
     "thisMonth": "Este mês",
@@ -480,7 +480,7 @@
     "image-title": "Título",
     "image-title": "Título",
     "insert-image": "Inserir imagem",
     "insert-image": "Inserir imagem",
     "link-href": "Link de referência",
     "link-href": "Link de referência",
-    "link-target": "",
+    "link-target": "Escolha onde abrir o link",
     "link-title": "Título do link",
     "link-title": "Título do link",
     "remove-link": "Remover",
     "remove-link": "Remover",
     "set-link": "Atribuir link"
     "set-link": "Atribuir link"
@@ -503,8 +503,8 @@
     "ends-at": "Válido até",
     "ends-at": "Válido até",
     "per-customer-limit": "Limite por cliente",
     "per-customer-limit": "Limite por cliente",
     "per-customer-limit-tooltip": "Número máximo de vezes que esta promoção pode ser utilizada por um único cliente",
     "per-customer-limit-tooltip": "Número máximo de vezes que esta promoção pode ser utilizada por um único cliente",
-    "promotion": "",
-    "search-by-name-or-coupon-code": "",
+    "promotion": "Promoção",
+    "search-by-name-or-coupon-code": "Pesquisar por nome ou código de cupão",
     "starts-at": "Válido a partir",
     "starts-at": "Válido a partir",
     "usage-limit": "Limite total de utilização",
     "usage-limit": "Limite total de utilização",
     "usage-limit-tooltip": "Número máximo de vezes que esta promoção pode ser utilizada no total"
     "usage-limit-tooltip": "Número máximo de vezes que esta promoção pode ser utilizada no total"
@@ -514,7 +514,7 @@
     "assets": "Imagens",
     "assets": "Imagens",
     "catalog": "Catálogo",
     "catalog": "Catálogo",
     "channels": "Canais",
     "channels": "Canais",
-    "collections": "Categorias",
+    "collections": "Colecções",
     "countries": "Países",
     "countries": "Países",
     "customer-groups": "Grupos",
     "customer-groups": "Grupos",
     "customers": "Clientes",
     "customers": "Clientes",
@@ -528,7 +528,7 @@
     "promotions": "Promoções",
     "promotions": "Promoções",
     "roles": "Gerir permissões",
     "roles": "Gerir permissões",
     "sales": "Vendas",
     "sales": "Vendas",
-    "sellers": "",
+    "sellers": "Vendedores",
     "settings": "Configurações",
     "settings": "Configurações",
     "shipping-methods": "Métodos de envio",
     "shipping-methods": "Métodos de envio",
     "stock-locations": "Localizações de stock",
     "stock-locations": "Localizações de stock",
@@ -551,7 +551,7 @@
     "assign-order-to-another-customer": "Atribuir pedido a outro cliente",
     "assign-order-to-another-customer": "Atribuir pedido a outro cliente",
     "billing-address": "Morada de faturação",
     "billing-address": "Morada de faturação",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
-    "cancel-entire-order": "",
+    "cancel-entire-order": "Cancelar encomenda completa",
     "cancel-fulfillment": "Cancelar entrega",
     "cancel-fulfillment": "Cancelar entrega",
     "cancel-modification": "Cancelar modificação",
     "cancel-modification": "Cancelar modificação",
     "cancel-order": "Cancelar encomenda",
     "cancel-order": "Cancelar encomenda",
@@ -559,24 +559,24 @@
     "cancel-reason-customer-request": "Encomenda do cliente",
     "cancel-reason-customer-request": "Encomenda do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens seleccionados",
     "cancel-selected-items": "Cancelar itens seleccionados",
-    "cancel-specified-items": "",
+    "cancel-specified-items": "Cancelar itens específicos",
     "cancellation-reason": "Motivo do cancelamento",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-items-success": "Itens da encomenda cancelados { count } { count, plural, one {item} other {itens} }",
     "cancelled-order-items-success": "Itens da encomenda cancelados { count } { count, plural, one {item} other {itens} }",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
-    "complete-draft-order": "",
+    "complete-draft-order": "Completar ordem rascunho",
     "confirm-modifications": "Confirmar modificações",
     "confirm-modifications": "Confirmar modificações",
     "contents": "Conteúdo",
     "contents": "Conteúdo",
     "create-fulfillment": "Criar entrega",
     "create-fulfillment": "Criar entrega",
     "create-fulfillment-success": "O processo entrega foi criado com sucesso",
     "create-fulfillment-success": "O processo entrega foi criado com sucesso",
     "customer": "Cliente",
     "customer": "Cliente",
-    "delete-draft-order": "",
-    "draft-order": "",
+    "delete-draft-order": "Apagar ordem rascunho",
+    "draft-order": "Ordem rascunho",
     "edit-billing-address": "Editar morada de faturação",
     "edit-billing-address": "Editar morada de faturação",
     "edit-shipping-address": "Editar morada de entrega",
     "edit-shipping-address": "Editar morada de entrega",
     "error-message": "Mensagem de erro",
     "error-message": "Mensagem de erro",
-    "existing-address": "",
-    "existing-customer": "",
-    "filter-is-active": "",
+    "existing-address": "Morada existente",
+    "existing-customer": "Cliente existente",
+    "filter-is-active": "Filtro está activo",
     "fulfill": "Enviar",
     "fulfill": "Enviar",
     "fulfill-order": "Enviar a encomenda",
     "fulfill-order": "Enviar a encomenda",
     "fulfillment": "Entrega",
     "fulfillment": "Entrega",
@@ -622,22 +622,22 @@
     "note-is-private": "Marcar como privada",
     "note-is-private": "Marcar como privada",
     "note-only-visible-to-administrators": "Visível somente para administradores",
     "note-only-visible-to-administrators": "Visível somente para administradores",
     "note-visible-to-customer": "Visível para administradores e clientes",
     "note-visible-to-customer": "Visível para administradores e clientes",
-    "order": "",
+    "order": "Encomenda",
     "order-history": "Histórico de encomendas",
     "order-history": "Histórico de encomendas",
-    "order-is-empty": "",
+    "order-is-empty": "Encomenda está vazia",
     "order-state-diagram": "Diagrama do estado da encomenda",
     "order-state-diagram": "Diagrama do estado da encomenda",
-    "order-type": "",
-    "order-type-aggregate": "",
-    "order-type-regular": "",
-    "order-type-seller": "",
-    "orders": "",
+    "order-type": "Tipo de encomenda",
+    "order-type-aggregate": "Encomenda agregada",
+    "order-type-regular": "Encomenda normal",
+    "order-type-seller": "Encomenda de vendedor",
+    "orders": "Encomendas",
     "original-quantity-at-checkout": "Quantidade original no checkout",
     "original-quantity-at-checkout": "Quantidade original no checkout",
     "payment": "Pagamento",
     "payment": "Pagamento",
     "payment-amount": "Valor do pagamento",
     "payment-amount": "Valor do pagamento",
     "payment-metadata": "Dados do pagamento",
     "payment-metadata": "Dados do pagamento",
     "payment-method": "Método de pagamento",
     "payment-method": "Método de pagamento",
     "payment-state": "Estado",
     "payment-state": "Estado",
-    "payments": "",
+    "payments": "Pagamentos",
     "placed-at": "Adicionada em",
     "placed-at": "Adicionada em",
     "preview-changes": "Revisar mudanças",
     "preview-changes": "Revisar mudanças",
     "previous-customer": "Cliente anterior",
     "previous-customer": "Cliente anterior",
@@ -668,17 +668,17 @@
     "removed-items": "Itens removidos",
     "removed-items": "Itens removidos",
     "return-to-stock": "Devolver ao stock",
     "return-to-stock": "Devolver ao stock",
     "search-by-order-filters": "Pesqusiar pelo código da encomenda / apelido do cliente / ID da transação",
     "search-by-order-filters": "Pesqusiar pelo código da encomenda / apelido do cliente / ID da transação",
-    "select-address": "",
-    "select-shipping-method": "",
+    "select-address": "Selecionar morada",
+    "select-shipping-method": "Selecionar método de envio",
     "select-state": "Seleccionar estado",
     "select-state": "Seleccionar estado",
-    "seller-orders": "",
-    "set-billing-address": "",
-    "set-coupon-codes": "",
-    "set-customer-for-order": "",
-    "set-customer-success": "",
+    "seller-orders": "Selecionar encomendas",
+    "set-billing-address": "Selecionar morada de facturação",
+    "set-coupon-codes": "Selecionar código de cupão",
+    "set-customer-for-order": "Selecionar cliente para encomenda",
+    "set-customer-success": "Cliente selecionado com sucesso",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",
-    "set-shipping-address": "",
-    "set-shipping-method": "",
+    "set-shipping-address": "Selecionar morada de envio",
+    "set-shipping-method": "Selecionar método de envio",
     "settle-payment": "Liquidar pagamento",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
     "settle-payment-success": "Pagamento liquidado com sucesso",
     "settle-payment-success": "Pagamento liquidado com sucesso",
@@ -687,7 +687,7 @@
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "shipping": "Envio",
     "shipping": "Envio",
     "shipping-address": "Morada de entrega",
     "shipping-address": "Morada de entrega",
-    "shipping-cancelled": "",
+    "shipping-cancelled": "Envio cancelado",
     "shipping-method": "Método de envio",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "state": "Estado",
     "sub-total": "Subtotal",
     "sub-total": "Subtotal",
@@ -712,30 +712,30 @@
     "add-countries-to-zone": "Adicionar paises para { zoneName }",
     "add-countries-to-zone": "Adicionar paises para { zoneName }",
     "add-countries-to-zone-success": "A adicionar { countryCount } {countryCount, plural, one {país} other {países}} à região \"{ zoneName }\"",
     "add-countries-to-zone-success": "A adicionar { countryCount } {countryCount, plural, one {país} other {países}} à região \"{ zoneName }\"",
     "add-products-to-test-order": "Adicionar produdos para a encomenda de teste",
     "add-products-to-test-order": "Adicionar produdos para a encomenda de teste",
-    "administrator": "",
+    "administrator": "Administrador",
     "channel": "Canal",
     "channel": "Canal",
     "channel-token": "Token do canal",
     "channel-token": "Token do canal",
-    "country": "",
+    "country": "País",
     "create-new-channel": "Criar novo canal",
     "create-new-channel": "Criar novo canal",
     "create-new-country": "Criar novo país",
     "create-new-country": "Criar novo país",
     "create-new-payment-method": "Criar novo método de pagamento",
     "create-new-payment-method": "Criar novo método de pagamento",
     "create-new-role": "Criar nova regra",
     "create-new-role": "Criar nova regra",
-    "create-new-seller": "",
+    "create-new-seller": "Criar novo vendedor",
     "create-new-shipping-method": "Criar novo método de envio",
     "create-new-shipping-method": "Criar novo método de envio",
     "create-new-tax-category": "Criar categoria de imposto",
     "create-new-tax-category": "Criar categoria de imposto",
     "create-new-tax-rate": "Criar nova taxa de imposto",
     "create-new-tax-rate": "Criar nova taxa de imposto",
     "create-new-zone": "Criar nova região",
     "create-new-zone": "Criar nova região",
-    "default-currency": "",
+    "default-currency": "Moeda padrão",
     "default-role-label": "Esta é uma regra padrão e não pode ser modificada",
     "default-role-label": "Esta é uma regra padrão e não pode ser modificada",
     "default-shipping-zone": "Região de envio padrão",
     "default-shipping-zone": "Região de envio padrão",
     "default-tax-zone": "Região de imposto padrão",
     "default-tax-zone": "Região de imposto padrão",
-    "defaults": "",
+    "defaults": "Padrões",
     "eligible": "Elegível",
     "eligible": "Elegível",
     "email-address": "E-mail",
     "email-address": "E-mail",
-    "email-address-or-identifier": "",
+    "email-address-or-identifier": "Email ou identificador",
     "first-name": "Nome",
     "first-name": "Nome",
     "fulfillment-handler": "Manipulador para a execução de envio",
     "fulfillment-handler": "Manipulador para a execução de envio",
-    "global-available-languages-tooltip": "",
+    "global-available-languages-tooltip": "Linguagens globais disponíveis",
     "global-out-of-stock-threshold": "Limite globalpara fora de estoque",
     "global-out-of-stock-threshold": "Limite globalpara fora de estoque",
     "global-out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes. Pode ser substituído pela variante do produto.",
     "global-out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes. Pode ser substituído pela variante do produto.",
     "last-name": "Apelido",
     "last-name": "Apelido",
@@ -743,17 +743,17 @@
     "password": "Palavra passe",
     "password": "Palavra passe",
     "payment-eligibility-checker": "Validação da elegibilidade do método",
     "payment-eligibility-checker": "Validação da elegibilidade do método",
     "payment-handler": "Manipulador do método",
     "payment-handler": "Manipulador do método",
-    "payment-method": "",
+    "payment-method": "Método de pagamento",
     "permissions": "Permissões",
     "permissions": "Permissões",
     "prices-include-tax": "Os preços incluem impostos para a região padrão",
     "prices-include-tax": "Os preços incluem impostos para a região padrão",
     "profile": "Perfil",
     "profile": "Perfil",
     "rate": "Taxa",
     "rate": "Taxa",
     "remove-countries-from-zone-success": "Eliminado { countryCount } {countryCount, plural, one {país} other {países}} da região \"{ zoneName }\"",
     "remove-countries-from-zone-success": "Eliminado { countryCount } {countryCount, plural, one {país} other {países}} da região \"{ zoneName }\"",
     "remove-from-zone": "Eliminar da região",
     "remove-from-zone": "Eliminar da região",
-    "role": "",
+    "role": "Regra",
     "roles": "Regras",
     "roles": "Regras",
     "search-by-product-name-or-sku": "Pesquisa por nome do produto ou SKU",
     "search-by-product-name-or-sku": "Pesquisa por nome do produto ou SKU",
-    "seller": "",
+    "seller": "Vendedor",
     "shipping-calculator": "Calculadora de envio",
     "shipping-calculator": "Calculadora de envio",
     "shipping-eligibility-checker": "Validação da elegibilidade do método",
     "shipping-eligibility-checker": "Validação da elegibilidade do método",
     "shipping-method": "Método de envio",
     "shipping-method": "Método de envio",
@@ -776,7 +776,7 @@
     "created": "Criado",
     "created": "Criado",
     "declined": "Recusado",
     "declined": "Recusado",
     "delivered": "Entregue",
     "delivered": "Entregue",
-    "draft": "",
+    "draft": "Rascunho",
     "error": "Erro",
     "error": "Erro",
     "failed": "Falhou",
     "failed": "Falhou",
     "modifying": "A modificar",
     "modifying": "A modificar",

+ 7 - 7
packages/common/src/shared-types.ts

@@ -12,8 +12,8 @@ export type DeepPartial<T> = {
         | (T[P] extends Array<infer U>
         | (T[P] extends Array<infer U>
               ? Array<DeepPartial<U>>
               ? Array<DeepPartial<U>>
               : T[P] extends ReadonlyArray<infer U>
               : T[P] extends ReadonlyArray<infer U>
-              ? ReadonlyArray<DeepPartial<U>>
-              : DeepPartial<T[P]>);
+                ? ReadonlyArray<DeepPartial<U>>
+                : DeepPartial<T[P]>);
 };
 };
 /* eslint-enable no-shadow, @typescript-eslint/no-shadow */
 /* eslint-enable no-shadow, @typescript-eslint/no-shadow */
 
 
@@ -53,8 +53,8 @@ export type JsonCompatible<T> = {
     [P in keyof T]: T[P] extends Json
     [P in keyof T]: T[P] extends Json
         ? T[P]
         ? T[P]
         : Pick<T, P> extends Required<Pick<T, P>>
         : Pick<T, P> extends Required<Pick<T, P>>
-        ? never
-        : JsonCompatible<T[P]>;
+          ? never
+          : JsonCompatible<T[P]>;
 };
 };
 
 
 /**
 /**
@@ -95,7 +95,7 @@ export type ID = string | number;
  * datetime     | datetime (m,s), timestamp (p)         | DateTime
  * datetime     | datetime (m,s), timestamp (p)         | DateTime
  * relation     | many-to-one / many-to-many relation   | As specified in config
  * relation     | many-to-one / many-to-many relation   | As specified in config
  *
  *
- * Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#configuration-options)
+ * Additionally, the CustomFieldType also dictates which [configuration options](/reference/typescript-api/custom-fields/#custom-field-config-properties)
  * are available for that custom field.
  * are available for that custom field.
  *
  *
  * @docsCategory custom-fields
  * @docsCategory custom-fields
@@ -219,7 +219,7 @@ export interface AdminUiConfig {
      * to. If set to "auto", the Admin UI app will determine the hostname from the
      * to. If set to "auto", the Admin UI app will determine the hostname from the
      * current location (i.e. `window.location.hostname`).
      * current location (i.e. `window.location.hostname`).
      *
      *
-     * @default 'http://localhost'
+     * @default 'auto'
      */
      */
     apiHost: string | 'auto';
     apiHost: string | 'auto';
     /**
     /**
@@ -228,7 +228,7 @@ export interface AdminUiConfig {
      * to. If set to "auto", the Admin UI app will determine the port from the
      * to. If set to "auto", the Admin UI app will determine the port from the
      * current location (i.e. `window.location.port`).
      * current location (i.e. `window.location.port`).
      *
      *
-     * @default 3000
+     * @default 'auto'
      */
      */
     apiPort: number | 'auto';
     apiPort: number | 'auto';
     /**
     /**

+ 1 - 0
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -376,6 +376,7 @@ export class ShopOrderResolver {
             true,
             true,
         );
         );
         return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
         return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
+        return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
     }
     }
 
 
     @Transaction()
     @Transaction()

+ 2 - 3
packages/core/src/config/vendure-config.ts

@@ -401,9 +401,8 @@ export interface AuthOptions {
      * @description
      * @description
      * Determines whether new User accounts require verification of their email address.
      * Determines whether new User accounts require verification of their email address.
      *
      *
-     * If set to "true", when registering via the `registerCustomerAccount` mutation, one should *not* set the
-     * `password` property - doing so will result in an error. Instead, the password is set at a later stage
-     * (once the email with the verification token has been opened) via the `verifyCustomerAccount` mutation.
+     * If set to "true", the customer will be required to verify their email address using a verification token
+     * they receive in their email. See the `registerCustomerAccount` mutation for more details on the verification behavior.
      *
      *
      * @default true
      * @default true
      */
      */

+ 6 - 1
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -200,11 +200,16 @@ export class EntityHydrator {
         const missingRelations: string[] = [];
         const missingRelations: string[] = [];
         for (const relation of options.relations.slice().sort()) {
         for (const relation of options.relations.slice().sort()) {
             if (typeof relation === 'string') {
             if (typeof relation === 'string') {
-                const parts = !relation.startsWith('customFields') ? relation.split('.') : [relation];
+                const parts = relation.split('.');
                 let entity: Record<string, any> | undefined = target;
                 let entity: Record<string, any> | undefined = target;
                 const path = [];
                 const path = [];
                 for (const part of parts) {
                 for (const part of parts) {
                     path.push(part);
                     path.push(part);
+                    // null = the relation has been fetched but was null in the database.
+                    // undefined = the relation has not been fetched.
+                    if (entity && entity[part] === null) {
+                        break;
+                    }
                     if (entity && entity[part]) {
                     if (entity && entity[part]) {
                         entity = Array.isArray(entity[part]) ? entity[part][0] : entity[part];
                         entity = Array.isArray(entity[part]) ? entity[part][0] : entity[part];
                     } else {
                     } else {

+ 4 - 4
packages/core/src/service/helpers/external-authentication/external-authentication.service.ts

@@ -94,10 +94,10 @@ export class ExternalAuthenticationService {
         config: {
         config: {
             strategy: string;
             strategy: string;
             externalIdentifier: string;
             externalIdentifier: string;
-            verified: boolean;
             emailAddress: string;
             emailAddress: string;
-            firstName?: string;
-            lastName?: string;
+            firstName: string;
+            lastName: string;
+            verified?: boolean;
         },
         },
     ): Promise<User> {
     ): Promise<User> {
         let user: User;
         let user: User;
@@ -206,7 +206,7 @@ export class ExternalAuthenticationService {
             }),
             }),
         );
         );
 
 
-        return newUser;
+        return savedUser;
     }
     }
 
 
     async findUser(
     async findUser(

+ 142 - 93
packages/core/src/service/services/collection.service.ts

@@ -23,7 +23,12 @@ import { In, IsNull } from 'typeorm';
 
 
 import { RequestContext, SerializedRequestContext } from '../../api/common/request-context';
 import { RequestContext, SerializedRequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
-import { ForbiddenError, IllegalOperationError, UserInputError } from '../../common/error/errors';
+import {
+    ForbiddenError,
+    IllegalOperationError,
+    InternalServerError,
+    UserInputError,
+} from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -101,11 +106,14 @@ export class CollectionService implements OnModuleInit {
                     .createQueryBuilder('collection')
                     .createQueryBuilder('collection')
                     .select('collection.id', 'id')
                     .select('collection.id', 'id')
                     .getRawMany();
                     .getRawMany();
-                await this.applyFiltersQueue.add({
-                    ctx: event.ctx.serialize(),
-                    collectionIds: collections.map(c => c.id),
-                },
-                {   ctx: event.ctx   });
+                await this.applyFiltersQueue.add(
+                    {
+                        ctx: event.ctx.serialize(),
+                        collectionIds: collections.map(c => c.id),
+                        applyToChangedVariantsOnly: true,
+                    },
+                    { ctx: event.ctx },
+                );
             });
             });
 
 
         this.applyFiltersQueue = await this.jobQueueService.createQueue({
         this.applyFiltersQueue = await this.jobQueueService.createQueue({
@@ -129,7 +137,7 @@ export class CollectionService implements OnModuleInit {
                         Logger.warn(`Could not find Collection with id ${collectionId}, skipping`);
                         Logger.warn(`Could not find Collection with id ${collectionId}, skipping`);
                     }
                     }
                     completed++;
                     completed++;
-                    if (collection) {
+                    if (collection !== undefined) {
                         let affectedVariantIds: ID[] = [];
                         let affectedVariantIds: ID[] = [];
                         try {
                         try {
                             affectedVariantIds = await this.applyCollectionFiltersInternal(
                             affectedVariantIds = await this.applyCollectionFiltersInternal(
@@ -147,8 +155,11 @@ export class CollectionService implements OnModuleInit {
                         }
                         }
                         job.setProgress(Math.ceil((completed / job.data.collectionIds.length) * 100));
                         job.setProgress(Math.ceil((completed / job.data.collectionIds.length) * 100));
                         if (affectedVariantIds.length) {
                         if (affectedVariantIds.length) {
-                            await this.eventBus.publish(
-                                new CollectionModificationEvent(ctx, collection, affectedVariantIds),
+                            // To avoid performance issues on huge collections we first split the affected variant ids into chunks
+                            this.chunkArray(affectedVariantIds, 50000).map(chunk =>
+                                this.eventBus.publish(
+                                    new CollectionModificationEvent(ctx, collection as Collection, chunk),
+                                ),
                             );
                             );
                         }
                         }
                     }
                     }
@@ -469,11 +480,13 @@ export class CollectionService implements OnModuleInit {
             input,
             input,
             collection,
             collection,
         );
         );
-        await this.applyFiltersQueue.add({
-            ctx: ctx.serialize(),
-            collectionIds: [collection.id],
-        },
-        {   ctx   });
+        await this.applyFiltersQueue.add(
+            {
+                ctx: ctx.serialize(),
+                collectionIds: [collection.id],
+            },
+            { ctx },
+        );
         await this.eventBus.publish(new CollectionEvent(ctx, collectionWithRelations, 'created', input));
         await this.eventBus.publish(new CollectionEvent(ctx, collectionWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, collection.id));
         return assertFound(this.findOne(ctx, collection.id));
     }
     }
@@ -495,12 +508,14 @@ export class CollectionService implements OnModuleInit {
         });
         });
         await this.customFieldRelationService.updateRelations(ctx, Collection, input, collection);
         await this.customFieldRelationService.updateRelations(ctx, Collection, input, collection);
         if (input.filters) {
         if (input.filters) {
-            await this.applyFiltersQueue.add({
-                ctx: ctx.serialize(),
-                collectionIds: [collection.id],
-                applyToChangedVariantsOnly: false,
-            },
-            {   ctx   });
+            await this.applyFiltersQueue.add(
+                {
+                    ctx: ctx.serialize(),
+                    collectionIds: [collection.id],
+                    applyToChangedVariantsOnly: false,
+                },
+                { ctx },
+            );
         } else {
         } else {
             const affectedVariantIds = await this.getCollectionProductVariantIds(collection);
             const affectedVariantIds = await this.getCollectionProductVariantIds(collection);
             await this.eventBus.publish(new CollectionModificationEvent(ctx, collection, affectedVariantIds));
             await this.eventBus.publish(new CollectionModificationEvent(ctx, collection, affectedVariantIds));
@@ -571,11 +586,13 @@ export class CollectionService implements OnModuleInit {
         siblings = moveToIndex(input.index, target, siblings);
         siblings = moveToIndex(input.index, target, siblings);
 
 
         await this.connection.getRepository(ctx, Collection).save(siblings);
         await this.connection.getRepository(ctx, Collection).save(siblings);
-        await this.applyFiltersQueue.add({
-            ctx: ctx.serialize(),
-            collectionIds: [target.id],
-        },
-        {   ctx   });
+        await this.applyFiltersQueue.add(
+            {
+                ctx: ctx.serialize(),
+                collectionIds: [target.id],
+            },
+            { ctx },
+        );
         return assertFound(this.findOne(ctx, input.collectionId));
         return assertFound(this.findOne(ctx, input.collectionId));
     }
     }
 
 
@@ -601,61 +618,117 @@ export class CollectionService implements OnModuleInit {
     };
     };
 
 
     /**
     /**
-     * Applies the CollectionFilters
-     *
-     * If applyToChangedVariantsOnly (default: true) is true, then apply collection job will process only changed variants
-     * If applyToChangedVariantsOnly (default: true) is false, then apply collection job will process all variants
-     * This param is used when we update collection and collection filters are changed to update all
-     * variants (because other attributes of collection can be changed https://github.com/vendure-ecommerce/vendure/issues/1015)
+     * Applies the CollectionFilters and returns the IDs of ProductVariants that need to be added or removed.
      */
      */
     private async applyCollectionFiltersInternal(
     private async applyCollectionFiltersInternal(
         collection: Collection,
         collection: Collection,
         applyToChangedVariantsOnly = true,
         applyToChangedVariantsOnly = true,
     ): Promise<ID[]> {
     ): Promise<ID[]> {
+        const masterConnection = this.connection.rawConnection.createQueryRunner('master').connection;
         const ancestorFilters = await this.getAncestorFilters(collection);
         const ancestorFilters = await this.getAncestorFilters(collection);
-        const preIds = await this.getCollectionProductVariantIds(collection);
-        const filteredVariantIds = await this.getFilteredProductVariantIds([
-            ...ancestorFilters,
-            ...(collection.filters || []),
-        ]);
-        const postIds = filteredVariantIds.map(v => v.id);
-        const preIdsSet = new Set(preIds);
-        const postIdsSet = new Set(postIds);
+        const filters = [...ancestorFilters, ...(collection.filters || [])];
 
 
-        const toDeleteIds = preIds.filter(id => !postIdsSet.has(id));
-        const toAddIds = postIds.filter(id => !preIdsSet.has(id));
+        const { collectionFilters } = this.configService.catalogOptions;
 
 
-        try {
-            // First we remove variants that are no longer in the collection
-            const chunkedDeleteIds = this.chunkArray(toDeleteIds, 500);
+        // Create a basic query to retrieve the IDs of product variants that match the collection filters
+        let filteredQb = masterConnection
+            .getRepository(ProductVariant)
+            .createQueryBuilder('productVariant')
+            .select('productVariant.id', 'id')
+            .setFindOptions({ loadEagerRelations: false });
 
 
-            for (const chunkedDeleteId of chunkedDeleteIds) {
-                await this.connection.rawConnection
-                    .createQueryBuilder()
-                    .relation(Collection, 'productVariants')
-                    .of(collection)
-                    .remove(chunkedDeleteId);
+        // If there are no filters, we need to ensure that the query returns no results
+        if (filters.length === 0) {
+            filteredQb.andWhere('1 = 0');
+        }
+
+        //  Applies the CollectionFilters and returns an array of ProductVariant entities which match
+        for (const filterType of collectionFilters) {
+            const filtersOfType = filters.filter(f => f.code === filterType.code);
+            if (filtersOfType.length) {
+                for (const filter of filtersOfType) {
+                    filteredQb = filterType.apply(filteredQb, filter.args);
+                }
             }
             }
+        }
 
 
-            // Then we add variants have been added
-            const chunkedAddIds = this.chunkArray(toAddIds, 500);
+        // Subquery for existing variants in the collection
+        const existingVariantsQb = masterConnection
+            .getRepository(ProductVariant)
+            .createQueryBuilder('variant')
+            .select('variant.id', 'id')
+            .setFindOptions({ loadEagerRelations: false })
+            .innerJoin('variant.collections', 'collection', 'collection.id = :id', { id: collection.id });
+
+        // Using CTE to find variants to add
+        const addQb = masterConnection
+            .createQueryBuilder()
+            .addCommonTableExpression(filteredQb, '_filtered_variants')
+            .addCommonTableExpression(existingVariantsQb, '_existing_variants')
+            .select('filtered_variants.id')
+            .from('_filtered_variants', 'filtered_variants')
+            .leftJoin(
+                '_existing_variants',
+                'existing_variants',
+                'filtered_variants.id = existing_variants.id',
+            )
+            .where('existing_variants.id IS NULL');
+
+        // Using CTE to find the variants to be deleted
+        const removeQb = masterConnection
+            .createQueryBuilder()
+            .addCommonTableExpression(filteredQb, '_filtered_variants')
+            .addCommonTableExpression(existingVariantsQb, '_existing_variants')
+            .select('existing_variants.id')
+            .from('_existing_variants', 'existing_variants')
+            .leftJoin(
+                '_filtered_variants',
+                'filtered_variants',
+                'existing_variants.id = filtered_variants.id',
+            )
+            .where('filtered_variants.id IS NULL')
+            .setParameters({ id: collection.id });
 
 
-            for (const chunkedAddId of chunkedAddIds) {
-                await this.connection.rawConnection
-                    .createQueryBuilder()
-                    .relation(Collection, 'productVariants')
-                    .of(collection)
-                    .add(chunkedAddId);
-            }
+        const [toAddIds, toRemoveIds] = await Promise.all([
+            addQb.getRawMany().then(results => results.map(result => result.id)),
+            removeQb.getRawMany().then(results => results.map(result => result.id)),
+        ]);
+
+        try {
+            await this.connection.rawConnection.transaction(async transactionalEntityManager => {
+                const chunkedDeleteIds = this.chunkArray(toRemoveIds, 5000);
+                const chunkedAddIds = this.chunkArray(toAddIds, 5000);
+                await Promise.all([
+                    // Delete variants that should no longer be in the collection
+                    ...chunkedDeleteIds.map(chunk =>
+                        transactionalEntityManager
+                            .createQueryBuilder()
+                            .relation(Collection, 'productVariants')
+                            .of(collection)
+                            .remove(chunk),
+                    ),
+                    // Adding options that should be in the collection
+                    ...chunkedAddIds.map(chunk =>
+                        transactionalEntityManager
+                            .createQueryBuilder()
+                            .relation(Collection, 'productVariants')
+                            .of(collection)
+                            .add(chunk),
+                    ),
+                ]);
+            });
         } catch (e: any) {
         } catch (e: any) {
             Logger.error(e);
             Logger.error(e);
         }
         }
 
 
         if (applyToChangedVariantsOnly) {
         if (applyToChangedVariantsOnly) {
-            return [...preIds.filter(id => !postIdsSet.has(id)), ...postIds.filter(id => !preIdsSet.has(id))];
-        } else {
-            return [...preIds.filter(id => !postIdsSet.has(id)), ...postIds];
+            return [...toAddIds, ...toRemoveIds];
         }
         }
+
+        return [
+            ...(await existingVariantsQb.getRawMany().then(results => results.map(result => result.id))),
+            ...toRemoveIds,
+        ];
     }
     }
 
 
     /**
     /**
@@ -676,32 +749,6 @@ export class CollectionService implements OnModuleInit {
         return ancestorFilters;
         return ancestorFilters;
     }
     }
 
 
-    /**
-     * Applies the CollectionFilters and returns an array of ProductVariant entities which match.
-     */
-    private async getFilteredProductVariantIds(filters: ConfigurableOperation[]): Promise<Array<{ id: ID }>> {
-        if (filters.length === 0) {
-            return [];
-        }
-        const { collectionFilters } = this.configService.catalogOptions;
-        let qb = this.connection.rawConnection
-            .getRepository(ProductVariant)
-            .createQueryBuilder('productVariant');
-
-        for (const filterType of collectionFilters) {
-            const filtersOfType = filters.filter(f => f.code === filterType.code);
-            if (filtersOfType.length) {
-                for (const filter of filtersOfType) {
-                    qb = filterType.apply(qb, filter.args);
-                }
-            }
-        }
-
-        // This is the most performant (time & memory) way to get
-        // just the variant IDs, which is all we need.
-        return qb.select('productVariant.id', 'id').getRawMany();
-    }
-
     /**
     /**
      * Returns the IDs of the Collection's ProductVariants.
      * Returns the IDs of the Collection's ProductVariants.
      */
      */
@@ -830,11 +877,13 @@ export class CollectionService implements OnModuleInit {
         );
         );
         await this.assetService.assignToChannel(ctx, { channelId: input.channelId, assetIds });
         await this.assetService.assignToChannel(ctx, { channelId: input.channelId, assetIds });
 
 
-        await this.applyFiltersQueue.add({
-            ctx: ctx.serialize(),
-            collectionIds: collectionsToAssign.map(collection => collection.id),
-        },
-        {   ctx   });
+        await this.applyFiltersQueue.add(
+            {
+                ctx: ctx.serialize(),
+                collectionIds: collectionsToAssign.map(collection => collection.id),
+            },
+            { ctx },
+        );
 
 
         return this.connection
         return this.connection
             .findByIdsInChannel(
             .findByIdsInChannel(

+ 1 - 0
packages/core/src/service/services/stock-movement.service.ts

@@ -214,6 +214,7 @@ export class StockMovementService {
                 ctx,
                 ctx,
                 ProductVariant,
                 ProductVariant,
                 orderLine.productVariantId,
                 orderLine.productVariantId,
+                { includeSoftDeleted: true },
             );
             );
             const saleLocations = await this.stockLocationService.getSaleLocations(
             const saleLocations = await this.stockLocationService.getSaleLocations(
                 ctx,
                 ctx,

+ 0 - 2
packages/create/package.json

@@ -24,7 +24,6 @@
     },
     },
     "devDependencies": {
     "devDependencies": {
         "@types/cross-spawn": "^6.0.6",
         "@types/cross-spawn": "^6.0.6",
-        "@types/detect-port": "^1.3.5",
         "@types/fs-extra": "^11.0.4",
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
         "@types/semver": "^7.5.8",
@@ -38,7 +37,6 @@
         "@vendure/common": "^3.0.0",
         "@vendure/common": "^3.0.0",
         "commander": "^11.0.0",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "cross-spawn": "^7.0.3",
-        "detect-port": "^1.5.1",
         "fs-extra": "^11.2.0",
         "fs-extra": "^11.2.0",
         "handlebars": "^4.7.8",
         "handlebars": "^4.7.8",
         "picocolors": "^1.0.0",
         "picocolors": "^1.0.0",

+ 20 - 7
packages/create/src/create-vendure-app.ts

@@ -1,7 +1,6 @@
 /* eslint-disable no-console */
 /* eslint-disable no-console */
 import { intro, note, outro, select, spinner } from '@clack/prompts';
 import { intro, note, outro, select, spinner } from '@clack/prompts';
 import { program } from 'commander';
 import { program } from 'commander';
-import detectPort from 'detect-port';
 import fs from 'fs-extra';
 import fs from 'fs-extra';
 import os from 'os';
 import os from 'os';
 import path from 'path';
 import path from 'path';
@@ -20,7 +19,7 @@ import {
     scaffoldAlreadyExists,
     scaffoldAlreadyExists,
     yarnIsAvailable,
     yarnIsAvailable,
 } from './helpers';
 } from './helpers';
-import { CliLogLevel, DbType, PackageManager } from './types';
+import { CliLogLevel, PackageManager } from './types';
 
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const packageJson = require('../package.json');
 const packageJson = require('../package.json');
@@ -63,15 +62,30 @@ export async function createVendureApp(
     if (!runPreChecks(name, useNpm)) {
     if (!runPreChecks(name, useNpm)) {
         return;
         return;
     }
     }
-    if (await isServerPortInUse()) {
-        console.log(pc.red(`Port ${SERVER_PORT} is in use. Please make it available and then re-try.`));
-        process.exit(1);
-    }
 
 
     intro(
     intro(
         `Let's create a ${pc.blue(pc.bold('Vendure App'))} ✨ ${pc.dim(`v${packageJson.version as string}`)}`,
         `Let's create a ${pc.blue(pc.bold('Vendure App'))} ✨ ${pc.dim(`v${packageJson.version as string}`)}`,
     );
     );
 
 
+    const portSpinner = spinner();
+    let port = SERVER_PORT;
+    const attemptedPortRange = 20;
+    portSpinner.start(`Establishing port...`);
+    while (await isServerPortInUse(port)) {
+        const nextPort = port + 1;
+        portSpinner.message(pc.yellow(`Port ${port} is in use. Attempting to use ${nextPort}`));
+        port = nextPort;
+        if (port > SERVER_PORT + attemptedPortRange) {
+            portSpinner.stop(pc.red('Could not find an available port'));
+            outro(
+                `Please ensure there is a port available between ${SERVER_PORT} and ${SERVER_PORT + attemptedPortRange}`,
+            );
+            process.exit(1);
+        }
+    }
+    portSpinner.stop(`Using port ${port}`);
+    process.env.PORT = port.toString();
+
     const root = path.resolve(name);
     const root = path.resolve(name);
     const appName = path.basename(root);
     const appName = path.basename(root);
     const scaffoldExists = scaffoldAlreadyExists(root, name);
     const scaffoldExists = scaffoldAlreadyExists(root, name);
@@ -211,7 +225,6 @@ export async function createVendureApp(
         const assetsDir = path.join(__dirname, '../assets');
         const assetsDir = path.join(__dirname, '../assets');
 
 
         const initialDataPath = path.join(assetsDir, 'initial-data.json');
         const initialDataPath = path.join(assetsDir, 'initial-data.json');
-        const port = await detectPort(3000);
         const vendureLogLevel =
         const vendureLogLevel =
             logLevel === 'silent'
             logLevel === 'silent'
                 ? LogLevel.Error
                 ? LogLevel.Error

+ 1 - 1
packages/create/src/gather-user-responses.ts

@@ -77,7 +77,7 @@ export async function gatherUserResponses(
         dbType === 'postgres'
         dbType === 'postgres'
             ? await select({
             ? await select({
                   message:
                   message:
-                      'Use SSL to connect to the database? (only enable if you database provider supports SSL)',
+                      'Use SSL to connect to the database? (only enable if your database provider supports SSL)',
                   options: [
                   options: [
                       { label: 'no', value: false },
                       { label: 'no', value: false },
                       { label: 'yes', value: true },
                       { label: 'yes', value: true },

+ 3 - 3
packages/create/src/helpers.ts

@@ -414,13 +414,13 @@ function throwDatabaseSchemaDoesNotExist(dbName: string, schemaName: string) {
     );
     );
 }
 }
 
 
-export function isServerPortInUse(): Promise<boolean> {
+export function isServerPortInUse(port: number): Promise<boolean> {
     // eslint-disable-next-line @typescript-eslint/no-var-requires
     // eslint-disable-next-line @typescript-eslint/no-var-requires
     const tcpPortUsed = require('tcp-port-used');
     const tcpPortUsed = require('tcp-port-used');
     try {
     try {
-        return tcpPortUsed.check(SERVER_PORT);
+        return tcpPortUsed.check(port);
     } catch (e: any) {
     } catch (e: any) {
-        console.log(pc.yellow(`Warning: could not determine whether port ${SERVER_PORT} is available`));
+        console.log(pc.yellow(`Warning: could not determine whether port ${port} is available`));
         return Promise.resolve(false);
         return Promise.resolve(false);
     }
     }
 }
 }

+ 1 - 0
packages/create/templates/.env.hbs

@@ -1,4 +1,5 @@
 APP_ENV=dev
 APP_ENV=dev
+PORT=3000
 COOKIE_SECRET={{ cookieSecret }}
 COOKIE_SECRET={{ cookieSecret }}
 SUPERADMIN_USERNAME={{{ escapeSingle superadminIdentifier }}}
 SUPERADMIN_USERNAME={{{ escapeSingle superadminIdentifier }}}
 SUPERADMIN_PASSWORD={{{ escapeSingle superadminPassword }}}
 SUPERADMIN_PASSWORD={{{ escapeSingle superadminPassword }}}

+ 1 - 0
packages/create/templates/environment.d.hbs

@@ -6,6 +6,7 @@ declare global {
     namespace NodeJS {
     namespace NodeJS {
         interface ProcessEnv {
         interface ProcessEnv {
             APP_ENV: string;
             APP_ENV: string;
+            PORT: string;
             COOKIE_SECRET: string;
             COOKIE_SECRET: string;
             SUPERADMIN_USERNAME: string;
             SUPERADMIN_USERNAME: string;
             SUPERADMIN_PASSWORD: string;
             SUPERADMIN_PASSWORD: string;

+ 6 - 5
packages/create/templates/vendure-config.hbs

@@ -4,17 +4,18 @@ import {
     DefaultSearchPlugin,
     DefaultSearchPlugin,
     VendureConfig,
     VendureConfig,
 } from '@vendure/core';
 } from '@vendure/core';
-import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
+import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import 'dotenv/config';
 import 'dotenv/config';
 import path from 'path';
 import path from 'path';
 
 
 const IS_DEV = process.env.APP_ENV === 'dev';
 const IS_DEV = process.env.APP_ENV === 'dev';
+const serverPort = +process.env.PORT || 3000;
 
 
 export const config: VendureConfig = {
 export const config: VendureConfig = {
     apiOptions: {
     apiOptions: {
-        port: 3000,
+        port: serverPort,
         adminApiPath: 'admin-api',
         adminApiPath: 'admin-api',
         shopApiPath: 'shop-api',
         shopApiPath: 'shop-api',
         // The following options are useful in development mode,
         // The following options are useful in development mode,
@@ -88,7 +89,7 @@ export const config: VendureConfig = {
             outputPath: path.join(__dirname, '../static/email/test-emails'),
             outputPath: path.join(__dirname, '../static/email/test-emails'),
             route: 'mailbox',
             route: 'mailbox',
             handlers: defaultEmailHandlers,
             handlers: defaultEmailHandlers,
-            templatePath: path.join(__dirname, '../static/email/templates'),
+            templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
             globalTemplateVars: {
             globalTemplateVars: {
                 // The following variables will change depending on your storefront implementation.
                 // The following variables will change depending on your storefront implementation.
                 // Here we are assuming a storefront running at http://localhost:8080.
                 // Here we are assuming a storefront running at http://localhost:8080.
@@ -100,9 +101,9 @@ export const config: VendureConfig = {
         }),
         }),
         AdminUiPlugin.init({
         AdminUiPlugin.init({
             route: 'admin',
             route: 'admin',
-            port: 3002,
+            port: serverPort + 2,
             adminUiConfig: {
             adminUiConfig: {
-                apiPort: 3000,
+                apiPort: serverPort,
             },
             },
         }),
         }),
     ],
     ],

+ 2 - 2
packages/dev-server/dev-config.ts

@@ -13,7 +13,7 @@ import {
     VendureConfig,
     VendureConfig,
 } from '@vendure/core';
 } from '@vendure/core';
 import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
 import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
-import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
+import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
 import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
 import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq';
 import 'dotenv/config';
 import 'dotenv/config';
 import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
 import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
@@ -90,7 +90,7 @@ export const devConfig: VendureConfig = {
             devMode: true,
             devMode: true,
             route: 'mailbox',
             route: 'mailbox',
             handlers: defaultEmailHandlers,
             handlers: defaultEmailHandlers,
-            templatePath: path.join(__dirname, '../email-plugin/templates'),
+            templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../email-plugin/templates')),
             outputPath: path.join(__dirname, 'test-emails'),
             outputPath: path.join(__dirname, 'test-emails'),
             globalTemplateVars: {
             globalTemplateVars: {
                 verifyEmailAddressUrl: 'http://localhost:4201/verify',
                 verifyEmailAddressUrl: 'http://localhost:4201/verify',

+ 0 - 1
packages/email-plugin/src/handler/event-handler.ts

@@ -125,7 +125,6 @@ import {
  *   plugins: [
  *   plugins: [
  *     EmailPlugin.init({
  *     EmailPlugin.init({
  *       handler: [...defaultEmailHandlers, quoteRequestedHandler],
  *       handler: [...defaultEmailHandlers, quoteRequestedHandler],
- *       templatePath: path.join(__dirname, 'vendure/email/templates'),
  *       // ... etc
  *       // ... etc
  *     }),
  *     }),
  *   ],
  *   ],

+ 77 - 68
packages/email-plugin/src/plugin.spec.ts

@@ -3,13 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import {
 import {
-    DefaultLogger,
     EventBus,
     EventBus,
     Injector,
     Injector,
     JobQueueService,
     JobQueueService,
     LanguageCode,
     LanguageCode,
     Logger,
     Logger,
-    LogLevel,
     Order,
     Order,
     OrderStateTransitionEvent,
     OrderStateTransitionEvent,
     PluginCommonModule,
     PluginCommonModule,
@@ -23,15 +21,14 @@ import path from 'path';
 import { Readable } from 'stream';
 import { Readable } from 'stream';
 import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
 import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
 
 
-import { orderConfirmationHandler } from './handler/default-email-handlers';
 import { EmailProcessor } from './email-processor';
 import { EmailProcessor } from './email-processor';
-import { EmailSender } from './sender/email-sender';
-import { EmailEventHandler } from './handler/event-handler';
 import { EmailEventListener } from './event-listener';
 import { EmailEventListener } from './event-listener';
+import { orderConfirmationHandler } from './handler/default-email-handlers';
+import { EmailEventHandler } from './handler/event-handler';
 import { EmailPlugin } from './plugin';
 import { EmailPlugin } from './plugin';
-import { EmailDetails, EmailPluginOptions, EmailTransportOptions, LoadTemplateInput } from './types';
-import { TemplateLoader } from './template-loader/template-loader';
-import fs from 'fs-extra';
+import { EmailSender } from './sender/email-sender';
+import { FileBasedTemplateLoader } from './template-loader/file-based-template-loader';
+import { EmailDetails, EmailPluginOptions, EmailTransportOptions } from './types';
 
 
 describe('EmailPlugin', () => {
 describe('EmailPlugin', () => {
     let eventBus: EventBus;
     let eventBus: EventBus;
@@ -54,7 +51,7 @@ describe('EmailPlugin', () => {
                 }),
                 }),
                 PluginCommonModule,
                 PluginCommonModule,
                 EmailPlugin.init({
                 EmailPlugin.init({
-                    templatePath: path.join(__dirname, '../test-templates'),
+                    templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../test-templates')),
                     transport: {
                     transport: {
                         type: 'testing',
                         type: 'testing',
                         onSend,
                         onSend,
@@ -95,7 +92,7 @@ describe('EmailPlugin', () => {
 
 
         await initPluginWithHandlers([handler]);
         await initPluginWithHandlers([handler]);
 
 
-        eventBus.publish(new MockEvent(ctx, true));
+        await eventBus.publish(new MockEvent(ctx, true));
         await pause();
         await pause();
         expect(onSend.mock.calls[0][0].subject).toBe('Hello');
         expect(onSend.mock.calls[0][0].subject).toBe('Hello');
         expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
         expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
@@ -118,11 +115,11 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, false));
+            await eventBus.publish(new MockEvent(ctx, false));
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend).toHaveBeenCalledTimes(1);
             expect(onSend).toHaveBeenCalledTimes(1);
         });
         });
@@ -138,13 +135,13 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
             const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
             const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
 
 
-            eventBus.publish(new MockEvent(ctxWithUser, true));
+            await eventBus.publish(new MockEvent(ctxWithUser, true));
             await pause();
             await pause();
             expect(onSend).toHaveBeenCalledTimes(1);
             expect(onSend).toHaveBeenCalledTimes(1);
         });
         });
@@ -160,11 +157,11 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, false));
+            await eventBus.publish(new MockEvent(ctx, false));
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend).toHaveBeenCalledTimes(1);
             expect(onSend).toHaveBeenCalledTimes(1);
         });
         });
@@ -186,7 +183,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
         });
         });
@@ -201,7 +198,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
             expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
         });
         });
@@ -221,7 +218,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
             expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
         });
         });
@@ -237,7 +234,7 @@ describe('EmailPlugin', () => {
                 globalTemplateVars: { globalVar: 'baz' },
                 globalTemplateVars: { globalVar: 'baz' },
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
         });
         });
@@ -268,7 +265,7 @@ describe('EmailPlugin', () => {
                 },
                 },
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe(`Job hello-service, blue`);
             expect(onSend.mock.calls[0][0].subject).toBe(`Job hello-service, blue`);
         });
         });
@@ -284,7 +281,7 @@ describe('EmailPlugin', () => {
                 globalTemplateVars: { globalVar: 'baz' },
                 globalTemplateVars: { globalVar: 'baz' },
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
             expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
         });
         });
@@ -301,7 +298,7 @@ describe('EmailPlugin', () => {
                 globalTemplateVars: { globalFrom: 'Test <test@test.com>' },
                 globalTemplateVars: { globalFrom: 'Test <test@test.com>' },
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].from).toBe('Test <test@test.com>');
             expect(onSend.mock.calls[0][0].from).toBe('Test <test@test.com>');
         });
         });
@@ -318,7 +315,7 @@ describe('EmailPlugin', () => {
                 globalTemplateVars: { globalVar: 'baz' },
                 globalTemplateVars: { globalVar: 'baz' },
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
         });
         });
@@ -333,7 +330,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
             await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
         });
         });
@@ -355,7 +352,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
             expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
         });
         });
@@ -370,7 +367,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
             expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
             expect(onSend.mock.calls[0][0].body).toContain('Price: €1.23');
             expect(onSend.mock.calls[0][0].body).toContain('Price: €1.23');
@@ -401,13 +398,13 @@ describe('EmailPlugin', () => {
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
             const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
             const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
-            eventBus.publish(new MockEvent(ctxTa, true));
+            await eventBus.publish(new MockEvent(ctxTa, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
             expect(onSend.mock.calls[0][0].body).toContain('Default body.');
             expect(onSend.mock.calls[0][0].body).toContain('Default body.');
 
 
             const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
             const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
-            eventBus.publish(new MockEvent(ctxDe, true));
+            await eventBus.publish(new MockEvent(ctxDe, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
             expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
             expect(onSend.mock.calls[1][0].body).toContain('German body.');
             expect(onSend.mock.calls[1][0].body).toContain('German body.');
@@ -429,7 +426,7 @@ describe('EmailPlugin', () => {
                 });
                 });
 
 
             const ctxEn = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.en } as any);
             const ctxEn = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.en } as any);
-            eventBus.publish(new MockEvent(ctxEn, true));
+            await eventBus.publish(new MockEvent(ctxEn, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
             expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
             expect(onSend.mock.calls[1][0].body).toContain('German body.');
             expect(onSend.mock.calls[1][0].body).toContain('German body.');
@@ -451,7 +448,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(
+            await eventBus.publish(
                 new MockEvent(
                 new MockEvent(
                     RequestContext.deserialize({
                     RequestContext.deserialize({
                         _channel: { code: DEFAULT_CHANNEL_CODE },
                         _channel: { code: DEFAULT_CHANNEL_CODE },
@@ -479,7 +476,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(
+            await eventBus.publish(
                 new MockEvent(
                 new MockEvent(
                     RequestContext.deserialize({
                     RequestContext.deserialize({
                         _channel: { code: DEFAULT_CHANNEL_CODE },
                         _channel: { code: DEFAULT_CHANNEL_CODE },
@@ -506,8 +503,8 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(RequestContext.empty(), false));
-            eventBus.publish(new MockEvent(RequestContext.empty(), true));
+            await eventBus.publish(new MockEvent(RequestContext.empty(), false));
+            await eventBus.publish(new MockEvent(RequestContext.empty(), true));
             await pause();
             await pause();
 
 
             expect(callCount).toBe(1);
             expect(callCount).toBe(1);
@@ -530,7 +527,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].attachments).toEqual([]);
             expect(onSend.mock.calls[0][0].attachments).toEqual([]);
         });
         });
@@ -548,7 +545,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
             expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
@@ -567,7 +564,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
             expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
@@ -586,7 +583,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
@@ -607,7 +604,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
@@ -627,7 +624,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
@@ -647,7 +644,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
@@ -669,7 +666,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
             const attachment = onSend.mock.calls[0][0].attachments[0].content;
@@ -691,7 +688,7 @@ describe('EmailPlugin', () => {
                 ]);
                 ]);
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(testingLogger.warnSpy.mock.calls[0][0]).toContain(
             expect(testingLogger.warnSpy.mock.calls[0][0]).toContain(
@@ -703,7 +700,7 @@ describe('EmailPlugin', () => {
     describe('orderConfirmationHandler', () => {
     describe('orderConfirmationHandler', () => {
         beforeEach(async () => {
         beforeEach(async () => {
             await initPluginWithHandlers([orderConfirmationHandler], {
             await initPluginWithHandlers([orderConfirmationHandler], {
-                templatePath: path.join(__dirname, '../templates'),
+                templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../templates')),
             });
             });
         });
         });
 
 
@@ -721,32 +718,42 @@ describe('EmailPlugin', () => {
         } as any;
         } as any;
 
 
         it('filters events with wrong order state', async () => {
         it('filters events with wrong order state', async () => {
-            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
+            await eventBus.publish(
+                new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order),
+            );
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
-            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
+            await eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
-            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order));
+            await eventBus.publish(
+                new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order),
+            );
             await pause();
             await pause();
             expect(onSend).not.toHaveBeenCalled();
             expect(onSend).not.toHaveBeenCalled();
 
 
-            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await eventBus.publish(
+                new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
+            );
             await pause();
             await pause();
             expect(onSend).toHaveBeenCalledTimes(1);
             expect(onSend).toHaveBeenCalledTimes(1);
         });
         });
 
 
         it('sets the Order Customer emailAddress as recipient', async () => {
         it('sets the Order Customer emailAddress as recipient', async () => {
-            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await eventBus.publish(
+                new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
+            );
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
             expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
         });
         });
 
 
         it('sets the subject', async () => {
         it('sets the subject', async () => {
-            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await eventBus.publish(
+                new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
+            );
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code as string}`);
             expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code as string}`);
@@ -769,7 +776,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('ENOENT: no such file or directory');
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('ENOENT: no such file or directory');
         });
         });
@@ -789,7 +796,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('Parse error on line 3:');
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('Parse error on line 3:');
         });
         });
@@ -812,7 +819,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('something went horribly wrong!');
             expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('something went horribly wrong!');
         });
         });
@@ -839,7 +846,7 @@ describe('EmailPlugin', () => {
                 emailSender: fakeSender,
                 emailSender: fakeSender,
             });
             });
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(send.mock.calls[0][0].subject).toBe('Hello');
             expect(send.mock.calls[0][0].subject).toBe('Hello');
             expect(send.mock.calls[0][0].recipient).toBe('test@test.com');
             expect(send.mock.calls[0][0].recipient).toBe('test@test.com');
@@ -863,7 +870,7 @@ describe('EmailPlugin', () => {
                 .setOptionalAddressFields(() => ({ cc: 'foo@bar.com' }));
                 .setOptionalAddressFields(() => ({ cc: 'foo@bar.com' }));
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].cc).toBe('foo@bar.com');
             expect(onSend.mock.calls[0][0].cc).toBe('foo@bar.com');
@@ -878,7 +885,7 @@ describe('EmailPlugin', () => {
                 .setOptionalAddressFields(() => ({ bcc: 'foo@bar.com' }));
                 .setOptionalAddressFields(() => ({ bcc: 'foo@bar.com' }));
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].bcc).toBe('foo@bar.com');
             expect(onSend.mock.calls[0][0].bcc).toBe('foo@bar.com');
@@ -893,7 +900,7 @@ describe('EmailPlugin', () => {
                 .setOptionalAddressFields(() => ({ replyTo: 'foo@bar.com' }));
                 .setOptionalAddressFields(() => ({ replyTo: 'foo@bar.com' }));
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
 
 
             expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
             expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
@@ -912,12 +919,14 @@ describe('EmailPlugin', () => {
                 .setSubject('Hello')
                 .setSubject('Hello')
                 .setTemplateVars(event => ({ subjectVar: 'foo' }));
                 .setTemplateVars(event => ({ subjectVar: 'foo' }));
             module = await initPluginWithHandlers([handler], {
             module = await initPluginWithHandlers([handler], {
-                transport: async (injector, ctx) => {
+                transport: async (injector, _ctx) => {
                     injectorArg = injector;
                     injectorArg = injector;
-                    ctxArg = ctx;
+                    ctxArg = _ctx;
                     return {
                     return {
                         type: 'testing',
                         type: 'testing',
-                        onSend: () => {},
+                        onSend: () => {
+                            /* */
+                        },
                     };
                     };
                 },
                 },
             });
             });
@@ -925,7 +934,7 @@ describe('EmailPlugin', () => {
                 _channel: { code: DEFAULT_CHANNEL_CODE },
                 _channel: { code: DEFAULT_CHANNEL_CODE },
                 _languageCode: LanguageCode.en,
                 _languageCode: LanguageCode.en,
             } as any);
             } as any);
-            module!.get(EventBus).publish(new MockEvent(ctx, true));
+            await module!.get(EventBus).publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(module).toBeDefined();
             expect(module).toBeDefined();
             expect(typeof module.get(EmailPlugin).options.transport).toBe('function');
             expect(typeof module.get(EmailPlugin).options.transport).toBe('function');
@@ -936,7 +945,7 @@ describe('EmailPlugin', () => {
                 _channel: { code: DEFAULT_CHANNEL_CODE },
                 _channel: { code: DEFAULT_CHANNEL_CODE },
                 _languageCode: LanguageCode.en,
                 _languageCode: LanguageCode.en,
             } as any);
             } as any);
-            module!.get(EventBus).publish(new MockEvent(ctx, true));
+            await module!.get(EventBus).publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(injectorArg?.constructor.name).toBe('Injector');
             expect(injectorArg?.constructor.name).toBe('Injector');
             expect(ctxArg?.constructor.name).toBe('RequestContext');
             expect(ctxArg?.constructor.name).toBe('RequestContext');
@@ -963,7 +972,7 @@ describe('EmailPlugin', () => {
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello');
             expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
             expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
@@ -979,21 +988,21 @@ describe('EmailPlugin', () => {
                 .setFrom('"test from" <noreply@test.com>')
                 .setFrom('"test from" <noreply@test.com>')
                 .setRecipient(() => 'test@test.com')
                 .setRecipient(() => 'test@test.com')
                 .setSubject(async (_e, _ctx, _i) => {
                 .setSubject(async (_e, _ctx, _i) => {
-                    const service = _i.get(MockService)
-                    const mockData = await service.someAsyncMethod()
+                    const service = _i.get(MockService);
+                    const mockData = await service.someAsyncMethod();
                     return `Hello from ${mockData} and {{ subjectVar }}`;
                     return `Hello from ${mockData} and {{ subjectVar }}`;
                 })
                 })
                 .setTemplateVars(event => ({ subjectVar: 'foo' }));
                 .setTemplateVars(event => ({ subjectVar: 'foo' }));
 
 
             await initPluginWithHandlers([handler]);
             await initPluginWithHandlers([handler]);
 
 
-            eventBus.publish(new MockEvent(ctx, true));
+            await eventBus.publish(new MockEvent(ctx, true));
             await pause();
             await pause();
             expect(onSend.mock.calls[0][0].subject).toBe('Hello from loaded data and foo');
             expect(onSend.mock.calls[0][0].subject).toBe('Hello from loaded data and foo');
             expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
             expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
             expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
             expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
         });
         });
-    })
+    });
 });
 });
 
 
 class FakeCustomSender implements EmailSender {
 class FakeCustomSender implements EmailSender {
@@ -1015,4 +1024,4 @@ class MockService {
     someAsyncMethod() {
     someAsyncMethod() {
         return Promise.resolve('loaded data');
         return Promise.resolve('loaded data');
     }
     }
-}
+}

+ 5 - 5
packages/email-plugin/src/plugin.ts

@@ -60,14 +60,14 @@ import {
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
- * import { defaultEmailHandlers, EmailPlugin } from '\@vendure/email-plugin';
+ * import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '\@vendure/email-plugin';
  *
  *
  * const config: VendureConfig = {
  * const config: VendureConfig = {
  *   // Add an instance of the plugin to the plugins array
  *   // Add an instance of the plugin to the plugins array
  *   plugins: [
  *   plugins: [
  *     EmailPlugin.init({
  *     EmailPlugin.init({
  *       handler: defaultEmailHandlers,
  *       handler: defaultEmailHandlers,
- *       templatePath: path.join(__dirname, 'static/email/templates'),
+ *       templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
  *       transport: {
  *       transport: {
  *         type: 'smtp',
  *         type: 'smtp',
  *         host: 'smtp.example.com',
  *         host: 'smtp.example.com',
@@ -226,13 +226,13 @@ import {
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
- * import { defaultEmailHandlers, EmailPlugin } from '\@vendure/email-plugin';
+ * import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '\@vendure/email-plugin';
  * import { MyTransportService } from './transport.services.ts';
  * import { MyTransportService } from './transport.services.ts';
  * const config: VendureConfig = {
  * const config: VendureConfig = {
  *   plugins: [
  *   plugins: [
  *     EmailPlugin.init({
  *     EmailPlugin.init({
  *       handler: defaultEmailHandlers,
  *       handler: defaultEmailHandlers,
- *       templatePath: path.join(__dirname, 'static/email/templates'),
+ *       templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
  *       transport: (injector, ctx) => {
  *       transport: (injector, ctx) => {
  *         if (ctx) {
  *         if (ctx) {
  *           return injector.get(MyTransportService).getSettings(ctx);
  *           return injector.get(MyTransportService).getSettings(ctx);
@@ -260,7 +260,7 @@ import {
  *   devMode: true,
  *   devMode: true,
  *   route: 'mailbox',
  *   route: 'mailbox',
  *   handler: defaultEmailHandlers,
  *   handler: defaultEmailHandlers,
- *   templatePath: path.join(__dirname, 'vendure/email/templates'),
+ *   templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
  *   outputPath: path.join(__dirname, 'test-emails'),
  *   outputPath: path.join(__dirname, 'test-emails'),
  * })
  * })
  * ```
  * ```

+ 22 - 1
packages/email-plugin/src/types.ts

@@ -219,7 +219,7 @@ export interface SMTPTransportOptions extends SMTPTransport.Options {
  *   plugins: [
  *   plugins: [
  *     EmailPlugin.init({
  *     EmailPlugin.init({
  *       handler: defaultEmailHandlers,
  *       handler: defaultEmailHandlers,
- *       templatePath: path.join(__dirname, 'static/email/templates'),
+ *       templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../static/email/templates')),
  *       transport: {
  *       transport: {
  *         type: 'ses',
  *         type: 'ses',
  *         SES: { ses, aws: { SendRawEmailCommand } },
  *         SES: { ses, aws: { SendRawEmailCommand } },
@@ -392,9 +392,30 @@ export interface EmailTemplateConfig {
     subject: string;
     subject: string;
 }
 }
 
 
+/**
+ * @description
+ * The object passed to the {@link TemplateLoader} `loadTemplate()` method.
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email Plugin Types
+ */
 export interface LoadTemplateInput {
 export interface LoadTemplateInput {
+    /**
+     * @description
+     * The type corresponds to the string passed to the EmailEventListener constructor.
+     */
     type: string;
     type: string;
+    /**
+     * @description
+     * The template name is specified by the EmailEventHander's call to
+     * the `addTemplate()` method, and will default to `body.hbs`
+     */
     templateName: string;
     templateName: string;
+    /**
+     * @description
+     * The variables defined by the globalTemplateVars as well as any variables defined in the
+     * EmailEventHandler's `setTemplateVars()` method.
+     */
     templateVars: any;
     templateVars: any;
 }
 }
 
 

+ 1 - 0
packages/payments-plugin/src/mollie/mollie.service.ts

@@ -178,6 +178,7 @@ export class MollieService {
                 authorizedAsOwnerOnly: false,
                 authorizedAsOwnerOnly: false,
                 channel: ctx.channel,
                 channel: ctx.channel,
                 languageCode: ctx.languageCode,
                 languageCode: ctx.languageCode,
+                req: ctx.req,
             });
             });
             await this.addPayment(
             await this.addPayment(
                 adminCtx,
                 adminCtx,