payload-plugin-newsletter 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -529,6 +529,103 @@ import {
529
529
  InlineToolbarFeature,
530
530
  lexicalEditor
531
531
  } from "@payloadcms/richtext-lexical";
532
+
533
+ // src/utils/blockValidation.ts
534
+ var EMAIL_INCOMPATIBLE_TYPES = [
535
+ "chart",
536
+ "dataTable",
537
+ "interactive",
538
+ "streamable",
539
+ "video",
540
+ "iframe",
541
+ "form",
542
+ "carousel",
543
+ "tabs",
544
+ "accordion",
545
+ "map"
546
+ ];
547
+ var validateEmailBlocks = (blocks) => {
548
+ blocks.forEach((block) => {
549
+ if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
550
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
551
+ }
552
+ const hasComplexFields = block.fields?.some((field) => {
553
+ const complexTypes = ["code", "json", "richText", "blocks", "array"];
554
+ return complexTypes.includes(field.type);
555
+ });
556
+ if (hasComplexFields) {
557
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
558
+ }
559
+ });
560
+ };
561
+ var createEmailSafeBlocks = (customBlocks = []) => {
562
+ validateEmailBlocks(customBlocks);
563
+ const baseBlocks = [
564
+ {
565
+ slug: "button",
566
+ fields: [
567
+ {
568
+ name: "text",
569
+ type: "text",
570
+ label: "Button Text",
571
+ required: true
572
+ },
573
+ {
574
+ name: "url",
575
+ type: "text",
576
+ label: "Button URL",
577
+ required: true,
578
+ admin: {
579
+ description: "Enter the full URL (including https://)"
580
+ }
581
+ },
582
+ {
583
+ name: "style",
584
+ type: "select",
585
+ label: "Button Style",
586
+ defaultValue: "primary",
587
+ options: [
588
+ { label: "Primary", value: "primary" },
589
+ { label: "Secondary", value: "secondary" },
590
+ { label: "Outline", value: "outline" }
591
+ ]
592
+ }
593
+ ],
594
+ interfaceName: "EmailButton",
595
+ labels: {
596
+ singular: "Button",
597
+ plural: "Buttons"
598
+ }
599
+ },
600
+ {
601
+ slug: "divider",
602
+ fields: [
603
+ {
604
+ name: "style",
605
+ type: "select",
606
+ label: "Divider Style",
607
+ defaultValue: "solid",
608
+ options: [
609
+ { label: "Solid", value: "solid" },
610
+ { label: "Dashed", value: "dashed" },
611
+ { label: "Dotted", value: "dotted" }
612
+ ]
613
+ }
614
+ ],
615
+ interfaceName: "EmailDivider",
616
+ labels: {
617
+ singular: "Divider",
618
+ plural: "Dividers"
619
+ }
620
+ }
621
+ ];
622
+ return [
623
+ ...baseBlocks,
624
+ ...customBlocks
625
+ ];
626
+ };
627
+
628
+ // src/fields/emailContent.ts
532
629
  var createEmailSafeFeatures = (additionalBlocks) => {
533
630
  const baseBlocks = [
534
631
  {
@@ -666,16 +763,89 @@ var createEmailSafeFeatures = (additionalBlocks) => {
666
763
  })
667
764
  ];
668
765
  };
766
+ var createEmailLexicalEditor = (customBlocks = []) => {
767
+ const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
768
+ return lexicalEditor({
769
+ features: [
770
+ // Toolbars
771
+ FixedToolbarFeature(),
772
+ InlineToolbarFeature(),
773
+ // Basic text formatting
774
+ BoldFeature(),
775
+ ItalicFeature(),
776
+ UnderlineFeature(),
777
+ StrikethroughFeature(),
778
+ // Links with enhanced configuration
779
+ LinkFeature({
780
+ fields: [
781
+ {
782
+ name: "url",
783
+ type: "text",
784
+ required: true,
785
+ admin: {
786
+ description: "Enter the full URL (including https://)"
787
+ }
788
+ },
789
+ {
790
+ name: "newTab",
791
+ type: "checkbox",
792
+ label: "Open in new tab",
793
+ defaultValue: false
794
+ }
795
+ ]
796
+ }),
797
+ // Lists
798
+ OrderedListFeature(),
799
+ UnorderedListFeature(),
800
+ // Headings - limited to h1, h2, h3 for email compatibility
801
+ HeadingFeature({
802
+ enabledHeadingSizes: ["h1", "h2", "h3"]
803
+ }),
804
+ // Basic paragraph and alignment
805
+ ParagraphFeature(),
806
+ AlignFeature(),
807
+ // Blockquotes
808
+ BlockquoteFeature(),
809
+ // Upload feature for images
810
+ UploadFeature({
811
+ collections: {
812
+ media: {
813
+ fields: [
814
+ {
815
+ name: "caption",
816
+ type: "text",
817
+ admin: {
818
+ description: "Optional caption for the image"
819
+ }
820
+ },
821
+ {
822
+ name: "altText",
823
+ type: "text",
824
+ label: "Alt Text",
825
+ required: true,
826
+ admin: {
827
+ description: "Alternative text for accessibility and when image cannot be displayed"
828
+ }
829
+ }
830
+ ]
831
+ }
832
+ }
833
+ }),
834
+ // Email-safe blocks (processed server-side)
835
+ BlocksFeature({
836
+ blocks: emailSafeBlocks
837
+ })
838
+ ]
839
+ });
840
+ };
669
841
  var emailSafeFeatures = createEmailSafeFeatures();
