payload-plugin-newsletter 0.25.1 → 0.25.3

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [0.25.3] - 2025-08-16
2
+
3
+ ### Added
4
+ - Media population support for upload nodes within rich text fields in custom blocks
5
+ - Rich text fields in custom blocks now properly populate nested image uploads for email rendering
6
+
7
+ ### Fixed
8
+ - Images inserted in rich text editors within custom blocks now render correctly in email previews
9
+
10
+ ## [0.25.2] - 2025-08-15
11
+
12
+ ### Fixed
13
+ - Fixed issue where editing draft broadcasts created duplicate entries in the provider instead of updating the existing broadcast
14
+ - Broadcasts now immediately sync to provider on creation with minimal data to establish the association
15
+
1
16
  ## [0.25.1] - 2025-08-09
2
17
 
3
18
  ### Fixed
@@ -1514,7 +1514,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
1514
1514
  return {
1515
1515
  path: "/:id/send",
1516
1516
  method: "post",
1517
- handler: async (req) => {
1517
+ handler: (async (req) => {
1518
1518
  try {
1519
1519
  const auth = await requireAdmin(req, config);
1520
1520
  if (!auth.authorized) {
@@ -1588,7 +1588,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
1588
1588
  error: "Failed to send broadcast"
1589
1589
  }, { status: 500 });
1590
1590
  }
1591
- }
1591
+ })
1592
1592
  };
1593
1593
  };
1594
1594
 
@@ -1598,7 +1598,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
1598
1598
  return {
1599
1599
  path: "/:id/schedule",
1600
1600
  method: "post",
1601
- handler: async (req) => {
1601
+ handler: (async (req) => {
1602
1602
  try {
1603
1603
  const auth = await requireAdmin(req, config);
1604
1604
  if (!auth.authorized) {
@@ -1692,7 +1692,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
1692
1692
  error: "Failed to schedule broadcast"
1693
1693
  }, { status: 500 });
1694
1694
  }
1695
- }
1695
+ })
1696
1696
  };
1697
1697
  };
1698
1698
 
@@ -1701,7 +1701,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
1701
1701
  return {
1702
1702
  path: "/:id/test",
1703
1703
  method: "post",
1704
- handler: async (req) => {
1704
+ handler: (async (req) => {
1705
1705
  try {
1706
1706
  const auth = await requireAdmin(req, config);
1707
1707
  if (!auth.authorized) {
@@ -1775,7 +1775,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
1775
1775
  error: "Failed to send test email"
1776
1776
  }, { status: 500 });
1777
1777
  }
1778
- }
1778
+ })
1779
1779
  };
1780
1780
  };
1781
1781
 
@@ -1851,6 +1851,10 @@ async function populateBlockMediaFields(node, payload, config) {
1851
1851
  }
1852
1852
  }
1853
1853
  }
1854
+ if (field.type === "richText" && node.fields[field.name]) {
1855
+ await populateRichTextUploads(node.fields[field.name], payload);
1856
+ payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
1857
+ }
1854
1858
  }
1855
1859
  }
1856
1860
  }
@@ -1860,11 +1864,51 @@ async function populateBlockMediaFields(node, payload, config) {
1860
1864
  }
1861
1865
  }
1862
1866
  }
1867
+ async function populateRichTextUploads(content, payload) {
1868
+ if (!content || typeof content !== "object") return;
1869
+ if (content.root?.children) {
1870
+ await processNodeArray(content.root.children);
1871
+ }
1872
+ if (Array.isArray(content)) {
1873
+ await processNodeArray(content);
1874
+ }
1875
+ async function processNodeArray(nodes) {
1876
+ await Promise.all(nodes.map(processNode));
1877
+ }
1878
+ async function processNode(node) {
1879
+ if (!node || typeof node !== "object") return;
1880
+ if (node.type === "upload" && node.relationTo === "media" && typeof node.value === "string" && node.value.match(/^[a-f0-9]{24}$/i)) {
1881
+ try {
1882
+ const media = await payload.findByID({
1883
+ collection: "media",
1884
+ id: node.value,
1885
+ depth: 0
1886
+ });
1887
+ if (media) {
1888
+ node.value = media;
1889
+ payload.logger?.info(`Populated rich text upload node:`, {
1890
+ mediaId: node.value,
1891
+ mediaUrl: media.url,
1892
+ filename: media.filename
1893
+ });
1894
+ }
1895
+ } catch (error) {
1896
+ payload.logger?.error(`Failed to populate rich text upload ${node.value}:`, error);
1897
+ }
1898
+ }
1899
+ if (node.children && Array.isArray(node.children)) {
1900
+ await processNodeArray(node.children);
1901
+ }
1902
+ if (node.root?.children && Array.isArray(node.root.children)) {
1903
+ await processNodeArray(node.root.children);
1904
+ }
1905
+ }
1906
+ }
1863
1907
  var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1864
