feat: Add optional notification email when invite is accepted (#3718)

* feat: Add optional notification email when invite is accepted

* Refactor to use beforeSend
This commit is contained in:
Tom Moor
2022-07-02 15:40:40 +03:00
committed by GitHub
parent ee22a127f6
commit 863f22750f
14 changed files with 169 additions and 66 deletions

View File

@@ -1,4 +1,5 @@
import mailer from "@server/emails/mailer";
import Logger from "@server/logging/Logger";
import Metrics from "@server/logging/metrics";
import { taskQueue } from "@server/queues";
import { TaskPriority } from "@server/queues/tasks/BaseTask";
@@ -54,11 +55,19 @@ export default abstract class BaseEmail<T extends EmailProps, S = any> {
* @returns A promise that resolves once the email has been successfully sent.
*/
public async send() {
const bsResponse = this.beforeSend
? await this.beforeSend(this.props)
: ({} as S);
const data = { ...this.props, ...bsResponse };
const templateName = this.constructor.name;
const bsResponse = await this.beforeSend?.(this.props);
if (bsResponse === false) {
Logger.info(
"email",
`Email ${templateName} not sent due to beforeSend hook`,
this.props
);
return;
}
const data = { ...this.props, ...(bsResponse ?? ({} as S)) };
try {
await mailer.sendMail({
@@ -116,10 +125,11 @@ export default abstract class BaseEmail<T extends EmailProps, S = any> {
/**
* beforeSend hook allows async loading additional data that was not passed
* through the serialized worker props.
* through the serialized worker props. If false is returned then the email
* send is aborted.
*
* @param props Props in email constructor
* @returns A promise resolving to additional data
*/
protected beforeSend?(props: T): Promise<S>;
protected beforeSend?(props: T): Promise<S | false>;
}

View File

@@ -1,4 +1,3 @@
import invariant from "invariant";
import * as React from "react";
import env from "@server/env";
import { Collection } from "@server/models";
@@ -37,7 +36,10 @@ export default class CollectionNotificationEmail extends BaseEmail<
const collection = await Collection.scope("withUser").findByPk(
collectionId
);
invariant(collection, "Collection not found");
if (!collection) {
return false;
}
return { collection };
}

View File

@@ -1,4 +1,3 @@
import invariant from "invariant";
import * as React from "react";
import { Document } from "@server/models";
import BaseEmail from "./BaseEmail";
@@ -36,7 +35,10 @@ export default class DocumentNotificationEmail extends BaseEmail<
> {
protected async beforeSend({ documentId }: InputProps) {
const document = await Document.unscoped().findByPk(documentId);
invariant(document, "Document not found");
if (!document) {
return false;
}
return { document };
}

View File

@@ -0,0 +1,82 @@
import * as React from "react";
import { NotificationSetting } from "@server/models";
import BaseEmail from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import EmailTemplate from "./components/EmailLayout";
import EmptySpace from "./components/EmptySpace";
import Footer from "./components/Footer";
import Header from "./components/Header";
import Heading from "./components/Heading";
type Props = {
to: string;
inviterId: string;
invitedName: string;
teamUrl: string;
};
type BeforeSendProps = {
unsubscribeUrl: string;
};
/**
* Email sent to a user when someone they invited successfully signs up.
*/
export default class InviteAcceptedEmail extends BaseEmail<Props> {
protected async beforeSend({ inviterId }: Props) {
const notificationSetting = await NotificationSetting.findOne({
where: {
userId: inviterId,
event: "emails.invite_accepted",
},
});
if (!notificationSetting) {
return false;
}
return { unsubscribeUrl: notificationSetting.unsubscribeUrl };
}
protected subject({ invitedName }: Props) {
return `${invitedName} has joined your Outline team`;
}
protected preview({ invitedName }: Props) {
return `Great news, ${invitedName}, accepted your invitation`;
}
protected renderAsText({ invitedName, teamUrl }: Props): string {
return `
Great news, ${invitedName} just accepted your invitation and has created an account. You can now start collaborating on documents.
Open Outline: ${teamUrl}
`;
}
protected render({
invitedName,
teamUrl,
unsubscribeUrl,
}: Props & BeforeSendProps) {
return (
<EmailTemplate>
<Header />
<Body>
<Heading>{invitedName} has joined your team</Heading>
<p>
Great news, {invitedName} just accepted your invitation and has
created an account. You can now start collaborating on documents.
</p>
<EmptySpace height={10} />
<p>
<Button href={teamUrl}>Open Outline</Button>
</p>
</Body>
<Footer unsubscribeUrl={unsubscribeUrl} />
</EmailTemplate>
);
}
}

View File

@@ -32,7 +32,7 @@ Welcome to Outline!
Outline is a place for your team to build and share knowledge.
To get started, head to your dashboard and try creating a collection to help document your workflow, create playbooks or help with team onboarding.
To get started, head to the home screen and try creating a collection to help document your processes, create playbooks, or plan your teams work.
You can also import existing Markdown documents by dragging and dropping them to your collections.
@@ -49,9 +49,9 @@ ${teamUrl}/home
<Heading>Welcome to Outline!</Heading>
<p>Outline is a place for your team to build and share knowledge.</p>
<p>
To get started, head to your dashboard and try creating a collection
to help document your workflow, create playbooks or help with team
onboarding.
To get started, head to the home screen and try creating a
collection to help document your processes, create playbooks, or
plan your teams work.
</p>
<p>
You can also import existing Markdown documents by dragging and
@@ -59,7 +59,7 @@ ${teamUrl}/home
</p>
<EmptySpace height={10} />
<p>
<Button href={`${teamUrl}/home`}>View my dashboard</Button>
<Button href={`${teamUrl}/home`}>Open Outline</Button>
</p>
</Body>