payload-plugin-newsletter 0.16.9 → 0.17.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.
@@ -950,62 +950,73 @@ async function convertToEmailSafeHtml(editorState, options) {
950
950
  if (!editorState) {
951
951
  return "";
952
952
  }
953
- const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
953
+ const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl, options?.customBlockConverter);
954
954
  const sanitizedHtml = DOMPurify.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
955
955
  if (options?.wrapInTemplate) {
956
956
  return wrapInEmailTemplate(sanitizedHtml, options.preheader);
957
957
  }
958
958
  return sanitizedHtml;
959
959
  }
960
- async function lexicalToEmailHtml(editorState, mediaUrl) {
960
+ async function lexicalToEmailHtml(editorState, mediaUrl, customBlockConverter) {
961
961
  const { root } = editorState;
962
962
  if (!root || !root.children) {
963
963
  return "";
964
964
  }
965
- const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
966
- return html;
965
+ const htmlParts = await Promise.all(
966
+ root.children.map((node) => convertNode(node, mediaUrl, customBlockConverter))
967
+ );
968
+ return htmlParts.join("");
967
969
  }
968
- function convertNode(node, mediaUrl) {
970
+ async function convertNode(node, mediaUrl, customBlockConverter) {
969
971
  switch (node.type) {
970
972
  case "paragraph":
971
- return convertParagraph(node, mediaUrl);
973
+ return convertParagraph(node, mediaUrl, customBlockConverter);
972
974
  case "heading":
973
- return convertHeading(node, mediaUrl);
975
+ return convertHeading(node, mediaUrl, customBlockConverter);
974
976
  case "list":
975
- return convertList(node, mediaUrl);
977
+ return convertList(node, mediaUrl, customBlockConverter);
976
978
  case "listitem":
977
- return convertListItem(node, mediaUrl);
979
+ return convertListItem(node, mediaUrl, customBlockConverter);
978
980
  case "blockquote":
979
- return convertBlockquote(node, mediaUrl);
981
+ return convertBlockquote(node, mediaUrl, customBlockConverter);
980
982
  case "text":
981
983
  return convertText(node);
982
984
  case "link":
983
- return convertLink(node, mediaUrl);
985
+ return convertLink(node, mediaUrl, customBlockConverter);
984
986
  case "linebreak":
985
987
  return "<br>";
986
988
  case "upload":
987
989
  return convertUpload(node, mediaUrl);
988
990
  case "block":
989
- return convertBlock(node, mediaUrl);
991
+ return await convertBlock(node, mediaUrl, customBlockConverter);
990
992
  default:
991
993
  if (node.children) {
992
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
994
+ const childParts = await Promise.all(
995
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
996
+ );
997
+ return childParts.join("");
993
998
  }
994
999
  return "";
995
1000
  }
996
1001
  }
997
- function convertParagraph(node, mediaUrl) {
1002
+ async function convertParagraph(node, mediaUrl, customBlockConverter) {
998
1003
  const align = getAlignment(node.format);
999
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1004
+ const childParts = await Promise.all(
1005
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1006
+ );
1007
+ const children = childParts.join("");
1000
1008
  if (!children.trim()) {
1001
1009
  return '<p style="margin: 0 0 16px 0; min-height: 1em;">&nbsp;</p>';
1002
1010
  }
1003
1011
  return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
1004
1012
  }
1005
- function convertHeading(node, mediaUrl) {
1013
+ async function convertHeading(node, mediaUrl, customBlockConverter) {
1006
1014
  const tag = node.tag || "h1";
1007
1015
  const align = getAlignment(node.format);
1008
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1016
+ const childParts = await Promise.all(
1017
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1018
+ );
1019
+ const children = childParts.join("");
1009
1020
  const styles2 = {
1010
1021
  h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
1011
1022
  h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
@@ -1014,18 +1025,27 @@ function convertHeading(node, mediaUrl) {
1014
1025
  const style = `${styles2[tag] || styles2.h3} text-align: ${align};`;
1015
1026
  return `<${tag} style="${style}">${children}</${tag}>`;
1016
1027
  }
1017
- function convertList(node, mediaUrl) {
1028
+ async function convertList(node, mediaUrl, customBlockConverter) {
1018
1029
  const tag = node.listType === "number" ? "ol" : "ul";
1019
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1030
+ const childParts = await Promise.all(
1031
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1032
+ );
1033
+ const children = childParts.join("");
1020
1034
  const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
1021
1035
  return `<${tag} style="${style}">${children}</${tag}>`;
1022
1036
  }
1023
- function convertListItem(node, mediaUrl) {
1024
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1037
+ async function convertListItem(node, mediaUrl, customBlockConverter) {
1038
+ const childParts = await Promise.all(
1039
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1040
+ );
1041
+ const children = childParts.join("");
1025
1042
  return `<li style="margin: 0 0 8px 0;">${children}</li>`;
1026
1043
  }
1027
- function convertBlockquote(node, mediaUrl) {
1028
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1044
+ async function convertBlockquote(node, mediaUrl, customBlockConverter) {
1045
+ const childParts = await Promise.all(
1046
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1047
+ );
1048
+ const children = childParts.join("");
1029
1049
  const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
1030
1050
  return `<blockquote style="${style}">${children}</blockquote>`;
1031
1051
  }
@@ -1045,8 +1065,11 @@ function convertText(node) {
1045
1065
  }
1046
1066
  return text;
1047
1067
  }
1048
- function convertLink(node, mediaUrl) {
1049
- const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
1068
+ async function convertLink(node, mediaUrl, customBlockConverter) {
1069
+ const childParts = await Promise.all(
1070
+ (node.children || []).map((child) => convertNode(child, mediaUrl, customBlockConverter))
1071
+ );
1072
+ const children = childParts.join("");
1050
1073
  const url = node.fields?.url || "#";
1051
1074
  const newTab = node.fields?.newTab ?? false;
1052
1075
  const targetAttr = newTab ? ' target="_blank"' : "";
@@ -1077,8 +1100,18 @@ function convertUpload(node, mediaUrl) {
1077
1100
  }
1078
1101
  return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
1079
1102
  }
1080
- function convertBlock(node, mediaUrl) {
1081
- const blockType = node.fields?.blockName;
1103
+ async function convertBlock(node, mediaUrl, customBlockConverter) {
1104
+ const blockType = node.fields?.blockName || node.blockName;
1105
+ if (customBlockConverter) {
1106
+ try {
1107
+ const customHtml = await customBlockConverter(node, mediaUrl);
1108
+ if (customHtml) {
1109
+ return customHtml;
1110
+ }
1111
+ } catch (error) {
1112
+ console.error(`Custom block converter error for ${blockType}:`, error);
1113
+ }
1114
+ }
1082
1115
  switch (blockType) {
1083
1116
  case "button":
1084
1117
  return convertButtonBlock(node.fields);
@@ -1086,7 +1119,10 @@ function convertBlock(node, mediaUrl) {
1086
1119
  return convertDividerBlock(node.fields);
1087
1120
  default:
1088
1121
  if (node.children) {
1089
- return node.children.map((child) => convertNode(child, mediaUrl)).join("");
1122
+ const childParts = await Promise.all(
1123
+ node.children.map((child) => convertNode(child, mediaUrl, customBlockConverter))
1124
+ );
1125
+ return childParts.join("");
1090
1126
  }
1091
1127
  return "";
1092
1128
  }
@@ -1460,7 +1496,9 @@ var createBroadcastsCollection = (pluginConfig) => {
1460
1496
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1461
1497
  const provider = new BroadcastApiProvider2(providerConfig);
1462
1498
  req.payload.logger.info("Converting content to HTML...");
1463
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1499
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
1500
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
1501
+ });
1464
1502
  if (!htmlContent || htmlContent.trim() === "") {
1465
1503
  req.payload.logger.info("Skipping provider sync - content is empty after conversion");
1466
1504
  return doc;
@@ -1556,77 +1594,49 @@ var createBroadcastsCollection = (pluginConfig) => {
1556
1594
  req.payload.logger.info("Still missing required fields for provider sync");
1557
1595
  return doc;
1558
1596
  }
1559
- try {
1560
- req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
1561
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1562
- if (!htmlContent || htmlContent.trim() === "") {
1563
- req.payload.logger.info("Skipping provider sync - content is empty after conversion");
1564
- return doc;
1565
- }
1566
- const createData = {
1567
- name: doc.subject,
1568
- subject: doc.subject,
1569
- preheader: doc.contentSection?.preheader,
1570
- content: htmlContent,
1571
- trackOpens: doc.settings?.trackOpens,
1572
- trackClicks: doc.settings?.trackClicks,
1573
- replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1574
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1575
- };
1576
- req.payload.logger.info("Creating broadcast with data:", {
1577
- name: createData.name,
1578
- subject: createData.subject,
1579
- preheader: createData.preheader || "NONE",
1580
- contentLength: htmlContent ? htmlContent.length : 0,
1581
- contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
1582
- apiUrl: providerConfig.apiUrl,
1583
- hasToken: !!providerConfig.token
1584
- });
1585
- const providerBroadcast = await provider.create(createData);
1586
- await req.payload.update({
1587
- collection: "broadcasts",
1588
- id: doc.id,
1589
- data: {
1590
- providerId: providerBroadcast.id,
1591
- providerData: providerBroadcast.providerData
1592
- },
1593
- req
1594
- });
1595
- req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
1596
- return {
1597
- ...doc,
1598
- providerId: providerBroadcast.id,
1599
- providerData: providerBroadcast.providerData
1600
- };
1601
- } catch (error) {
1602
- req.payload.logger.error("Raw error from broadcast provider (deferred create):");
1603
- req.payload.logger.error(error);
1604
- if (error instanceof Error) {
1605
- req.payload.logger.error("Error is instance of Error:", {
1606
- message: error.message,
1607
- stack: error.stack,
1608
- name: error.name,
1609
- ...error.details,
1610
- ...error.response,
1611
- ...error.data,
1612
- ...error.status,
1613
- ...error.statusText
1614
- });
1615
- } else if (typeof error === "string") {
1616
- req.payload.logger.error("Error is a string:", error);
1617
- } else if (error && typeof error === "object") {
1618
- req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
1619
- } else {
1620
- req.payload.logger.error("Unknown error type:", typeof error);
1621
- }
1622
- req.payload.logger.error("Failed broadcast document (deferred create):", {
1623
- id: doc.id,
1624
- subject: doc.subject,
1625
- hasContent: !!doc.contentSection?.content,
1626
- contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
1627
- });
1597
+ req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
1598
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content, {
1599
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
1600
+ });
1601
+ if (!htmlContent || htmlContent.trim() === "") {
1602
+ req.payload.logger.info("Skipping provider sync - content is empty after conversion");
1628
1603
  return doc;
1629
1604
  }
1605
+ const createData = {
1606
+ name: doc.subject,
1607
+ subject: doc.subject,
1608
+ preheader: doc.contentSection?.preheader,
1609
+ content: htmlContent,
1610
+ trackOpens: doc.settings?.trackOpens,
1611
+ trackClicks: doc.settings?.trackClicks,
1612
+ replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1613
+ audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1614
+ };
1615
+ req.payload.logger.info("Creating broadcast with data:", {
1616
+ name: createData.name,
1617
+ subject: createData.subject,
1618
+ preheader: createData.preheader || "NONE",
1619
+ contentLength: htmlContent ? htmlContent.length : 0,
1620
+ contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
1621
+ apiUrl: providerConfig.apiUrl,
1622
+ hasToken: !!providerConfig.token
1623
+ });
1624
+ const providerBroadcast = await provider.create(createData);
1625
+ await req.payload.update({
1626
+ collection: "broadcasts",
1627
+ id: doc.id,
1628
+ data: {
1629
+ providerId: providerBroadcast.id,
1630
+ providerData: providerBroadcast.providerData
1631
+ },
1632
+ req
1633
+ });
1634
+ req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
1635
+ return {
1636
+ ...doc,
1637
+ providerId: providerBroadcast.id,
1638
+ providerData: providerBroadcast.providerData
1639
+ };
1630
1640
  }
1631
1641
  if (doc.providerId) {
1632
1642
  const capabilities = provider.getCapabilities();
@@ -1646,7 +1656,9 @@ var createBroadcastsCollection = (pluginConfig) => {
1646
1656
  updates.preheader = doc.contentSection?.preheader;
1647
1657
  }
1648
1658
  if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
1649
- updates.content = await convertToEmailSafeHtml(doc.contentSection?.content);
1659
+ updates.content = await convertToEmailSafeHtml(doc.contentSection?.content, {
1660
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
1661
+ });
1650
1662
  }
1651
1663
  if (doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens) {
1652
1664
  updates.trackOpens = doc.settings.trackOpens;
@@ -1671,16 +1683,32 @@ var createBroadcastsCollection = (pluginConfig) => {
1671
1683
  }
1672
1684
  }
1673
1685
  } catch (error) {
1686
+ req.payload.logger.error("Raw error from broadcast update operation:");
1687
+ req.payload.logger.error(error);
1674
1688
  if (error instanceof Error) {
1675
- req.payload.logger.error("Failed to handle broadcast update operation:", {
1689
+ req.payload.logger.error("Error is instance of Error:", {
1676
1690
  message: error.message,
1677
1691
  stack: error.stack,
1678
1692
  name: error.name,
1679
- ...error.details
1693
+ ...error.details,
1694
+ ...error.response,
1695
+ ...error.data,
1696
+ ...error.status,
1697
+ ...error.statusText
1680
1698
  });
1699
+ } else if (typeof error === "string") {
1700
+ req.payload.logger.error("Error is a string:", error);
1701
+ } else if (error && typeof error === "object") {
1702
+ req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
1681
1703
  } else {
1682
- req.payload.logger.error("Failed to handle broadcast update operation:", error);
1704
+ req.payload.logger.error("Unknown error type:", typeof error);
1683
1705
  }
1706
+ req.payload.logger.error("Failed broadcast document (update operation):", {
1707
+ id: doc.id,
1708
+ subject: doc.subject,
1709
+ hasContent: !!doc.contentSection?.content,
1710
+ contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
1711
+ });
1684
1712
  }
1685
1713
  }
1686
1714
  return doc;