Skip to content

Commit

Permalink
add subjects to templates (#364)
Browse files Browse the repository at this point in the history
* add subjects to templates

* use jsdom-env, add more tests

* import template
  • Loading branch information
elliothursh authored Nov 28, 2022
1 parent 6bae8ec commit 5635a53
Show file tree
Hide file tree
Showing 17 changed files with 287 additions and 36 deletions.
2 changes: 1 addition & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const jsdomConfig = async (): Promise<any> => {
name: "jest.jsdom",
color: "magenta",
},
testEnvironment: "jest-environment-jsdom",
testEnvironment: "<rootDir>/jsdom.env.ts",
setupFilesAfterEnv: ["<rootDir>/testSetup.ts"],
testMatch: [
"<rootDir>/packages/cli/src/pages/**/__test__/**/*.test.[jt]s?(x)",
Expand Down
21 changes: 21 additions & 0 deletions jsdom.env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { TestEnvironment } from "jest-environment-jsdom";

class CustomTestEnvironment extends TestEnvironment {
async setup() {
await super.setup();
// TextEncoder is required by node-html-parser
if (typeof this.global.TextEncoder === "undefined") {
const { TextEncoder } = require("util");
this.global.TextEncoder = TextEncoder;
}

// setImmediate is required by nodemailer
if (typeof this.global.setImmediate === "undefined") {
const { setImmediate } = require("timers");
this.global.setImmediate = setImmediate;
}
}
}

module.exports = CustomTestEnvironment;
10 changes: 3 additions & 7 deletions packages/cli/src/components/PreviewSender.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { useCallback, useEffect, useState } from "react";

type PreviewSenderProps = {
html?: string;
previewFunction?: string;
previewClass?: string;
previewFunction: string;
previewClass: string;
};

const PreviewSender: React.FC<PreviewSenderProps> = ({
html,
previewFunction,
previewClass,
}) => {
Expand All @@ -34,10 +32,8 @@ const PreviewSender: React.FC<PreviewSenderProps> = ({
setSending(true);
const payload: SendPreviewRequestBody = {
to: email,
html,
previewFunction,
previewClass,
subject: `${previewClass} - ${previewFunction}`,
};

const response = await fetch("/api/previews/send", {
Expand Down Expand Up @@ -76,7 +72,7 @@ const PreviewSender: React.FC<PreviewSenderProps> = ({
setSending(false);
}
},
[html, previewClass, previewFunction, email]
[previewClass, previewFunction, email]
);

const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ type Intercept = {

type SendPreviewRequestBody = {
to: string;
html?: string;
previewFunction?: string;
previewClass?: string;
subject: string;
previewFunction: string;
previewClass: string;
};

type SendPreviewResponseBody = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import {
textBase,
textXl,
} from "./components/theme";
import { Template } from "mailing-core";

const AccountCreated: React.FC<{ name: string }> = ({ name }) => (
type AccountCreatedProps = { name: string };

const AccountCreated: Template<AccountCreatedProps> = ({ name }) => (
<Mjml>
<Head />
<MjmlBody width={600}>
Expand Down Expand Up @@ -76,4 +79,6 @@ const AccountCreated: React.FC<{ name: string }> = ({ name }) => (
</Mjml>
);

AccountCreated.subject = ({ name }) => `Welcome to BookBook, ${name}!`;

export default AccountCreated;
3 changes: 2 additions & 1 deletion packages/cli/src/generator_templates/ts/emails/NewSignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ type NewSignInProps = {
body: ReactElement;
bulletedList: ReactElement;
};
import { Template } from "mailing-core";

const NewSignIn: React.FC<NewSignInProps> = ({
const NewSignIn: Template<NewSignInProps> = ({
name,
headline,
body,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
MjmlSpacer,
MjmlDivider,
} from "mjml-react";
import { Template } from "mailing-core";

type ReservationProps = {
headline: string;
Expand All @@ -27,7 +28,7 @@ type ReservationProps = {
ctaText?: string;
};

const Reservation: React.FC<ReservationProps> = ({
const Reservation: Template<ReservationProps> = ({
headline,
body,
bulletedList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {
MjmlSpacer,
MjmlDivider,
} from "mjml-react";
import { Template } from "mailing-core";

type ResetPasswordProps = {
name: string;
body: ReactElement;
ctaText: string;
};

const ResetPassword: React.FC<ResetPasswordProps> = ({
const ResetPassword: Template<ResetPasswordProps> = ({
name,
body,
ctaText,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`index should return previews 1`] = `
{
"previewText": {
"/previews/AccountCreated/accountCreated": "Welcome to BookBook, Amelita!",
"/previews/NewSignIn/newSignIn": "Security Alert: New Sign-In Hello Amelita, We noticed a new sign-in to your BookBook account on a Mac device. If this was you, you don’t nee",
"/previews/Reservation/reservationChanged": "Reservation Changed • Salazar in Silver Lake • Sunday, Aug 22 at 1:30pm • Party of 4, patio You’re all set! Your reservation at Salazar has ",
"/previews/Reservation/reservationConfirmed": "Reservation Confirmed • Salazar in Silver Lake • Saturday, Aug 22 at 1:30pm • Party of 4, patio Thanks for booking your reservation at Salaz",
"/previews/Reservation/reservationWithError": "Reservation Canceled • Salazar in Silver Lake • Sunday, Aug 22 at 1:30pm • Party of 4, patio If this was a mistake or if you changed your mi",
"/previews/ResetPassword/resetPassword": "Hello Amelita, We’ve received your request to change your password. Use the link below to set up a new password for your account. This link ",
},
"previews": [
[
"AccountCreated",
[
"accountCreated",
],
],
[
"NewSignIn",
[
"newSignIn",
],
],
[
"Reservation",
[
"reservationWithError",
"reservationConfirmed",
"reservationChanged",
],
],
[
"ResetPassword",
[
"resetPassword",
],
],
],
}
`;
37 changes: 37 additions & 0 deletions packages/cli/src/pages/api/previews/__test__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import index from "..";

describe("index", () => {
it("should return previews", async () => {
const { req, res } = createMocks({
method: "GET",
});

await index(
req as unknown as NextApiRequest,
res as unknown as NextApiResponse
);

expect(res.statusCode).toBe(200);
const json = res._getJSONData();
expect(json).toMatchSnapshot();
});

it("should return template subject if it exists", async () => {
const { req, res } = createMocks({
method: "GET",
});

await index(
req as unknown as NextApiRequest,
res as unknown as NextApiResponse
);

expect(res.statusCode).toBe(200);
const json = res._getJSONData();
expect(json["previewText"]["/previews/AccountCreated/accountCreated"]).toBe(
"Welcome to BookBook, Amelita!"
);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/pages/api/previews/__test__/send.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jest.mock("../../../../moduleManifest", () => ({

jest.mock("../../../../util/moduleManifestUtil", () => ({
getPreviewComponent: jest.fn(),
getTemplateModule: jest.fn(),
}));

describe("send", () => {
Expand Down
31 changes: 25 additions & 6 deletions packages/cli/src/pages/api/previews/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { parse } from "node-html-parser";
import { render } from "../../../util/mjml";
import {
getPreviewComponent,
getTemplateModule,
previewTree,
} from "../../../util/moduleManifestUtil";
import { error } from "../../../util/log";
Expand All @@ -24,16 +25,34 @@ async function getPreviewFunction(
try {
const component = await getPreviewComponent(previewClass, name);
if (!component) throw new Error(`${previewClass}#${name} not found`);
const { html } = render(component);
// slice out the body to minimize funky head parsing
const body = /<body[^>]*>((.|[\n\r])*)<\/body>/im.exec(html);
if (body && body[1]) {
const root = parse(body[1]);
text = root.text.replace(/\s+/g, " ").trim().substring(0, MAX_TEXT_CHARS);

// Try to build preview text from subject
const template = getTemplateModule(previewClass);
if (template) {
if (typeof template.subject === "function") {
text = template.subject(component.props);
} else if (typeof template.subject === "string") {
text = template.subject;
}
}

// If that didn't work, try to build preview text from the rendered preview
if (text.length === 0) {
const { html } = render(component);
// slice out the body to minimize funky head parsing
const body = /<body[^>]*>((.|[\n\r])*)<\/body>/im.exec(html);
if (body && body[1]) {
const root = parse(body[1]);
text = root.text
.replace(/\s+/g, " ")
.trim()
.substring(0, MAX_TEXT_CHARS);
}
}
} catch (e) {
error(`error rendering text preview for ${previewClass}#${name}`, e);
}

return [`/previews/${previewClass}/${name}`, text];
}

Expand Down
22 changes: 16 additions & 6 deletions packages/cli/src/pages/api/previews/send.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { error } from "../../../util/log";
import { sendMail } from "../../../moduleManifest";
import { getPreviewComponent } from "../../../util/moduleManifestUtil";
import {
getPreviewComponent,
getTemplateModule,
} from "../../../util/moduleManifestUtil";
import { jsonStringifyError } from "../../../util/jsonStringifyError";

export default async function send(
Expand All @@ -14,18 +17,25 @@ export default async function send(
}

const body: SendPreviewRequestBody = req.body;
const { to, subject, previewClass, previewFunction } = body;
let component;
const { to, previewClass, previewFunction } = body;
let subject = `${previewClass} - ${previewFunction}`;

if (previewClass && previewFunction) {
component = await getPreviewComponent(previewClass, previewFunction);
}
const component = await getPreviewComponent(previewClass, previewFunction);
if (!component) {
error("no component found");
res.status(400).json({ error: "no html provided, no component found" });
return;
}

const template = getTemplateModule(previewClass);
if (template) {
if (typeof template.subject === "function") {
subject = template.subject(component.props);
} else if (typeof template.subject === "string") {
subject = template.subject;
}
}

try {
await sendMail({
component,
Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/pages/api/sendMail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ export default async function handler(
return res.status(403).json({ error: "to, cc, or bcc must be specified" });
}

// validate subject
if (typeof mailOptions.subject !== "string") {
return res.status(403).json({ error: "subject must be specified" });
}

if (!html) {
// validate template name
if (typeof templateName !== "string") {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/util/moduleManifestUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JSXElementConstructor, ReactElement } from "react";
import { Template } from "mailing-core";
import moduleManifest, { config } from "../moduleManifest";

export function previewTree(): [string, string[]][] {
Expand All @@ -14,6 +15,14 @@ export function previewTree(): [string, string[]][] {
});
}

export function getTemplateModule(name?: string) {
if (!name) return null;

return moduleManifest.templates[
name as keyof typeof moduleManifest.templates
] as unknown as Template<any>;
}

export function getPreviewModule(name: string) {
return moduleManifest.previews[name as keyof typeof moduleManifest.previews];
}
Expand All @@ -29,6 +38,7 @@ export async function getPreviewComponent(
}
| undefined = previews[name as keyof typeof previews] as any;
const previewComponent = previewModule?.[functionName];

return typeof previewComponent === "function"
? await previewComponent()
: undefined;
Expand Down
Loading

2 comments on commit 5635a53

@vercel
Copy link

@vercel vercel bot commented on 5635a53 Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web-emails – ./packages/web

web-emails-git-main-sofn.vercel.app
emails.mailing.run
mailing-web.vercel.app
web-emails-sofn.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 5635a53 Nov 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.