670
842
  var createEmailContentField = (overrides) => {
671
- const features = createEmailSafeFeatures(overrides?.additionalBlocks);
843
+ const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
672
844
  return {
673
845
  name: "content",
674
846
  type: "richText",
675
847
  required: true,
676
- editor: lexicalEditor({
677
- features
678
- }),
848
+ editor,
679
849
  admin: {
680
850
  description: "Email content with limited formatting for compatibility",
681
851
  ...overrides?.admin
@@ -985,6 +1155,12 @@ var createBroadcastsCollection = (pluginConfig) => {
985
1155
  const customizations = pluginConfig.customizations?.broadcasts;
986
1156
  return {
987
1157
  slug: "broadcasts",
1158
+ versions: {
1159
+ drafts: {
1160
+ autosave: true,
1161
+ schedulePublish: true
1162
+ }
1163
+ },
988
1164
  labels: {
989
1165
  singular: "Broadcast",
990
1166
  plural: "Broadcasts"
@@ -992,7 +1168,7 @@ var createBroadcastsCollection = (pluginConfig) => {
992
1168
  admin: {
993
1169
  useAsTitle: "subject",
994
1170
  description: "Individual email campaigns sent to subscribers",
995
- defaultColumns: ["subject", "status", "sentAt", "recipientCount", "actions"]
1171
+ defaultColumns: ["subject", "_status", "status", "sentAt", "recipientCount"]
996
1172
  },
997
1173
  fields: [
998
1174
  {
@@ -1027,13 +1203,15 @@ var createBroadcastsCollection = (pluginConfig) => {
1027
1203
  }
1028
1204
  },
1029
1205
  // Apply content field customization if provided
1030
- customizations?.fieldOverrides?.content ? customizations.fieldOverrides.content(createEmailContentField({
1031
- admin: { description: "Email content" },
1032
- additionalBlocks: customizations.customBlocks
1033
- })) : createEmailContentField({
1034
- admin: { description: "Email content" },
1035
- additionalBlocks: customizations?.customBlocks
1036
- })
1206
+ // Process blocks server-side to avoid client serialization issues
1207
+ (() => {
1208
+ const emailEditor = createEmailLexicalEditor(customizations?.customBlocks);
1209
+ const baseField = createEmailContentField({
1210
+ admin: { description: "Email content" },
1211
+ editor: emailEditor
1212
+ });
1213
+ return customizations?.fieldOverrides?.content ? customizations.fieldOverrides.content(baseField) : baseField;
1214
+ })()
1037
1215
  ]
1038
1216
  },
1039
1217
  {
@@ -1203,18 +1381,6 @@ var createBroadcastsCollection = (pluginConfig) => {
1203
1381
  condition: () => false
1204
1382
  // Hidden by default
1205
1383
  }
1206
- },
1207
- // UI Field for custom actions in list view
1208
- {
1209
- name: "actions",
1210
- type: "ui",
1211
- admin: {
1212
- components: {
1213
- Cell: "payload-plugin-newsletter/components#ActionsCell",
1214
- Field: "payload-plugin-newsletter/components#EmptyField"
1215
- },
1216
- disableListColumn: false
1217
- }
1218
1384
  }
1219
1385
  ],
1220
1386
  hooks: {
@@ -1328,6 +1494,48 @@ var createBroadcastsCollection = (pluginConfig) => {
1328
1494
  req.payload.logger.error("Failed to delete broadcast from provider:", error);
1329
1495
  }
1330
1496
  return doc;
1497
+ },
1498
+ // Hook to send when published
1499
+ async ({ doc, req }) => {
1500
+ if (doc._status === "published" && doc.providerId) {
1501
+ if (doc.status === "sent" || doc.status === "sending") {
1502
+ return doc;
1503
+ }
1504
+ try {
1505
+ const broadcastConfig = pluginConfig.providers?.broadcast;
1506
+ const resendConfig = pluginConfig.providers?.resend;
1507
+ if (!broadcastConfig && !resendConfig) {
1508
+ req.payload.logger.error("No provider configured for sending");
1509
+ return doc;
1510
+ }
1511
+ if (broadcastConfig) {
1512
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1513
+ const provider = new BroadcastApiProvider2(broadcastConfig);
1514
+ await provider.send(doc.providerId);
1515
+ }
1516
+ await req.payload.update({
1517
+ collection: "broadcasts",
1518
+ id: doc.id,
1519
+ data: {
1520
+ status: "sending" /* SENDING */,
1521
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
1522
+ },
1523
+ req
1524
+ });
1525
+ req.payload.logger.info(`Broadcast ${doc.id} sent successfully`);
1526
+ } catch (error) {
1527
+ req.payload.logger.error(`Failed to send broadcast ${doc.id}:`, error);
1528
+ await req.payload.update({
1529
+ collection: "broadcasts",
1530
+ id: doc.id,
1531
+ data: {
1532
+ status: "failed" /* FAILED */
1533
+ },
1534
+ req
1535
+ });
1536
+ }
1537
+ }
1538
+ return doc;
1331
1539
  }
1332
1540
  ]
1333
1541
  }