payload-plugin-newsletter
Version:
Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration
536 lines (532 loc) • 14 kB
JavaScript
// src/fields/emailContent.ts
import {
BoldFeature,
ItalicFeature,
UnderlineFeature,
StrikethroughFeature,
LinkFeature,
OrderedListFeature,
UnorderedListFeature,
HeadingFeature,
ParagraphFeature,
AlignFeature,
BlockquoteFeature,
BlocksFeature,
UploadFeature,
FixedToolbarFeature,
InlineToolbarFeature,
lexicalEditor
} from "@payloadcms/richtext-lexical";
// src/utils/blockValidation.ts
var EMAIL_INCOMPATIBLE_TYPES = [
"chart",
"dataTable",
"interactive",
"streamable",
"video",
"iframe",
"form",
"carousel",
"tabs",
"accordion",
"map"
];
var validateEmailBlocks = (blocks) => {
blocks.forEach((block) => {
if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
}
const hasComplexFields = block.fields?.some((field) => {
const complexTypes = ["code", "json", "richText", "blocks", "array"];
return complexTypes.includes(field.type);
});
if (hasComplexFields) {
console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
}
});
};
var createEmailSafeBlocks = (customBlocks = []) => {
validateEmailBlocks(customBlocks);
const baseBlocks = [
{
slug: "button",
fields: [
{
name: "text",
type: "text",
label: "Button Text",
required: true
},
{
name: "url",
type: "text",
label: "Button URL",
required: true,
admin: {
description: "Enter the full URL (including https://)"
}
},
{
name: "style",
type: "select",
label: "Button Style",
defaultValue: "primary",
options: [
{ label: "Primary", value: "primary" },
{ label: "Secondary", value: "secondary" },
{ label: "Outline", value: "outline" }
]
}
],
interfaceName: "EmailButton",
labels: {
singular: "Button",
plural: "Buttons"
}
},
{
slug: "divider",
fields: [
{
name: "style",
type: "select",
label: "Divider Style",
defaultValue: "solid",
options: [
{ label: "Solid", value: "solid" },
{ label: "Dashed", value: "dashed" },
{ label: "Dotted", value: "dotted" }
]
}
],
interfaceName: "EmailDivider",
labels: {
singular: "Divider",
plural: "Dividers"
}
}
];
return [
...baseBlocks,
...customBlocks
];
};
// src/fields/emailContent.ts
var createEmailSafeFeatures = (additionalBlocks) => {
const baseBlocks = [
{
slug: "button",
fields: [
{
name: "text",
type: "text",
label: "Button Text",
required: true
},
{
name: "url",
type: "text",
label: "Button URL",
required: true,
admin: {
description: "Enter the full URL (including https://)"
}
},
{
name: "style",
type: "select",
label: "Button Style",
defaultValue: "primary",
options: [
{ label: "Primary", value: "primary" },
{ label: "Secondary", value: "secondary" },
{ label: "Outline", value: "outline" }
]
}
],
interfaceName: "EmailButton",
labels: {
singular: "Button",
plural: "Buttons"
}
},
{
slug: "divider",
fields: [
{
name: "style",
type: "select",
label: "Divider Style",
defaultValue: "solid",
options: [
{ label: "Solid", value: "solid" },
{ label: "Dashed", value: "dashed" },
{ label: "Dotted", value: "dotted" }
]
}
],
interfaceName: "EmailDivider",
labels: {
singular: "Divider",
plural: "Dividers"
}
}
];
const allBlocks = [
...baseBlocks,
...additionalBlocks || []
];
return [
// Toolbars
FixedToolbarFeature(),
// Fixed toolbar at the top
InlineToolbarFeature(),
// Floating toolbar when text is selected
// Basic text formatting
BoldFeature(),
ItalicFeature(),
UnderlineFeature(),
StrikethroughFeature(),
// Links with enhanced configuration
LinkFeature({
fields: [
{
name: "url",
type: "text",
required: true,
admin: {
description: "Enter the full URL (including https://)"
}
},
{
name: "newTab",
type: "checkbox",
label: "Open in new tab",
defaultValue: false
}
]
}),
// Lists
OrderedListFeature(),
UnorderedListFeature(),
// Headings - limited to h1, h2, h3 for email compatibility
HeadingFeature({
enabledHeadingSizes: ["h1", "h2", "h3"]
}),
// Basic paragraph and alignment
ParagraphFeature(),
AlignFeature(),
// Blockquotes
BlockquoteFeature(),
// Upload feature for images
UploadFeature({
collections: {
media: {
fields: [
{
name: "caption",
type: "text",
admin: {
description: "Optional caption for the image"
}
},
{
name: "altText",
type: "text",
label: "Alt Text",
required: true,
admin: {
description: "Alternative text for accessibility and when image cannot be displayed"
}
}
]
}
}
}),
// Custom blocks for email-specific content
BlocksFeature({
blocks: allBlocks
})
];
};
var createEmailLexicalEditor = (customBlocks = []) => {
const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
return lexicalEditor({
features: [
// Toolbars
FixedToolbarFeature(),
InlineToolbarFeature(),
// Basic text formatting
BoldFeature(),
ItalicFeature(),
UnderlineFeature(),
StrikethroughFeature(),
// Links with enhanced configuration
LinkFeature({
fields: [
{
name: "url",
type: "text",
required: true,
admin: {
description: "Enter the full URL (including https://)"
}
},
{
name: "newTab",
type: "checkbox",
label: "Open in new tab",
defaultValue: false
}
]
}),
// Lists
OrderedListFeature(),
UnorderedListFeature(),
// Headings - limited to h1, h2, h3 for email compatibility
HeadingFeature({
enabledHeadingSizes: ["h1", "h2", "h3"]
}),
// Basic paragraph and alignment
ParagraphFeature(),
AlignFeature(),
// Blockquotes
BlockquoteFeature(),
// Upload feature for images
UploadFeature({
collections: {
media: {
fields: [
{
name: "caption",
type: "text",
admin: {
description: "Optional caption for the image"
}
},
{
name: "altText",
type: "text",
label: "Alt Text",
required: true,
admin: {
description: "Alternative text for accessibility and when image cannot be displayed"
}
}
]
}
}
}),
// Email-safe blocks (processed server-side)
BlocksFeature({
blocks: emailSafeBlocks
})
]
});
};
var emailSafeFeatures = createEmailSafeFeatures();
var createEmailContentField = (overrides) => {
const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
return {
name: "content",
type: "richText",
required: true,
editor,
admin: {
description: "Email content with limited formatting for compatibility",
...overrides?.admin
},
...overrides
};
};
// src/fields/broadcastInlinePreview.ts
var createBroadcastInlinePreviewField = () => {
return {
name: "broadcastInlinePreview",
type: "ui",
admin: {
components: {
Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
}
}
};
};
// src/fields/newsletterScheduling.ts
function createNewsletterSchedulingFields(config) {
const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
const fields = [
{
name: groupName,
type: "group",
label: "Newsletter Scheduling",
admin: {
condition: (data, { user }) => user?.collection === "users"
// Only show for admin users
},
fields: [
{
name: "scheduled",
type: "checkbox",
label: "Schedule for Newsletter",
defaultValue: false,
admin: {
description: "Schedule this content to be sent as a newsletter"
}
},
{
name: "scheduledDate",
type: "date",
label: "Send Date",
required: true,
admin: {
date: {
pickerAppearance: "dayAndTime"
},
condition: (data) => data?.[groupName]?.scheduled,
description: "When to send this newsletter"
}
},
{
name: "sentDate",
type: "date",
label: "Sent Date",
admin: {
readOnly: true,
condition: (data) => data?.[groupName]?.sendStatus === "sent",
description: "When this newsletter was sent"
}
},
{
name: "sendStatus",
type: "select",
label: "Status",
options: [
{ label: "Draft", value: "draft" },
{ label: "Scheduled", value: "scheduled" },
{ label: "Sending", value: "sending" },
{ label: "Sent", value: "sent" },
{ label: "Failed", value: "failed" }
],
defaultValue: "draft",
admin: {
readOnly: true,
description: "Current send status"
}
},
{
name: "emailSubject",
type: "text",
label: "Email Subject",
required: true,
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Subject line for the newsletter email"
}
},
{
name: "preheader",
type: "text",
label: "Email Preheader",
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Preview text that appears after the subject line"
}
},
{
name: "segments",
type: "select",
label: "Target Segments",
hasMany: true,
options: [
{ label: "All Subscribers", value: "all" },
...config.i18n?.locales?.map((locale) => ({
label: `${locale.toUpperCase()} Subscribers`,
value: locale
})) || []
],
defaultValue: ["all"],
admin: {
condition: (data) => data?.[groupName]?.scheduled,
description: "Which subscriber segments to send to"
}
},
{
name: "testEmails",
type: "array",
label: "Test Email Recipients",
admin: {
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
description: "Send test emails before scheduling"
},
fields: [
{
name: "email",
type: "email",
required: true
}
]
}
]
}
];
if (createMarkdownField) {
fields.push(createMarkdownFieldInternal({
name: `${contentField}Markdown`,
richTextField: contentField,
label: "Email Content (Markdown)",
admin: {
position: "sidebar",
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
description: "Markdown version for email rendering",
readOnly: true
}
}));
}
return fields;
}
function createMarkdownFieldInternal(config) {
return {
name: config.name,
type: "textarea",
label: config.label || "Markdown",
admin: {
...config.admin,
description: config.admin?.description || "Auto-generated from rich text content"
},
hooks: {
afterRead: [
async ({ data }) => {
if (data?.[config.richTextField]) {
try {
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
return convertLexicalToMarkdown({
data: data[config.richTextField]
});
} catch {
return "";
}
}
return "";
}
],
beforeChange: [
() => {
return null;
}
]
}
};
}
export {
createBroadcastInlinePreviewField,
createEmailContentField,
createEmailLexicalEditor,
createEmailSafeBlocks,
createEmailSafeFeatures,
createNewsletterSchedulingFields,
emailSafeFeatures,
validateEmailBlocks
};
//# sourceMappingURL=fields.js.map