payload-plugin-newsletter 0.14.3 → 0.15.1

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/dist/fields.js CHANGED
@@ -17,144 +17,323 @@ import {
17
17
  InlineToolbarFeature,
18
18
  lexicalEditor
19
19
  } from "@payloadcms/richtext-lexical";
20
- var emailSafeFeatures = [
21
- // Toolbars
22
- FixedToolbarFeature(),
23
- // Fixed toolbar at the top
24
- InlineToolbarFeature(),
25
- // Floating toolbar when text is selected
26
- // Basic text formatting
27
- BoldFeature(),
28
- ItalicFeature(),
29
- UnderlineFeature(),
30
- StrikethroughFeature(),
31
- // Links with enhanced configuration
32
- LinkFeature({
33
- fields: [
34
- {
35
- name: "url",
36
- type: "text",
37
- required: true,
38
- admin: {
39
- description: "Enter the full URL (including https://)"
20
+
21
+ // src/utils/blockValidation.ts
22
+ var EMAIL_INCOMPATIBLE_TYPES = [
23
+ "chart",
24
+ "dataTable",
25
+ "interactive",
26
+ "streamable",
27
+ "video",
28
+ "iframe",
29
+ "form",
30
+ "carousel",
31
+ "tabs",
32
+ "accordion",
33
+ "map"
34
+ ];
35
+ var validateEmailBlocks = (blocks) => {
36
+ blocks.forEach((block) => {
37
+ if (EMAIL_INCOMPATIBLE_TYPES.includes(block.slug)) {
38
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" may not be email-compatible. Consider creating an email-specific version.`);
39
+ }
40
+ const hasComplexFields = block.fields?.some((field) => {
41
+ const complexTypes = ["code", "json", "richText", "blocks", "array"];
42
+ return complexTypes.includes(field.type);
43
+ });
44
+ if (hasComplexFields) {
45
+ console.warn(`\u26A0\uFE0F Block "${block.slug}" contains complex field types that may not render consistently in email clients.`);
46
+ }
47
+ });
48
+ };
49
+ var createEmailSafeBlocks = (customBlocks = []) => {
50
+ validateEmailBlocks(customBlocks);
51
+ const baseBlocks = [
52
+ {
53
+ slug: "button",
54
+ fields: [
55
+ {
56
+ name: "text",
57
+ type: "text",
58
+ label: "Button Text",
59
+ required: true
60
+ },
61
+ {
62
+ name: "url",
63
+ type: "text",
64
+ label: "Button URL",
65
+ required: true,
66
+ admin: {
67
+ description: "Enter the full URL (including https://)"
68
+ }
69
+ },
70
+ {
71
+ name: "style",
72
+ type: "select",
73
+ label: "Button Style",
74
+ defaultValue: "primary",
75
+ options: [
76
+ { label: "Primary", value: "primary" },
77
+ { label: "Secondary", value: "secondary" },
78
+ { label: "Outline", value: "outline" }
79
+ ]
40
80
  }
41
- },
42
- {
43
- name: "newTab",
44
- type: "checkbox",
45
- label: "Open in new tab",
46
- defaultValue: false
81
+ ],
82
+ interfaceName: "EmailButton",
83
+ labels: {
84
+ singular: "Button",
85
+ plural: "Buttons"
47
86
  }
48
- ]
49
- }),
50
- // Lists
51
- OrderedListFeature(),
52
- UnorderedListFeature(),
53
- // Headings - limited to h1, h2, h3 for email compatibility
54
- HeadingFeature({
55
- enabledHeadingSizes: ["h1", "h2", "h3"]
56
- }),
57
- // Basic paragraph and alignment
58
- ParagraphFeature(),
59
- AlignFeature(),
60
- // Blockquotes
61
- BlockquoteFeature(),
62
- // Upload feature for images
63
- UploadFeature({
64
- collections: {
65
- media: {
66
- fields: [
67
- {
68
- name: "caption",
69
- type: "text",
70
- admin: {
71
- description: "Optional caption for the image"
72
- }
73
- },
74
- {
75
- name: "altText",
76
- type: "text",
77
- label: "Alt Text",
78
- required: true,
79
- admin: {
80
- description: "Alternative text for accessibility and when image cannot be displayed"
81
- }
87
+ },
88
+ {
89
+ slug: "divider",
90
+ fields: [
91
+ {
92
+ name: "style",
93
+ type: "select",
94
+ label: "Divider Style",
95
+ defaultValue: "solid",
96
+ options: [
97
+ { label: "Solid", value: "solid" },
98
+ { label: "Dashed", value: "dashed" },
99
+ { label: "Dotted", value: "dotted" }
100
+ ]
101
+ }
102
+ ],
103
+ interfaceName: "EmailDivider",
104
+ labels: {
105
+ singular: "Divider",
106
+ plural: "Dividers"
107
+ }
108
+ }
109
+ ];
110
+ return [
111
+ ...baseBlocks,
112
+ ...customBlocks
113
+ ];
114
+ };
115
+
116
+ // src/fields/emailContent.ts
117
+ var createEmailSafeFeatures = (additionalBlocks) => {
118
+ const baseBlocks = [
119
+ {
120
+ slug: "button",
121
+ fields: [
122
+ {
123
+ name: "text",
124
+ type: "text",
125
+ label: "Button Text",
126
+ required: true
127
+ },
128
+ {
129
+ name: "url",
130
+ type: "text",
131
+ label: "Button URL",
132
+ required: true,
133
+ admin: {
134
+ description: "Enter the full URL (including https://)"
82
135
  }
83
- ]
136
+ },
137
+ {
138
+ name: "style",
139
+ type: "select",
140
+ label: "Button Style",
141
+ defaultValue: "primary",
142
+ options: [
143
+ { label: "Primary", value: "primary" },
144
+ { label: "Secondary", value: "secondary" },
145
+ { label: "Outline", value: "outline" }
146
+ ]
147
+ }
148
+ ],
149
+ interfaceName: "EmailButton",
150
+ labels: {
151
+ singular: "Button",
152
+ plural: "Buttons"
153
+ }
154
+ },
155
+ {
156
+ slug: "divider",
157
+ fields: [
158
+ {
159
+ name: "style",
160
+ type: "select",
161
+ label: "Divider Style",
162
+ defaultValue: "solid",
163
+ options: [
164
+ { label: "Solid", value: "solid" },
165
+ { label: "Dashed", value: "dashed" },
166
+ { label: "Dotted", value: "dotted" }
167
+ ]
168
+ }
169
+ ],
170
+ interfaceName: "EmailDivider",
171
+ labels: {
172
+ singular: "Divider",
173
+ plural: "Dividers"
84
174
  }
85
175
  }
86
- }),
87
- // Custom blocks for email-specific content
88
- BlocksFeature({
89
- blocks: [
90
- {
91
- slug: "button",
176
+ ];
177
+ const allBlocks = [
178
+ ...baseBlocks,
179
+ ...additionalBlocks || []
180
+ ];
181
+ return [
182
+ // Toolbars
183
+ FixedToolbarFeature(),
184
+ // Fixed toolbar at the top
185
+ InlineToolbarFeature(),
186
+ // Floating toolbar when text is selected
187
+ // Basic text formatting
188
+ BoldFeature(),
189
+ ItalicFeature(),
190
+ UnderlineFeature(),
191
+ StrikethroughFeature(),
192
+ // Links with enhanced configuration
193
+ LinkFeature({
194
+ fields: [
195
+ {
196
+ name: "url",
197
+ type: "text",
198
+ required: true,
199
+ admin: {
200
+ description: "Enter the full URL (including https://)"
201
+ }
202
+ },
203
+ {
204
+ name: "newTab",
205
+ type: "checkbox",
206
+ label: "Open in new tab",
207
+ defaultValue: false
208
+ }
209
+ ]
210
+ }),
211
+ // Lists
212
+ OrderedListFeature(),
213
+ UnorderedListFeature(),
214
+ // Headings - limited to h1, h2, h3 for email compatibility
215
+ HeadingFeature({
216
+ enabledHeadingSizes: ["h1", "h2", "h3"]
217
+ }),
218
+ // Basic paragraph and alignment
219
+ ParagraphFeature(),
220
+ AlignFeature(),
221
+ // Blockquotes
222
+ BlockquoteFeature(),
223
+ // Upload feature for images
224
+ UploadFeature({
225
+ collections: {
226
+ media: {
227
+ fields: [
228
+ {
229
+ name: "caption",
230
+ type: "text",
231
+ admin: {
232
+ description: "Optional caption for the image"
233
+ }
234
+ },
235
+ {
236
+ name: "altText",
237
+ type: "text",
238
+ label: "Alt Text",
239
+ required: true,
240
+ admin: {
241
+ description: "Alternative text for accessibility and when image cannot be displayed"
242
+ }
243
+ }
244
+ ]
245
+ }
246
+ }
247
+ }),
248
+ // Custom blocks for email-specific content
249
+ BlocksFeature({
250
+ blocks: allBlocks
251
+ })
252
+ ];
253
+ };
254
+ var createEmailLexicalEditor = (customBlocks = []) => {
255
+ const emailSafeBlocks = createEmailSafeBlocks(customBlocks);
256
+ return lexicalEditor({
257
+ features: [
258
+ // Toolbars
259
+ FixedToolbarFeature(),
260
+ InlineToolbarFeature(),
261
+ // Basic text formatting
262
+ BoldFeature(),
263
+ ItalicFeature(),
264
+ UnderlineFeature(),
265
+ StrikethroughFeature(),
266
+ // Links with enhanced configuration
267
+ LinkFeature({
92
268
  fields: [
93
- {
94
- name: "text",
95
- type: "text",
96
- label: "Button Text",
97
- required: true
98
- },
99
269
  {
100
270
  name: "url",
101
271
  type: "text",
102
- label: "Button URL",
103
272
  required: true,
104
273
  admin: {
105
274
  description: "Enter the full URL (including https://)"
106
275
  }
107
276
  },
108
277
  {
109
- name: "style",
110
- type: "select",
111
- label: "Button Style",
112
- defaultValue: "primary",
113
- options: [
114
- { label: "Primary", value: "primary" },
115
- { label: "Secondary", value: "secondary" },
116
- { label: "Outline", value: "outline" }
117
- ]
278
+ name: "newTab",
279
+ type: "checkbox",
280
+ label: "Open in new tab",
281
+ defaultValue: false
118
282
  }
119
- ],
120
- interfaceName: "EmailButton",
121
- labels: {
122
- singular: "Button",
123
- plural: "Buttons"
124
- }
125
- },
126
- {
127
- slug: "divider",
128
- fields: [
129
- {
130
- name: "style",
131
- type: "select",
132
- label: "Divider Style",
133
- defaultValue: "solid",
134
- options: [
135
- { label: "Solid", value: "solid" },
136
- { label: "Dashed", value: "dashed" },
137
- { label: "Dotted", value: "dotted" }
283
+ ]
284
+ }),
285
+ // Lists
286
+ OrderedListFeature(),
287
+ UnorderedListFeature(),
288
+ // Headings - limited to h1, h2, h3 for email compatibility
289
+ HeadingFeature({
290
+ enabledHeadingSizes: ["h1", "h2", "h3"]
291
+ }),
292
+ // Basic paragraph and alignment
293
+ ParagraphFeature(),
294
+ AlignFeature(),
295
+ // Blockquotes
296
+ BlockquoteFeature(),
297
+ // Upload feature for images
298
+ UploadFeature({
299
+ collections: {
300
+ media: {
301
+ fields: [
302
+ {
303
+ name: "caption",
304
+ type: "text",
305
+ admin: {
306
+ description: "Optional caption for the image"
307
+ }
308
+ },
309
+ {
310
+ name: "altText",
311
+ type: "text",
312
+ label: "Alt Text",
313
+ required: true,
314
+ admin: {
315
+ description: "Alternative text for accessibility and when image cannot be displayed"
316
+ }
317
+ }
138
318
  ]
139
319
  }
140
- ],
141
- interfaceName: "EmailDivider",
142
- labels: {
143
- singular: "Divider",
144
- plural: "Dividers"
145
320
  }
146
- }
321
+ }),
322
+ // Email-safe blocks (processed server-side)
323
+ BlocksFeature({
324
+ blocks: emailSafeBlocks
325
+ })
147
326
  ]
148
- })
149
- ];
327
+ });
328
+ };
329
+ var emailSafeFeatures = createEmailSafeFeatures();
150
330
  var createEmailContentField = (overrides) => {
331
+ const editor = overrides?.editor || createEmailLexicalEditor(overrides?.additionalBlocks);
151
332
  return {
152
333
  name: "content",
153
334
  type: "richText",
154
335
  required: true,
155
- editor: lexicalEditor({
156
- features: emailSafeFeatures
157
- }),
336
+ editor,
158
337
  admin: {
159
338
  description: "Email content with limited formatting for compatibility",
160
339
  ...overrides?.admin
@@ -162,8 +341,196 @@ var createEmailContentField = (overrides) => {
162
341
  ...overrides
163
342
  };
164
343
  };
344
+
345
+ // src/fields/broadcastInlinePreview.ts
346
+ var createBroadcastInlinePreviewField = () => {
347
+ return {
348
+ name: "broadcastInlinePreview",
349
+ type: "ui",
350
+ admin: {
351
+ components: {
352
+ Field: "payload-plugin-newsletter/components#BroadcastInlinePreview"
353
+ }
354
+ }
355
+ };
356
+ };
357
+
358
+ // src/fields/newsletterScheduling.ts
359
+ function createNewsletterSchedulingFields(config) {
360
+ const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
361
+ const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
362
+ const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
363
+ const fields = [
364
+ {
365
+ name: groupName,
366
+ type: "group",
367
+ label: "Newsletter Scheduling",
368
+ admin: {
369
+ condition: (data, { user }) => user?.collection === "users"
370
+ // Only show for admin users
371
+ },
372
+ fields: [
373
+ {
374
+ name: "scheduled",
375
+ type: "checkbox",
376
+ label: "Schedule for Newsletter",
377
+ defaultValue: false,
378
+ admin: {
379
+ description: "Schedule this content to be sent as a newsletter"
380
+ }
381
+ },
382
+ {
383
+ name: "scheduledDate",
384
+ type: "date",
385
+ label: "Send Date",
386
+ required: true,
387
+ admin: {
388
+ date: {
389
+ pickerAppearance: "dayAndTime"
390
+ },
391
+ condition: (data) => data?.[groupName]?.scheduled,
392
+ description: "When to send this newsletter"
393
+ }
394
+ },
395
+ {
396
+ name: "sentDate",
397
+ type: "date",
398
+ label: "Sent Date",
399
+ admin: {
400
+ readOnly: true,
401
+ condition: (data) => data?.[groupName]?.sendStatus === "sent",
402
+ description: "When this newsletter was sent"
403
+ }
404
+ },
405
+ {
406
+ name: "sendStatus",
407
+ type: "select",
408
+ label: "Status",
409
+ options: [
410
+ { label: "Draft", value: "draft" },
411
+ { label: "Scheduled", value: "scheduled" },
412
+ { label: "Sending", value: "sending" },
413
+ { label: "Sent", value: "sent" },
414
+ { label: "Failed", value: "failed" }
415
+ ],
416
+ defaultValue: "draft",
417
+ admin: {
418
+ readOnly: true,
419
+ description: "Current send status"
420
+ }
421
+ },
422
+ {
423
+ name: "emailSubject",
424
+ type: "text",
425
+ label: "Email Subject",
426
+ required: true,
427
+ admin: {
428
+ condition: (data) => data?.[groupName]?.scheduled,
429
+ description: "Subject line for the newsletter email"
430
+ }
431
+ },
432
+ {
433
+ name: "preheader",
434
+ type: "text",
435
+ label: "Email Preheader",
436
+ admin: {
437
+ condition: (data) => data?.[groupName]?.scheduled,
438
+ description: "Preview text that appears after the subject line"
439
+ }
440
+ },
441
+ {
442
+ name: "segments",
443
+ type: "select",
444
+ label: "Target Segments",
445
+ hasMany: true,
446
+ options: [
447
+ { label: "All Subscribers", value: "all" },
448
+ ...config.i18n?.locales?.map((locale) => ({
449
+ label: `${locale.toUpperCase()} Subscribers`,
450
+ value: locale
451
+ })) || []
452
+ ],
453
+ defaultValue: ["all"],
454
+ admin: {
455
+ condition: (data) => data?.[groupName]?.scheduled,
456
+ description: "Which subscriber segments to send to"
457
+ }
458
+ },
459
+ {
460
+ name: "testEmails",
461
+ type: "array",
462
+ label: "Test Email Recipients",
463
+ admin: {
464
+ condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
465
+ description: "Send test emails before scheduling"
466
+ },
467
+ fields: [
468
+ {
469
+ name: "email",
470
+ type: "email",
471
+ required: true
472
+ }
473
+ ]
474
+ }
475
+ ]
476
+ }
477
+ ];
478
+ if (createMarkdownField) {
479
+ fields.push(createMarkdownFieldInternal({
480
+ name: `${contentField}Markdown`,
481
+ richTextField: contentField,
482
+ label: "Email Content (Markdown)",
483
+ admin: {
484
+ position: "sidebar",
485
+ condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
486
+ description: "Markdown version for email rendering",
487
+ readOnly: true
488
+ }
489
+ }));
490
+ }
491
+ return fields;
492
+ }
493
+ function createMarkdownFieldInternal(config) {
494
+ return {
495
+ name: config.name,
496
+ type: "textarea",
497
+ label: config.label || "Markdown",
498
+ admin: {
499
+ ...config.admin,
500
+ description: config.admin?.description || "Auto-generated from rich text content"
501
+ },
502
+ hooks: {
503
+ afterRead: [
504
+ async ({ data }) => {
505
+ if (data?.[config.richTextField]) {
506
+ try {
507
+ const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
508
+ return convertLexicalToMarkdown({
509
+ data: data[config.richTextField]
510
+ });
511
+ } catch {
512
+ return "";
513
+ }
514
+ }
515
+ return "";
516
+ }
517
+ ],
518
+ beforeChange: [
519
+ () => {
520
+ return null;
521
+ }
522
+ ]
523
+ }
524
+ };
525
+ }
165
526
  export {
527
+ createBroadcastInlinePreviewField,
166
528
  createEmailContentField,
167
- emailSafeFeatures
529
+ createEmailLexicalEditor,
530
+ createEmailSafeBlocks,
531
+ createEmailSafeFeatures,
532
+ createNewsletterSchedulingFields,
533
+ emailSafeFeatures,
534
+ validateEmailBlocks
168
535
  };
169
536
  //# sourceMappingURL=fields.js.map