Przeglądaj źródła

docs: Add docs guide on email & notifications

Michael Bromley 2 lat temu
rodzic
commit
72255ce92a

BIN
docs/docs/guides/core-concepts/email/email-plugin-flow.webp


+ 155 - 0
docs/docs/guides/core-concepts/email/index.mdx

@@ -0,0 +1,155 @@
+---
+title: "Email & Notifications"
+---
+
+A typical ecommerce application needs to notify customers of certain events, such as when they place an order or
+when their order has been shipped. This is usually done via email, but can also be done via SMS or push notifications.
+
+## Email
+
+Email is the most common way to notify customers of events, so a default Vendure installation includes our [EmailPlugin](/reference/core-plugins/email-plugin).
+
+The EmailPlugin by default uses [Nodemailer](https://nodemailer.com/about/) to send emails via a variety of
+different transports, including SMTP, SendGrid, Mailgun, and more.
+The plugin is configured with a list of [EmailEventHandlers](/reference/core-plugins/email-plugin/email-event-handler) which are responsible for
+sending emails in response to specific events.
+
+:::note
+This guide will cover some of the main concepts of the EmailPlugin, but for a more in-depth look at how to configure
+and use it, see the [EmailPlugin API docs](/reference/core-plugins/email-plugin).
+:::
+
+Here's an illustration of the flow of an email being sent:
+
+![Email plugin flow](./email-plugin-flow.webp)
+
+All emails are triggered by a particular [Event](/guides/developer-guide/events/) - in this case when the state of an
+Order changes. The EmailPlugin ships with a set of [default email handlers](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/default-email-handlers.ts),
+one of which is responsible for sending "order confirmation" emails.
+
+### EmailEventHandlers
+
+Let's take a closer look at a simplified version of the `orderConfirmationHandler`:
+
+```ts
+import { OrderStateTransitionEvent } from '@vendure/core';
+import { EmailEventListener, transformOrderLineAssetUrls, hydrateShippingLines } from '@vendure/email-plugin';
+
+// The 'order-confirmation' string is used by the EmailPlugin to identify
+// which template to use when rendering the email.
+export const orderConfirmationHandler = new EmailEventListener('order-confirmation')
+    .on(OrderStateTransitionEvent)
+    // Only send the email when the Order is transitioning to the
+    // "PaymentSettled" state and the Order has a customer associated with it.
+    .filter(
+        event =>
+            event.toState === 'PaymentSettled'
+            && !!event.order.customer,
+    )
+    // We commonly need to load some additional data to be able to render the email
+    // template. This is done via the `loadData()` method. In this method we are
+    // mutating the Order object to ensure that product images are correctly
+    // displayed in the email, as well as fetching shipping line data from the database.
+    .loadData(async ({ event, injector }) => {
+        transformOrderLineAssetUrls(event.ctx, event.order, injector);
+        const shippingLines = await hydrateShippingLines(event.ctx, event.order, injector);
+        return { shippingLines };
+    })
+    // Here we are setting the recipient of the email to be the
+    // customer's email address.
+    .setRecipient(event => event.order.customer!.emailAddress)
+    // We can interpolate variables from the EmailPlugin's configured
+    // `globalTemplateVars` object.
+    .setFrom('{{ fromAddress }}')
+    // We can also interpolate variables made available by the
+    // `setTemplateVars()` method below
+    .setSubject('Order confirmation for #{{ order.code }}')
+    // The object returned here defines the variables which are
+    // available to the email template.
+    .setTemplateVars(event => ({ order: event.order, shippingLines: event.data.shippingLines }))
+```
+
+To recap:
+
+- The handler listens for a specific event
+- It optionally filters those events to determine whether an email should be sent
+- It specifies the details of the email to be sent, including the recipient, subject, template variables, etc.
+
+The full range of methods available when setting up an EmailEventHandler can be found in the [EmailEventHandler API docs](/reference/core-plugins/email-plugin/email-event-handler).
+
+### Email variables
+
+In the example above, we used the `setTemplateVars()` method to define the variables which are available to the email template.
+Additionally, there are global variables which are made available to _all_ email templates & EmailEventHandlers. These are
+defined in the `globalTemplateVars` property of the EmailPlugin config:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { EmailPlugin } from '@vendure/email-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        EmailPlugin.init({
+            // ...
+            // highlight-start
+            globalTemplateVars: {
+                fromAddress: '"MyShop" <noreply@myshop.com>',
+                verifyEmailAddressUrl: 'https://www.myshop.com/verify',
+                passwordResetUrl: 'https://www.myshop.com/password-reset',
+                changeEmailAddressUrl: 'https://www.myshop.com/verify-email-address-change'
+            },
+            // highlight-end
+        }),
+    ],
+};
+```
+
+### Email integrations
+
+The EmailPlugin is designed to be flexible enough to work with many different email services. The default
+configuration uses Nodemailer to send emails via SMTP, but you can easily configure it to use a different
+transport. For instance:
+
+- [AWS SES](https://www.vendure.io/marketplace/aws-ses)
+- [SendGrid](https://www.vendure.io/marketplace/sendgrid)
+
+## Other notification methods
+
+The pattern of listening for events and triggering some action in response is not limited to emails. You can
+use the same pattern to trigger other actions, such as sending SMS messages or push notifications. For instance,
+let's say you wanted to create a plugin which sends an SMS message to the customer when their order is shipped.
+
+:::note
+This is just a simplified example to illustrate the pattern.
+:::
+
+```ts title="src/plugins/sms-plugin/sms-plugin.ts"
+import { OnModuleInit } from '@nestjs/common';
+import { PluginCommonModule, VendurePlugin, EventBus } from '@vendure/core';
+import { OrderStateTransitionEvent } from '@vendure/core';
+
+// A custom service which sends SMS messages
+// using a third-party SMS provider such as Twilio.
+import { SmsService } from './sms.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [SmsService],
+})
+export class SmsPlugin implements OnModuleInit {
+    constructor(
+        private eventBus: EventBus,
+        private smsService: SmsService,
+    ) {}
+
+    onModuleInit() {
+        this.eventBus
+            .ofType(OrderStateTransitionEvent)
+            .filter(event => event.toState === 'Shipped')
+            .subscribe(event => {
+                this.smsService.sendOrderShippedMessage(event.order);
+            });
+    }
+}
+```

+ 1 - 1
docs/docs/guides/developer-guide/events/index.mdx

@@ -91,7 +91,7 @@ lifecycle hooks of a plugin or service (see [NestJS Lifecycle events](https://do
 Here's an example where we subscribe to the `ProductEvent` and use it to trigger a rebuild of a static storefront:
 Here's an example where we subscribe to the `ProductEvent` and use it to trigger a rebuild of a static storefront:
 
 
 ```ts title="src/plugins/storefront-build/storefront-build.plugin.ts"
 ```ts title="src/plugins/storefront-build/storefront-build.plugin.ts"
-import { Injectable, OnModuleInit } from '@nestjs/common';
+import { OnModuleInit } from '@nestjs/common';
 import { EventBus, ProductEvent, PluginCommonModule, VendurePlugin } from '@vendure/core';
 import { EventBus, ProductEvent, PluginCommonModule, VendurePlugin } from '@vendure/core';
 
 
 import { StorefrontBuildService } from './services/storefront-build.service';
 import { StorefrontBuildService } from './services/storefront-build.service';

+ 1 - 1
docs/docs/guides/developer-guide/the-api-layer/index.mdx

@@ -147,7 +147,7 @@ data transformation.
 
 
 Guards, interceptors, pipes and filters can be added to your own custom resolvers and controllers
 Guards, interceptors, pipes and filters can be added to your own custom resolvers and controllers
 using the NestJS decorators as given in the NestJS docs. However, a common pattern is to register them globally via a
 using the NestJS decorators as given in the NestJS docs. However, a common pattern is to register them globally via a
-plugin:
+[Vendure plugin](/guides/developer-guide/plugins/):
 
 
 ```ts title="src/plugins/my-plugin/my-plugin.ts"
 ```ts title="src/plugins/my-plugin/my-plugin.ts"
 import { VendurePlugin } from '@vendure/core';
 import { VendurePlugin } from '@vendure/core';

+ 3 - 0
docs/docs/guides/developer-guide/updating/index.md

@@ -33,6 +33,9 @@ Then run `npm install` or `yarn install` depending on which package manager you
 
 
 If you are using UI extensions to create your own custom Admin UI using the [`compileUiExtensions`](/reference/admin-ui-api/ui-devkit/compile-ui-extensions/) function, then you'll need to **delete and re-compile your admin-ui directory after upgrading** (this is the directory specified by the [`outputPath`](/reference/admin-ui-api/ui-devkit/ui-extension-compiler-options#outputpath) property).
 If you are using UI extensions to create your own custom Admin UI using the [`compileUiExtensions`](/reference/admin-ui-api/ui-devkit/compile-ui-extensions/) function, then you'll need to **delete and re-compile your admin-ui directory after upgrading** (this is the directory specified by the [`outputPath`](/reference/admin-ui-api/ui-devkit/ui-extension-compiler-options#outputpath) property).
 
 
+If you also have an `.angular` directory in your project, you should delete this too after the update to ensure that any stale cached files are removed.
+
+
 ## Versioning Policy & Breaking changes
 ## Versioning Policy & Breaking changes
 
 
 Vendure generally follows the [SemVer convention](https://semver.org/) for version numbering. This means that breaking API changes will only be introduced with changes to the major version (the first of the 3 digits in the version).
 Vendure generally follows the [SemVer convention](https://semver.org/) for version numbering. This means that breaking API changes will only be introduced with changes to the major version (the first of the 3 digits in the version).

+ 3 - 2
docs/docs/reference/core-plugins/elasticsearch-plugin/index.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## ElasticsearchPlugin
 ## ElasticsearchPlugin
 
 
-<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="222" packageName="@vendure/elasticsearch-plugin" />
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="223" packageName="@vendure/elasticsearch-plugin" />
 
 
 This plugin allows your product search to be powered by [Elasticsearch](https://github.com/elastic/elasticsearch) - a powerful Open Source search
 This plugin allows your product search to be powered by [Elasticsearch](https://github.com/elastic/elasticsearch) - a powerful Open Source search
 engine. This is a drop-in replacement for the DefaultSearchPlugin which exposes many powerful configuration options enabling your storefront
 engine. This is a drop-in replacement for the DefaultSearchPlugin which exposes many powerful configuration options enabling your storefront
@@ -22,7 +22,8 @@ advanced Elasticsearch features like spacial search.
 
 
 **Requires Elasticsearch v7.0 < required Elasticsearch version < 7.10 **
 **Requires Elasticsearch v7.0 < required Elasticsearch version < 7.10 **
 Elasticsearch version 7.10.2 will throw error due to incompatibility with elasticsearch-js client.
 Elasticsearch version 7.10.2 will throw error due to incompatibility with elasticsearch-js client.
-[Check here for more info](https://github.com/elastic/elasticsearch-js/issues/1519)
+[Check here for more info](https://github.com/elastic/elasticsearch-js/issues/1519).
+
 `yarn add @elastic/elasticsearch @vendure/elasticsearch-plugin`
 `yarn add @elastic/elasticsearch @vendure/elasticsearch-plugin`
 
 
 or
 or

+ 0 - 59
docs/docs/reference/core-plugins/email-plugin/custom-template-loader.md

@@ -1,59 +0,0 @@
----
-title: "Custom Template Loader"
-isDefaultIndex: false
-generated: true
----
-<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
-import MemberInfo from '@site/src/components/MemberInfo';
-import GenerationInfo from '@site/src/components/GenerationInfo';
-import MemberDescription from '@site/src/components/MemberDescription';
-
-
-## TemplateLoader
-
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="391" packageName="@vendure/email-plugin" />
-
-Load an email template based on the given request context, type and template name
-and return the template as a string.
-
-*Example*
-
-```ts
-import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
-
-class MyTemplateLoader implements TemplateLoader {
-     loadTemplate(injector, ctx, { type, templateName }){
-         return myCustomTemplateFunction(ctx);
-     }
-}
-
-// In vendure-config.ts:
-...
-EmailPlugin.init({
-    templateLoader: new MyTemplateLoader()
-    ...
-})
-```
-
-```ts title="Signature"
-interface TemplateLoader {
-    loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
-    loadPartials?(): Promise<Partial[]>;
-}
-```
-
-<div className="members-wrapper">
-
-### 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;`}   />
-
-
-### loadPartials
-
-<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
-
-
-
-
-</div>

