UNPKG

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
// 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