payload-plugin-newsletter 0.25.9 → 0.25.12

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,27 @@
1
+ ## [0.25.12] - 2026-01-02
2
+
3
+ ### Added
4
+ - New exported utilities for external preview generation:
5
+ - `generateBroadcastPreviewHtml()` - Generate preview HTML from broadcast content
6
+ - `populateMediaFields()` - Resolve Media IDs to full media objects with URLs
7
+ - `convertToEmailSafeHtml()` - Convert Lexical content to email-safe HTML
8
+ - `PreviewOptions` and `PreviewResult` types for TypeScript support
9
+ - Extracted media population logic to `src/utils/mediaPopulation.ts` for cleaner code organization
10
+ - Created `src/utils/preview.ts` with high-level preview generation utility
11
+
12
+ ### Changed
13
+ - Refactored `src/endpoints/broadcasts/preview.ts` to use new utility files
14
+ - Improved TypeScript types for media population functions
15
+
16
+ ## [0.25.11] - 2025-12-11
17
+
18
+ ### Fixed
19
+ - Fixed LinkFeature configuration causing URLs to disappear in the Lexical editor link drawer
20
+ - The custom `fields` array was replacing Payload's default fields, removing the crucial `linkType` field
21
+ - Now uses default LinkFeature fields which include all required fields for the drawer UI to work correctly
22
+ - Fixed TypeScript errors with Payload logger API (pino signature requires object first, then message)
23
+ - Fixed `relationTo` type errors where `string | string[]` was passed to `findByID`
24
+
1
25
  ## [0.25.9] - 2025-08-19
2
26
 
3
27
  ### Fixed
@@ -754,25 +754,8 @@ var createEmailSafeFeatures = (additionalBlocks) => {
754
754
  (0, import_richtext_lexical.ItalicFeature)(),
755
755
  (0, import_richtext_lexical.UnderlineFeature)(),
756
756
  (0, import_richtext_lexical.StrikethroughFeature)(),
757
- // Links with enhanced configuration
758
- (0, import_richtext_lexical.LinkFeature)({
759
- fields: [
760
- {
761
- name: "url",
762
- type: "text",
763
- required: true,
764
- admin: {
765
- description: "Enter the full URL (including https://)"
766
- }
767
- },
768
- {
769
- name: "newTab",
770
- type: "checkbox",
771
- label: "Open in new tab",
772
- defaultValue: false
773
- }
774
- ]
775
- }),
757
+ // Links - use default fields to ensure drawer UI works correctly
758
+ (0, import_richtext_lexical.LinkFeature)(),
776
759
  // Lists
777
760
  (0, import_richtext_lexical.OrderedListFeature)(),
778
761
  (0, import_richtext_lexical.UnorderedListFeature)(),
@@ -828,25 +811,8 @@ var createEmailLexicalEditor = (customBlocks = []) => {
828
811
  (0, import_richtext_lexical.ItalicFeature)(),
829
812
  (0, import_richtext_lexical.UnderlineFeature)(),
830
813
  (0, import_richtext_lexical.StrikethroughFeature)(),
831
- // Links with enhanced configuration
832
- (0, import_richtext_lexical.LinkFeature)({
833
- fields: [
834
- {
835
- name: "url",
836
- type: "text",
837
- required: true,
838
- admin: {
839
- description: "Enter the full URL (including https://)"
840
- }
841
- },
842
- {
843
- name: "newTab",
844
- type: "checkbox",
845
- label: "Open in new tab",
846
- defaultValue: false
847
- }
848
- ]
849
- }),
814
+ // Links - use default fields to ensure drawer UI works correctly
815
+ (0, import_richtext_lexical.LinkFeature)(),
850
816
  // Lists
851
817
  (0, import_richtext_lexical.OrderedListFeature)(),
852
818
  (0, import_richtext_lexical.UnorderedListFeature)(),
@@ -1414,7 +1380,7 @@ async function getBroadcastConfig(req, pluginConfig) {
1414
1380
  }
1415
1381
  return pluginConfig.providers?.broadcast || null;
1416
1382
  } catch (error) {
1417
- req.payload.logger.error("Failed to get broadcast config from settings:", error);
1383
+ req.payload.logger.error({ error: String(error) }, "Failed to get broadcast config from settings");
1418
1384
  return pluginConfig.providers?.broadcast || null;
1419
1385
  }
1420
1386
  }
@@ -1792,70 +1758,88 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
1792
1758
  };