+ 14 - 14
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 };
     globalTemplateVars?: { [key: string]: any };
@@ -38,43 +38,43 @@ 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/custom-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 }`}   />
 <MemberInfo kind="property" type={`{ [key: string]: any }`}   />
 
 
-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"
+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.
 email.
 ### 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.
 
 
 
 

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

@@ -130,7 +130,7 @@ type EmailAttachment = Omit<Attachment, 'raw'> & { path?: string }
 
 
 ## SetTemplateVarsFn
 ## SetTemplateVarsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="411" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="414" 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 +145,7 @@ type SetTemplateVarsFn<Event> = (
 
 
 ## SetAttachmentsFn
 ## SetAttachmentsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="425" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="428" 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 +158,7 @@ type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<Ema
 
 
 ## OptionalAddressFields
 ## OptionalAddressFields
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="435" packageName="@vendure/email-plugin" since="1.1.0" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="438" packageName="@vendure/email-plugin" since="1.1.0" />
 
 
 Optional address-related fields for sending the email.
 Optional address-related fields for sending the email.
 
 
@@ -194,7 +194,7 @@ An email address that will appear on the _Reply-To:_ field
 
 
 ## SetOptionalAddressFieldsFn
 ## SetOptionalAddressFieldsFn
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="461" packageName="@vendure/email-plugin" since="1.1.0" />
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="464" 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>.
 
 

+ 66 - 0
docs/docs/reference/core-plugins/email-plugin/email-utils.md

@@ -0,0 +1,66 @@
+---
+title: "Email Utils"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## transformOrderLineAssetUrls
+
+<GenerationInfo sourceFile="packages/email-plugin/src/default-email-handlers.ts" sourceLine="101" packageName="@vendure/email-plugin" />
+
+Applies the configured `AssetStorageStrategy.toAbsoluteUrl()` function to each of the
+OrderLine's `featuredAsset.preview` properties, so that they can be correctly displayed
+in the email template.
+This is required since that step usually happens at the API in middleware, which is not
+applicable in this context. So we need to do it manually.
+
+**Note: Mutates the Order object**
+
+```ts title="Signature"
+function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, injector: Injector): Order
+```
+Parameters
+
+### ctx
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>`} />
+
+### order
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/entities/order#order'>Order</a>`} />
+
+### injector
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/common/injector#injector'>Injector</a>`} />
+
+
+
+## hydrateShippingLines
+
+<GenerationInfo sourceFile="packages/email-plugin/src/default-email-handlers.ts" sourceLine="122" packageName="@vendure/email-plugin" />
+
+Ensures that the ShippingLines are hydrated so that we can use the
+`shippingMethod.name` property in the email template.
+
+```ts title="Signature"
+function hydrateShippingLines(ctx: RequestContext, order: Order, injector: Injector): Promise<ShippingLine[]>
+```
+Parameters
+
+### ctx
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>`} />
+
+### order
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/entities/order#order'>Order</a>`} />
+
+### injector
+
+<MemberInfo kind="parameter" type={`<a href='/reference/typescript-api/common/injector#injector'>Injector</a>`} />
+

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