1908
  return {
1865
1909
  path: "/preview",
1866
1910
  method: "post",
1867
- handler: async (req) => {
1911
+ handler: (async (req) => {
1868
1912
  try {
1869
1913
  const data = await (req.json?.() || Promise.resolve({}));
1870
1914
  const { content, preheader, subject, documentData } = data;
@@ -1904,7 +1948,7 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1904
1948
  error: "Failed to generate email preview"
1905
1949
  }, { status: 500 });
1906
1950
  }
1907
- }
1951
+ })
1908
1952
  };
1909
1953
  };
1910
1954
 
@@ -2249,10 +2293,6 @@ var createBroadcastsCollection = (pluginConfig) => {
2249
2293
  async ({ doc, operation, req, previousDoc }) => {
2250
2294
  if (!hasProviders) return doc;
2251
2295
  if (operation === "create") {
2252
- if (!doc.subject || !doc.contentSection?.content) {
2253
- req.payload.logger.info("Skipping provider sync - broadcast has no subject or content yet");
2254
- return doc;
2255
- }
2256
2296
  try {
2257
2297
  const providerConfig = await getBroadcastConfig(req, pluginConfig);
2258
2298
  if (!providerConfig || !providerConfig.token) {
@@ -2261,45 +2301,32 @@ var createBroadcastsCollection = (pluginConfig) => {
2261
2301
  }
2262
2302
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
2263
2303
  const provider = new BroadcastApiProvider2(providerConfig);
2264
- req.payload.logger.info("Populating media fields and converting content to HTML...");
2265
- const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2266
- const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
2267
- const htmlContent = await convertToEmailSafeHtml(populatedContent, {
2268
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
2269
- customWrapper: emailPreviewConfig?.customWrapper,
2270
- preheader: doc.contentSection?.preheader,
2271
- subject: doc.subject,
2272
- documentData: doc,
2273
- // Pass entire document
2274
- customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2275
- });
2276
- if (!htmlContent || htmlContent.trim() === "") {
2277
- req.payload.logger.info("Skipping provider sync - content is empty after conversion");
2278
- return doc;
2279
- }
2304
+ const subject = doc.subject || `Draft Broadcast ${(/* @__PURE__ */ new Date()).toISOString()}`;
2305
+ const htmlContent = doc.contentSection?.content ? await convertToEmailSafeHtml(
2306
+ await populateMediaFields(doc.contentSection.content, req.payload, pluginConfig),
2307
+ {
2308
+ wrapInTemplate: pluginConfig.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
2309
+ customWrapper: pluginConfig.customizations?.broadcasts?.emailPreview?.customWrapper,
2310
+ preheader: doc.contentSection?.preheader,
2311
+ subject,
2312
+ documentData: doc,
2313
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2314
+ }
2315
+ ) : "<p>Draft content - to be updated</p>";
2280
2316
  const createData = {
2281
- name: doc.subject,
2282
- // Use subject as name since we removed the name field
2283
- subject: doc.subject,
2284
- preheader: doc.contentSection?.preheader,
2317
+ name: subject,
2318
+ // Use subject as name
2319
+ subject,
2320
+ preheader: doc.contentSection?.preheader || "",
2285
2321
  content: htmlContent,
2286
- trackOpens: doc.settings?.trackOpens,
2287
- trackClicks: doc.settings?.trackClicks,
2322
+ trackOpens: doc.settings?.trackOpens ?? true,
2323
+ trackClicks: doc.settings?.trackClicks ?? true,
2288
2324
  replyTo: doc.settings?.replyTo || providerConfig.replyTo,
2289
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
2325
+ audienceIds: doc.audienceIds?.map((a) => a.audienceId) || []
2290
2326
  };
2291
- req.payload.logger.info("Creating broadcast with data:", {
2292
- name: createData.name,
2327
+ req.payload.logger.info("Creating broadcast in provider with minimal data to establish association", {
2293
2328
  subject: createData.subject,
2294
- preheader: createData.preheader || "NONE",
2295
- contentLength: htmlContent ? htmlContent.length : 0,
2296
- contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
2297
- trackOpens: createData.trackOpens,
2298
- trackClicks: createData.trackClicks,
2299
- replyTo: createData.replyTo,
2300
- audienceIds: createData.audienceIds || [],
2301
- apiUrl: providerConfig.apiUrl,
2302
- hasToken: !!providerConfig.token
2329
+ hasActualContent: !!doc.contentSection?.content
2303
2330
  });
2304
2331
  const providerBroadcast = await provider.create(createData);
2305
2332
  await req.payload.update({
@@ -2311,40 +2338,14 @@ var createBroadcastsCollection = (pluginConfig) => {
2311
2338
  },
2312
2339
  req
2313
2340
  });
2341
+ req.payload.logger.info(`Broadcast ${doc.id} created in provider with ID ${providerBroadcast.id}`);
2314
2342
  return {
2315
2343
  ...doc,
2316
2344
  providerId: providerBroadcast.id,
2317
2345
  providerData: providerBroadcast.providerData
2318
2346
  };
2319
2347
  } catch (error) {
2320
- req.payload.logger.error("Raw error from broadcast provider:");
2321
- req.payload.logger.error(error);
2322
- if (error instanceof Error) {
2323
- req.payload.logger.error("Error is instance of Error:", {
2324
- message: error.message,
2325
- stack: error.stack,
2326
- name: error.name,
2327
- // If it's a BroadcastProviderError, it might have additional details
2328
- ...error.details,
2329
- // Check if it's a fetch response error
2330
- ...error.response,
2331
- ...error.data,
2332
- ...error.status,
2333
- ...error.statusText
2334
- });
2335
- } else if (typeof error === "string") {
2336
- req.payload.logger.error("Error is a string:", error);
2337
- } else if (error && typeof error === "object") {
2338
- req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
2339
- } else {
2340
- req.payload.logger.error("Unknown error type:", typeof error);
2341
- }
2342
- req.payload.logger.error("Failed broadcast document:", {
2343
- id: doc.id,
2344
- subject: doc.subject,
2345
- hasContent: !!doc.contentSection?.content,
2346
- contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
2347
- });
2348
+ req.payload.logger.error("Failed to create broadcast in provider during initial creation:", error);
2348
2349
  return doc;
2349
2350
  }
2350
2351
  }
@@ -2364,61 +2365,8 @@ var createBroadcastsCollection = (pluginConfig) => {
2364
2365
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
2365
2366
  const provider = new BroadcastApiProvider2(providerConfig);
2366
2367
  if (!doc.providerId) {
2367
- if (!doc.subject || !doc.contentSection?.content) {
2368
- req.payload.logger.info("Still missing required fields for provider sync");
2369
- return doc;
2370
- }
2371
- req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
2372
- const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
2373
- const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
2374
- const htmlContent = await convertToEmailSafeHtml(populatedContent, {
2375
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
2376
- customWrapper: emailPreviewConfig?.customWrapper,
2377
- preheader: doc.contentSection?.preheader,
2378
- subject: doc.subject,
2379
- documentData: doc,
2380
- // Pass entire document
2381
- customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
2382
- });
2383
- if (!htmlContent || htmlContent.trim() === "") {
2384
- req.payload.logger.info("Skipping provider sync - content is empty after conversion");
2385
- return doc;
2386
- }
2387
- const createData = {
2388
- name: doc.subject,
2389
- subject: doc.subject,
2390
- preheader: doc.contentSection?.preheader,
2391
- content: htmlContent,
2392
- trackOpens: doc.settings?.trackOpens,
2393
- trackClicks: doc.settings?.trackClicks,
2394
- replyTo: doc.settings?.replyTo || providerConfig.replyTo,
2395
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
2396
- };
2397
- req.payload.logger.info("Creating broadcast with data:", {
2398
- name: createData.name,
2399
- subject: createData.subject,
2400
- preheader: createData.preheader || "NONE",
2401
- contentLength: htmlContent ? htmlContent.length : 0,
2402
- contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
2403
- apiUrl: providerConfig.apiUrl,
2404
- hasToken: !!providerConfig.token
2405
- });
2406
- const providerBroadcast = await provider.create(createData);
2407
- await req.payload.update({
2408
- collection: "broadcasts",
2409
- id: doc.id,
2410
- data: {
2411
- providerId: providerBroadcast.id,
2412
- providerData: providerBroadcast.providerData
2413
- },
2414
- req
2415
- });
2416
- req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
2417
- return {
2418
- ...doc,
2419
- providerId: providerBroadcast.id,
2420
- providerData: providerBroadcast.providerData
2421
- };
2368
+ req.payload.logger.warn(`Broadcast ${doc.id} has no providerId - provider sync skipped. This shouldn't happen with immediate creation.`);
2369
+ return doc;
2422
2370
  }
2423
2371
  if (doc.providerId) {
2424
2372
  const capabilities = provider.getCapabilities();