1793
1759
  };
1794
1760
 
1795
- // src/endpoints/broadcasts/preview.ts
1761
+ // src/utils/mediaPopulation.ts
1796
1762
  async function populateMediaFields(content, payload, config) {
1797
1763
  if (!content || typeof content !== "object") return content;
1798
- if (content.root?.children) {
1799
- for (const child of content.root.children) {
1764
+ const typedContent = content;
1765
+ if (typedContent.root?.children) {
1766
+ for (const child of typedContent.root.children) {
1800
1767
  await populateBlockMediaFields(child, payload, config);
1801
1768
  }
1802
1769
  }
1803
1770
  return content;
1804
1771
  }
1805
1772
  async function populateBlockMediaFields(node, payload, config) {
1806
- if (node.type === "block" && node.fields) {
1807
- const blockType = node.fields.blockType || node.fields.blockName;
1773
+ if (!node || typeof node !== "object") return;
1774
+ const typedNode = node;
1775
+ if (typedNode.type === "block" && typedNode.fields) {
1776
+ const blockType = typedNode.fields.blockType || typedNode.fields.blockName;
1808
1777
  const customBlocks = config.customizations?.broadcasts?.customBlocks || [];
1809
1778
  const blockConfig = customBlocks.find((b) => b.slug === blockType);
1810
1779
  if (blockConfig && blockConfig.fields) {
1811
1780
  for (const field of blockConfig.fields) {
1812
- if (field.type === "upload" && field.relationTo && node.fields[field.name]) {
1813
- const fieldValue = node.fields[field.name];
1781
+ if (field.type === "upload" && field.relationTo && typedNode.fields[field.name]) {
1782
+ const fieldValue = typedNode.fields[field.name];
1783
+ const collectionName = Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo;
1814
1784
  if (typeof fieldValue === "string" && fieldValue.match(/^[a-f0-9]{24}$/i)) {
1815
1785
  try {
1816
1786
  const media = await payload.findByID({
1817
- collection: field.relationTo,
1787
+ collection: collectionName,
1818
1788
  id: fieldValue,
1819
1789
  depth: 0
1820
1790
  });
1821
1791
  if (media) {
1822
- node.fields[field.name] = media;
1823
- payload.logger?.info(`Populated ${field.name} for block ${blockType}:`, {
1824
- mediaId: fieldValue,
1825
- mediaUrl: media.url,
1826
- filename: media.filename
1827
- });
1792
+ typedNode.fields[field.name] = media;
1793
+ payload.logger?.info(
1794
+ {
1795
+ mediaId: fieldValue,
1796
+ mediaUrl: media.url,
1797
+ filename: media.filename
1798
+ },
1799
+ `Populated ${field.name} for block ${blockType}`
1800
+ );
1828
1801
  }
1829
1802
  } catch (error) {
1830
- payload.logger?.error(`Failed to populate ${field.name} for block ${blockType}:`, error);
1803
+ payload.logger?.error(
1804
+ { error: String(error) },
1805
+ `Failed to populate ${field.name} for block ${blockType}`
1806
+ );
1831
1807
  }
1832
1808
  }
1833
1809
  }
1834
1810
  if (field.type === "array" && field.fields) {
1835
- const arrayValue = node.fields[field.name];
1811
+ const arrayValue = typedNode.fields[field.name];
1836
1812
  if (Array.isArray(arrayValue)) {
1837
1813
  for (const arrayItem of arrayValue) {
1838
1814
  if (arrayItem && typeof arrayItem === "object") {
1815
+ const typedArrayItem = arrayItem;
1839
1816
  for (const arrayField of field.fields) {
1840
- if (arrayField.type === "upload" && arrayField.relationTo && arrayItem[arrayField.name]) {
1841
- const arrayFieldValue = arrayItem[arrayField.name];
1817
+ if (arrayField.type === "upload" && arrayField.relationTo && typedArrayItem[arrayField.name]) {
1818
+ const arrayFieldValue = typedArrayItem[arrayField.name];
1819
+ const arrayCollectionName = Array.isArray(arrayField.relationTo) ? arrayField.relationTo[0] : arrayField.relationTo;
1842
1820
  if (typeof arrayFieldValue === "string" && arrayFieldValue.match(/^[a-f0-9]{24}$/i)) {
1843
1821
  try {
1844
1822
  const media = await payload.findByID({
1845
- collection: arrayField.relationTo,
1823
+ collection: arrayCollectionName,
1846
1824
  id: arrayFieldValue,
1847
1825
  depth: 0
1848
1826
  });
1849
1827
  if (media) {
1850
- arrayItem[arrayField.name] = media;
1851
- payload.logger?.info(`Populated array ${arrayField.name} for block ${blockType}:`, {
1852
- mediaId: arrayFieldValue,
1853
- mediaUrl: media.url,
1854
- filename: media.filename
1855
- });
1828
+ typedArrayItem[arrayField.name] = media;
1829
+ payload.logger?.info(
1830
+ {
1831
+ mediaId: arrayFieldValue,
1832
+ mediaUrl: media.url,
1833
+ filename: media.filename
1834
+ },
1835
+ `Populated array ${arrayField.name} for block ${blockType}`
1836
+ );
1856
1837
  }
1857
1838
  } catch (error) {
1858
- payload.logger?.error(`Failed to populate array ${arrayField.name} for block ${blockType}:`, error);
1839
+ payload.logger?.error(
1840
+ { error: String(error) },
1841
+ `Failed to populate array ${arrayField.name} for block ${blockType}`
1842
+ );
1859
1843
  }
1860
1844
  }
1861
1845
  }
@@ -1864,23 +1848,24 @@ async function populateBlockMediaFields(node, payload, config) {
1864
1848
  }
1865
1849
  }
1866
1850
  }
1867
- if (field.type === "richText" && node.fields[field.name]) {
1868
- await populateRichTextUploads(node.fields[field.name], payload);
1851
+ if (field.type === "richText" && typedNode.fields[field.name]) {
1852
+ await populateRichTextUploads(typedNode.fields[field.name], payload);
1869
1853
  payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
1870
1854
  }
1871
1855
  }
1872
1856
  }
1873
1857
  }
1874
- if (node.children) {
1875
- for (const child of node.children) {
1858
+ if (typedNode.children) {
1859
+ for (const child of typedNode.children) {
1876
1860
  await populateBlockMediaFields(child, payload, config);
1877
1861
  }
1878
1862
  }
1879
1863
  }
1880
1864
  async function populateRichTextUploads(content, payload) {
1881
1865
  if (!content || typeof content !== "object") return;
1882
- if (content.root?.children) {
1883
- await processNodeArray(content.root.children);
1866
+ const typedContent = content;
1867
+ if (typedContent.root?.children) {
1868
+ await processNodeArray(typedContent.root.children);
1884
1869
  }
1885
1870
  if (Array.isArray(content)) {
1886
1871
  await processNodeArray(content);
@@ -1890,33 +1875,42 @@ async function populateRichTextUploads(content, payload) {
1890
1875
  }
1891
1876
  async function processNode(node) {
1892
1877
  if (!node || typeof node !== "object") return;
1893
- if (node.type === "upload" && node.relationTo === "media" && typeof node.value === "string" && node.value.match(/^[a-f0-9]{24}$/i)) {
1878
+ const typedNode = node;
1879
+ if (typedNode.type === "upload" && typedNode.relationTo === "media" && typeof typedNode.value === "string" && typedNode.value.match(/^[a-f0-9]{24}$/i)) {
1894
1880
  try {
1895
1881
  const media = await payload.findByID({
1896
1882
  collection: "media",
1897
- id: node.value,
1883
+ id: typedNode.value,
1898
1884
  depth: 0
1899
1885
  });
1900
1886
  if (media) {
1901
- node.value = media;
1902
- payload.logger?.info(`Populated rich text upload node:`, {
1903
- mediaId: node.value,
1904
- mediaUrl: media.url,
1905
- filename: media.filename
1906
- });
1887
+ typedNode.value = media;
1888
+ payload.logger?.info(
1889
+ {
1890
+ mediaId: typedNode.value,
1891
+ mediaUrl: media.url,
1892
+ filename: media.filename
1893
+ },
1894
+ "Populated rich text upload node"
1895
+ );
1907
1896
  }
1908
1897
  } catch (error) {
1909
- payload.logger?.error(`Failed to populate rich text upload ${node.value}:`, error);
1898
+ payload.logger?.error(
1899
+ { error: String(error) },
1900
+ `Failed to populate rich text upload ${typedNode.value}`
1901
+ );
1910
1902
  }
1911
1903
  }
1912
- if (node.children && Array.isArray(node.children)) {
1913
- await processNodeArray(node.children);
1904
+ if (typedNode.children && Array.isArray(typedNode.children)) {
1905
+ await processNodeArray(typedNode.children);
1914
1906
  }
1915
- if (node.root?.children && Array.isArray(node.root.children)) {
1916
- await processNodeArray(node.root.children);
1907
+ if (typedNode.root?.children && Array.isArray(typedNode.root.children)) {
1908
+ await processNodeArray(typedNode.root.children);
1917
1909
  }
1918
1910
  }
1919
1911
  }
1912
+
1913
+ // src/endpoints/broadcasts/preview.ts
1920
1914
  var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
1921
1915
  return {
1922
1916
  path: "/preview",
@@ -2310,7 +2304,7 @@ var createBroadcastsCollection = (pluginConfig) => {
2310
2304
  return doc;
2311
2305
  }
2312
2306
  if (operation === "update") {
2313
- req.payload.logger.info("Broadcast afterChange update hook triggered", {
2307
+ req.payload.logger.info({
2314
2308
  operation,
2315
2309
  hasProviderId: !!doc.providerId,
2316
2310
  hasExternalId: !!doc.externalId,
@@ -2318,7 +2312,7 @@ var createBroadcastsCollection = (pluginConfig) => {
2318
2312
  publishStatus: doc._status,
2319
2313
  hasSubject: !!doc.subject,
2320
2314
  hasContent: !!doc.contentSection?.content
2321
- });
2315
+ }, "Broadcast afterChange update hook triggered");
2322
2316
  try {
2323
2317
  const providerConfig = await getBroadcastConfig(req, pluginConfig);
2324
2318
  if (!providerConfig || !providerConfig.token) {
@@ -2405,10 +2399,10 @@ var createBroadcastsCollection = (pluginConfig) => {
2405
2399
  if (JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds)) {
2406
2400
  updates.audienceIds = doc.audienceIds?.map((a) => a.audienceId);
2407
2401
  }
2408
- req.payload.logger.info("Syncing broadcast updates to provider", {
2402
+ req.payload.logger.info({
2409
2403
  providerId: doc.providerId,
2410
2404
  updates
2411
- });
2405
+ }, "Syncing broadcast updates to provider");
2412
2406
  await provider.update(doc.providerId, updates);
2413
2407
  req.payload.logger.info(`Broadcast ${doc.id} synced to provider successfully`);
2414
2408
  } else {
@@ -2430,18 +2424,18 @@ var createBroadcastsCollection = (pluginConfig) => {
2430
2424
  ...error.statusText
2431
2425
  });
2432
2426
  } else if (typeof error === "string") {
2433
- req.payload.logger.error("Error is a string:", error);
2427
+ req.payload.logger.error({ errorValue: error }, "Error is a string");
2434
2428
  } else if (error && typeof error === "object") {
2435
- req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
2429
+ req.payload.logger.error({ errorValue: JSON.stringify(error, null, 2) }, "Error is an object");
2436
2430
  } else {
2437
- req.payload.logger.error("Unknown error type:", typeof error);
2431
+ req.payload.logger.error({ errorType: typeof error }, "Unknown error type");
2438
2432
  }
2439
- req.payload.logger.error("Failed broadcast document (update operation):", {
2433
+ req.payload.logger.error({
2440
2434
  id: doc.id,
2441
2435
  subject: doc.subject,
2442
2436
  hasContent: !!doc.contentSection?.content,
2443
2437
  contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
2444
- });
2438
+ }, "Failed broadcast document (update operation)");
2445
2439
  }
2446
2440
  }
2447
2441
  return doc;
@@ -2487,7 +2481,7 @@ var createBroadcastsCollection = (pluginConfig) => {
2487
2481
  ...error.details
2488
2482
  });
2489
2483
  } else {
2490
- req.payload.logger.error(`Failed to send broadcast ${doc.id}:`, error);
2484
+ req.payload.logger.error({ error: String(error) }, `Failed to send broadcast ${doc.id}`);
2491
2485
  }
2492
2486
  await req.payload.update({
2493
2487
  collection: "broadcasts",
@@ -2530,7 +2524,7 @@ var createBroadcastsCollection = (pluginConfig) => {
2530
2524
  ...error.details
2531
2525
  });
2532
2526
  } else {
2533
- req.payload.logger.error("Failed to delete broadcast from provider:", error);
2527
+ req.payload.logger.error({ error: String(error) }, "Failed to delete broadcast from provider");
2534
2528
  }
2535
2529
  }
2536
2530
  return doc;