@@ -11,15 +11,14 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 
 ## EmailPlugin
 ## EmailPlugin
 
 
-<GenerationInfo sourceFile="packages/email-plugin/src/plugin.ts" sourceLine="277" packageName="@vendure/email-plugin" />
+<GenerationInfo sourceFile="packages/email-plugin/src/plugin.ts" sourceLine="276" packageName="@vendure/email-plugin" />
 
 
 The EmailPlugin creates and sends transactional emails based on Vendure events. By default, it uses an [MJML](https://mjml.io/)-based
 The EmailPlugin creates and sends transactional emails based on Vendure events. By default, it uses an [MJML](https://mjml.io/)-based
 email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
 email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
 
 
 ## High-level description
 ## High-level description
-Vendure has an internal events system (see <a href='/reference/typescript-api/events/event-bus#eventbus'>EventBus</a>) that allows plugins to subscribe to events. The EmailPlugin is configured with
-<a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>s that listen for a specific event and when it is published, the handler defines which template to use to generate
-the resulting email.
+Vendure has an internal events system (see <a href='/reference/typescript-api/events/event-bus#eventbus'>EventBus</a>) that allows plugins to subscribe to events. The EmailPlugin is configured with <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>s
+that listen for a specific event and when it is published, the handler defines which template to use to generate the resulting email.
 
 
 The plugin comes with a set of default handlers for the following events:
 The plugin comes with a set of default handlers for the following events:
 - Order confirmation
 - Order confirmation
@@ -88,7 +87,7 @@ language which makes the task of creating responsive email markup simple. By def
 
 
 Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
 Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
 
 
-```HTML
+```html
 <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
 <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
 
 
 <p>Thank you for your order!</p>
 <p>Thank you for your order!</p>
@@ -136,12 +135,12 @@ import { CustomerService } from '@vendure/core';
 
 
 // This allows you to then customize each handler to your needs.
 // This allows you to then customize each handler to your needs.
 // For example, let's set a new subject line to the order confirmation:
 // For example, let's set a new subject line to the order confirmation:
-orderConfirmationHandler
+const myOrderConfirmationHandler = orderConfirmationHandler
   .setSubject(`We received your order!`);
   .setSubject(`We received your order!`);
 
 
 // Another example: loading additional data and setting new
 // Another example: loading additional data and setting new
 // template variables.
 // template variables.
-passwordResetHandler
+const myPasswordResetHandler = passwordResetHandler
   .loadData(async ({ event, injector }) => {
   .loadData(async ({ event, injector }) => {
     const customerService = injector.get(CustomerService);
     const customerService = injector.get(CustomerService);
     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
@@ -156,9 +155,9 @@ passwordResetHandler
 // individually
 // individually
 EmailPlugin.init({
 EmailPlugin.init({
   handlers: [
   handlers: [
-    orderConfirmationHandler,
+    myOrderConfirmationHandler,
+    myPasswordResetHandler,
     emailVerificationHandler,
     emailVerificationHandler,
-    passwordResetHandler,
     emailAddressChangeHandler,
     emailAddressChangeHandler,
   ],
   ],
   // ...
   // ...
@@ -187,10 +186,10 @@ const config: VendureConfig = {
           return injector.get(MyTransportService).getSettings(ctx);
           return injector.get(MyTransportService).getSettings(ctx);
         } else {
         } else {
           return {
           return {
-             type: 'smtp',
-             host: 'smtp.example.com',
-             // ... etc.
-           }
+            type: 'smtp',
+            host: 'smtp.example.com',
+            // ... etc.
+          }
         }
         }
       }
       }
     }),
     }),

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

@@ -0,0 +1,100 @@
+---
+title: "TemplateLoader"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## TemplateLoader
+
+<GenerationInfo sourceFile="packages/email-plugin/src/types.ts" sourceLine="392" packageName="@vendure/email-plugin" />
+
+Loads email templates based on the given request context, type and template name
+and return the template as a string.
+
+*Example*
+
+```ts
+import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
+
+class MyTemplateLoader implements TemplateLoader {
+     loadTemplate(injector, ctx, { type, templateName }){
+         return myCustomTemplateFunction(ctx);
+     }
+}
+
+// In vendure-config.ts:
+...
+EmailPlugin.init({
+    templateLoader: new MyTemplateLoader()
+    ...
+})
+```
+
+```ts title="Signature"
+interface TemplateLoader {
+    loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
+    loadPartials?(): Promise<Partial[]>;
+}
+```
+
+<div className="members-wrapper">
+
+### 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;`}   />
+
+Load template and return it's content as a string
+### loadPartials
+
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+
+Load partials and return their contents.
+This method is only called during initialization, i.e. during server startup.
+
+
+</div>
+
+
+## FileBasedTemplateLoader
+
+<GenerationInfo sourceFile="packages/email-plugin/src/template-loader.ts" sourceLine="15" packageName="@vendure/email-plugin" />
+
+Loads email templates from the local file system. This is the default
+loader used by the EmailPlugin.
+
+```ts title="Signature"
+class FileBasedTemplateLoader implements TemplateLoader {
+    constructor(templatePath: string)
+    loadTemplate(_injector: Injector, _ctx: RequestContext, { type, templateName }: LoadTemplateInput) => Promise<string>;
+    loadPartials() => Promise<Partial[]>;
+}
+```
+* Implements: <code><a href='/reference/core-plugins/email-plugin/template-loader#templateloader'>TemplateLoader</a></code>
+
+
+
+<div className="members-wrapper">
+
+### constructor
+
+<MemberInfo kind="method" type={`(templatePath: string) => FileBasedTemplateLoader`}   />
+
+
+### 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;`}   />
+
+
+### loadPartials
+
+<MemberInfo kind="method" type={`() => Promise&#60;Partial[]&#62;`}   />
+
+
+
+
+</div>

+ 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
package.json

@@ -12,7 +12,7 @@
     "format": "prettier --write --html-whitespace-sensitivity ignore",
     "format": "prettier --write --html-whitespace-sensitivity ignore",
     "docs:generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "docs:generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "docs:generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
     "docs:generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
-    "docs:build": "yarn docs:generate-graphql-docs && yarn docs:generate-typescript-docs",
+    "docs:build": "yarn docs:generate-typescript-docs && yarn docs:generate-graphql-docs",
     "codegen": "tsc -p scripts/codegen/plugins && ts-node scripts/codegen/generate-graphql-types.ts",
     "codegen": "tsc -p scripts/codegen/plugins && ts-node scripts/codegen/generate-graphql-types.ts",
     "version": "yarn check-imports && yarn check-angular-versions && yarn build && yarn check-core-type-defs && yarn generate-changelog && git add CHANGELOG* && git add */version.ts",
     "version": "yarn check-imports && yarn check-angular-versions && yarn build && yarn check-core-type-defs && yarn generate-changelog && git add CHANGELOG* && git add */version.ts",
     "dev-server:start": "cd packages/dev-server && yarn start",
     "dev-server:start": "cd packages/dev-server && yarn start",

+ 6 - 0
packages/email-plugin/src/default-email-handlers.ts

@@ -94,6 +94,9 @@ export const defaultEmailHandlers: Array<EmailEventHandler<any, any>> = [
  * applicable in this context. So we need to do it manually.
  * applicable in this context. So we need to do it manually.
  *
  *
  * **Note: Mutates the Order object**
  * **Note: Mutates the Order object**
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email utils
  */
  */
 export function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, injector: Injector): Order {
 export function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, injector: Injector): Order {
     const { assetStorageStrategy } = injector.get(ConfigService).assetOptions;
     const { assetStorageStrategy } = injector.get(ConfigService).assetOptions;
@@ -112,6 +115,9 @@ export function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, i
  * @description
  * @description
  * Ensures that the ShippingLines are hydrated so that we can use the
  * Ensures that the ShippingLines are hydrated so that we can use the
  * `shippingMethod.name` property in the email template.
  * `shippingMethod.name` property in the email template.
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email utils
  */
  */
 export async function hydrateShippingLines(
 export async function hydrateShippingLines(
     ctx: RequestContext,
     ctx: RequestContext,

+ 11 - 12
packages/email-plugin/src/plugin.ts

@@ -43,9 +43,8 @@ import {
  * email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
  * email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
  *
  *
  * ## High-level description
  * ## High-level description
- * Vendure has an internal events system (see {@link EventBus}) that allows plugins to subscribe to events. The EmailPlugin is configured with
- * {@link EmailEventHandler}s that listen for a specific event and when it is published, the handler defines which template to use to generate
- * the resulting email.
+ * Vendure has an internal events system (see {@link EventBus}) that allows plugins to subscribe to events. The EmailPlugin is configured with {@link EmailEventHandler}s
+ * that listen for a specific event and when it is published, the handler defines which template to use to generate the resulting email.
  *
  *
  * The plugin comes with a set of default handlers for the following events:
  * The plugin comes with a set of default handlers for the following events:
  * - Order confirmation
  * - Order confirmation
@@ -113,7 +112,7 @@ import {
  *
  *
  * Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
  * Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
  *
  *
- * ```HTML
+ * ```html
  * <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
  * <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
  *
  *
  * <p>Thank you for your order!</p>
  * <p>Thank you for your order!</p>
@@ -161,12 +160,12 @@ import {
  *
  *
  * // This allows you to then customize each handler to your needs.
  * // This allows you to then customize each handler to your needs.
  * // For example, let's set a new subject line to the order confirmation:
  * // For example, let's set a new subject line to the order confirmation:
- * orderConfirmationHandler
+ * const myOrderConfirmationHandler = orderConfirmationHandler
  *   .setSubject(`We received your order!`);
  *   .setSubject(`We received your order!`);
  *
  *
  * // Another example: loading additional data and setting new
  * // Another example: loading additional data and setting new
  * // template variables.
  * // template variables.
- * passwordResetHandler
+ * const myPasswordResetHandler = passwordResetHandler
  *   .loadData(async ({ event, injector }) => {
  *   .loadData(async ({ event, injector }) => {
  *     const customerService = injector.get(CustomerService);
  *     const customerService = injector.get(CustomerService);
  *     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
  *     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
@@ -181,9 +180,9 @@ import {
  * // individually
  * // individually
  * EmailPlugin.init({
  * EmailPlugin.init({
  *   handlers: [
  *   handlers: [
- *     orderConfirmationHandler,
+ *     myOrderConfirmationHandler,
+ *     myPasswordResetHandler,
  *     emailVerificationHandler,
  *     emailVerificationHandler,
- *     passwordResetHandler,
  *     emailAddressChangeHandler,
  *     emailAddressChangeHandler,
  *   ],
  *   ],
  *   // ...
  *   // ...
@@ -211,10 +210,10 @@ import {
  *           return injector.get(MyTransportService).getSettings(ctx);
  *           return injector.get(MyTransportService).getSettings(ctx);
  *         } else {
  *         } else {
  *           return {
  *           return {
-                type: 'smtp',
-                host: 'smtp.example.com',
-                // ... etc.
-              }
+ *             type: 'smtp',
+ *             host: 'smtp.example.com',
+ *             // ... etc.
+ *           }
  *         }
  *         }
  *       }
  *       }
  *     }),
  *     }),

+ 16 - 9
packages/email-plugin/src/template-loader.ts

@@ -1,14 +1,19 @@
 import { Injector, RequestContext } from '@vendure/core';
 import { Injector, RequestContext } from '@vendure/core';
 import fs from 'fs/promises';
 import fs from 'fs/promises';
 import path from 'path';
 import path from 'path';
+
 import { LoadTemplateInput, Partial, TemplateLoader } from './types';
 import { LoadTemplateInput, Partial, TemplateLoader } from './types';
 
 
 /**
 /**
- * Loads email templates according to the configured TemplateConfig values.
+ * @description
+ * Loads email templates from the local file system. This is the default
+ * loader used by the EmailPlugin.
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage TemplateLoader
  */
  */
 export class FileBasedTemplateLoader implements TemplateLoader {
 export class FileBasedTemplateLoader implements TemplateLoader {
-
-    constructor(private templatePath: string) { }
+    constructor(private templatePath: string) {}
 
 
     async loadTemplate(
     async loadTemplate(
         _injector: Injector,
         _injector: Injector,
@@ -22,11 +27,13 @@ export class FileBasedTemplateLoader implements TemplateLoader {
     async loadPartials(): Promise<Partial[]> {
     async loadPartials(): Promise<Partial[]> {
         const partialsPath = path.join(this.templatePath, 'partials');
         const partialsPath = path.join(this.templatePath, 'partials');
         const partialsFiles = await fs.readdir(partialsPath);
         const partialsFiles = await fs.readdir(partialsPath);
-        return Promise.all(partialsFiles.map(async (file) => {
-            return {
-                name: path.basename(file, '.hbs'),
-                content: await fs.readFile(path.join(partialsPath, file), 'utf-8')
-            }
-        }));
+        return Promise.all(
+            partialsFiles.map(async file => {
+                return {
+                    name: path.basename(file, '.hbs'),
+                    content: await fs.readFile(path.join(partialsPath, file), 'utf-8'),
+                };
+            }),
+        );
     }
     }
 }
 }

+ 6 - 3
packages/email-plugin/src/types.ts

@@ -364,7 +364,7 @@ export interface Partial {
 
 
 /**
 /**
  * @description
  * @description
- * Load an email template based on the given request context, type and template name
+ * Loads email templates based on the given request context, type and template name
  * and return the template as a string.
  * and return the template as a string.
  *
  *
  * @example
  * @example
@@ -386,16 +386,19 @@ export interface Partial {
  * ```
  * ```
  *
  *
  * @docsCategory core plugins/EmailPlugin
  * @docsCategory core plugins/EmailPlugin
- * @docsPage Custom Template Loader
+ * @docsPage TemplateLoader
+ * @docsWeight 0
  */
  */
 export interface TemplateLoader {
 export interface TemplateLoader {
     /**
     /**
+     * @description
      * Load template and return it's content as a string
      * Load template and return it's content as a string
      */
      */
     loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
     loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
     /**
     /**
+     * @description
      * Load partials and return their contents.
      * Load partials and return their contents.
-     * This method is only called during initalization, i.e. during server startup.
+     * This method is only called during initialization, i.e. during server startup.
      */
      */
     loadPartials?(): Promise<Partial[]>;
     loadPartials?(): Promise<Partial[]>;
 }
 }