Browse Source

docs: Add docs guide on email & notifications

Michael Bromley 2 years ago
parent
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:
 
 ```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 { 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
 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"
 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 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
 
 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
 
-<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
 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 **
 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`
 
 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 {
     templatePath?: string;
     templateLoader?: TemplateLoader;
-    transport:
-        | EmailTransportOptions
-        | ((
-              injector?: Injector,
-              ctx?: RequestContext,
+    transport:
+        | EmailTransportOptions
+        | ((
+              injector?: Injector,
+              ctx?: RequestContext,
           ) => EmailTransportOptions | Promise<EmailTransportOptions>);
     handlers: Array<EmailEventHandler<string, any>>;
     globalTemplateVars?: { [key: string]: any };
@@ -38,43 +38,43 @@ interface EmailPluginOptions {
 
 <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`.
 ### 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`
 ### 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.
 ### 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;`}   />
 
-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.
 ### globalTemplateVars
 
 <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.
 ### 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>"   />
 
-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.
 ### 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>"   />
 
-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.
 
 

+ 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
 
-<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.
 See <a href='/reference/core-plugins/email-plugin/email-event-handler#emaileventhandler'>EmailEventHandler</a>.setTemplateVars().
@@ -145,7 +145,7 @@ type SetTemplateVarsFn<Event> = (
 
 ## 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.
 See https://nodemailer.com/message/attachments/ for more information about
@@ -158,7 +158,7 @@ type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<Ema
 
 ## 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.
 
@@ -194,7 +194,7 @@ An email address that will appear on the _Reply-To:_ field
 
 ## 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>.
 

+ 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
 
-<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
 email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
 
 ## 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:
 - 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/):
 
-```HTML
+```html
 <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</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.
 // For example, let's set a new subject line to the order confirmation:
-orderConfirmationHandler
+const myOrderConfirmationHandler = orderConfirmationHandler
   .setSubject(`We received your order!`);
 
 // Another example: loading additional data and setting new
 // template variables.
-passwordResetHandler
+const myPasswordResetHandler = passwordResetHandler
   .loadData(async ({ event, injector }) => {
     const customerService = injector.get(CustomerService);
     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
@@ -156,9 +155,9 @@ passwordResetHandler
 // individually
 EmailPlugin.init({
   handlers: [
-    orderConfirmationHandler,
+    myOrderConfirmationHandler,
+    myPasswordResetHandler,
     emailVerificationHandler,
-    passwordResetHandler,
     emailAddressChangeHandler,
   ],
   // ...
@@ -187,10 +186,10 @@ const config: VendureConfig = {
           return injector.get(MyTransportService).getSettings(ctx);
         } else {
           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>;
     'relation-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 };
-    '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>;
 }
@@ -107,7 +107,7 @@ type DefaultFormConfigHash = {
 
 ### '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'
@@ -117,12 +117,12 @@ type DefaultFormConfigHash = {
 
 ### 'textarea-form-input'
 
-<MemberInfo kind="property" type={`{         spellcheck?: boolean;     }`}   />
+<MemberInfo kind="property" type={`{
         spellcheck?: boolean;
     }`}   />
 
 
 ### 'product-multi-form-input'
 
-<MemberInfo kind="property" type={`{         selectionMode?: 'product' | 'variant';     }`}   />
+<MemberInfo kind="property" type={`{
         selectionMode?: 'product' | 'variant';
     }`}   />
 
 
 ### 'combination-mode-form-input'

+ 1 - 1
package.json

@@ -12,7 +12,7 @@
     "format": "prettier --write --html-whitespace-sensitivity ignore",
     "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: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",
     "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",

+ 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.
  *
  * **Note: Mutates the Order object**
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email utils
  */
 export function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, injector: Injector): Order {
     const { assetStorageStrategy } = injector.get(ConfigService).assetOptions;
@@ -112,6 +115,9 @@ export function transformOrderLineAssetUrls(ctx: RequestContext, order: Order, i
  * @description
  * Ensures that the ShippingLines are hydrated so that we can use the
  * `shippingMethod.name` property in the email template.
+ *
+ * @docsCategory core plugins/EmailPlugin
+ * @docsPage Email utils
  */
 export async function hydrateShippingLines(
     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.
  *
  * ## 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:
  * - 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/):
  *
- * ```HTML
+ * ```html
  * <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
  *
  * <p>Thank you for your order!</p>
@@ -161,12 +160,12 @@ import {
  *
  * // This allows you to then customize each handler to your needs.
  * // For example, let's set a new subject line to the order confirmation:
- * orderConfirmationHandler
+ * const myOrderConfirmationHandler = orderConfirmationHandler
  *   .setSubject(`We received your order!`);
  *
  * // Another example: loading additional data and setting new
  * // template variables.
- * passwordResetHandler
+ * const myPasswordResetHandler = passwordResetHandler
  *   .loadData(async ({ event, injector }) => {
  *     const customerService = injector.get(CustomerService);
  *     const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
@@ -181,9 +180,9 @@ import {
  * // individually
  * EmailPlugin.init({
  *   handlers: [
- *     orderConfirmationHandler,
+ *     myOrderConfirmationHandler,
+ *     myPasswordResetHandler,
  *     emailVerificationHandler,
- *     passwordResetHandler,
  *     emailAddressChangeHandler,
  *   ],
  *   // ...
@@ -211,10 +210,10 @@ import {
  *           return injector.get(MyTransportService).getSettings(ctx);
  *         } else {
  *           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 fs from 'fs/promises';
 import path from 'path';
+
 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 {
-
-    constructor(private templatePath: string) { }
+    constructor(private templatePath: string) {}
 
     async loadTemplate(
         _injector: Injector,
@@ -22,11 +27,13 @@ export class FileBasedTemplateLoader implements TemplateLoader {
     async loadPartials(): Promise<Partial[]> {
         const partialsPath = path.join(this.templatePath, 'partials');
         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
- * 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.
  *
  * @example
@@ -386,16 +386,19 @@ export interface Partial {
  * ```
  *
  * @docsCategory core plugins/EmailPlugin
- * @docsPage Custom Template Loader
+ * @docsPage TemplateLoader
+ * @docsWeight 0
  */
 export interface TemplateLoader {
     /**
+     * @description
      * Load template and return it's content as a string
      */
     loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
     /**
+     * @description
      * 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[]>;
 }