payload-plugin-newsletter 0.26.0 → 0.28.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.
package/dist/admin.d.ts CHANGED
@@ -24,4 +24,43 @@ declare const EmailPreview: React.FC<EmailPreviewProps>;
24
24
 
25
25
  declare const WebhookConfiguration: React.FC;
26
26
 
27
- export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, EmailPreview, type EmailPreviewProps, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
27
+ /**
28
+ * Field component that shows scheduling controls based on broadcast status
29
+ * Shows Schedule button for drafts, Cancel button for scheduled broadcasts
30
+ */
31
+ declare const BroadcastScheduleField: React.FC;
32
+
33
+ interface BroadcastScheduleButtonProps {
34
+ broadcastId: string;
35
+ sendStatus?: string;
36
+ providerId?: string;
37
+ }
38
+ /**
39
+ * Button to open the schedule modal for a broadcast
40
+ * Only shows for draft broadcasts that have been synced to the provider
41
+ */
42
+ declare const BroadcastScheduleButton: React.FC<BroadcastScheduleButtonProps>;
43
+
44
+ interface CancelScheduleButtonProps {
45
+ broadcastId: string;
46
+ sendStatus?: string;
47
+ scheduledAt?: string;
48
+ }
49
+ /**
50
+ * Button to cancel a scheduled broadcast
51
+ * Only shows for broadcasts with 'scheduled' status
52
+ */
53
+ declare const CancelScheduleButton: React.FC<CancelScheduleButtonProps>;
54
+
55
+ interface ScheduleModalProps {
56
+ broadcastId: string;
57
+ onScheduled?: () => void;
58
+ onClose: () => void;
59
+ }
60
+ /**
61
+ * Modal for scheduling a broadcast
62
+ * Uses native HTML date-time inputs for compatibility
63
+ */
64
+ declare const ScheduleModal: React.FC<ScheduleModalProps>;
65
+
66
+ export { BroadcastInlinePreview, type BroadcastInlinePreviewProps, BroadcastScheduleButton, BroadcastScheduleField, CancelScheduleButton, EmailPreview, type EmailPreviewProps, ScheduleModal, StatusBadge, type StatusBadgeProps, WebhookConfiguration };
package/dist/admin.js CHANGED
@@ -332,9 +332,550 @@ var WebhookConfiguration = () => {
332
332
  ] })
333
333
  ] }) });
334
334
  };
335
+
336
+ // src/components/Broadcasts/BroadcastScheduleField.tsx
337
+ import { useDocumentInfo, useFormFields as useFormFields3 } from "@payloadcms/ui";
338
+
339
+ // src/components/Broadcasts/BroadcastScheduleButton.tsx
340
+ import { useState as useState4 } from "react";
341
+
342
+ // src/components/Broadcasts/ScheduleModal.tsx
343
+ import { useState as useState3, useCallback as useCallback2 } from "react";
344
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
345
+ var ScheduleModal = ({
346
+ broadcastId,
347
+ onScheduled,
348
+ onClose
349
+ }) => {
350
+ const [selectedDate, setSelectedDate] = useState3("");
351
+ const [selectedTime, setSelectedTime] = useState3("");
352
+ const [timezone, setTimezone] = useState3(
353
+ Intl.DateTimeFormat().resolvedOptions().timeZone
354
+ );
355
+ const [isLoading, setIsLoading] = useState3(false);
356
+ const [error, setError] = useState3(null);
357
+ const getMinDate = () => {
358
+ const today = /* @__PURE__ */ new Date();
359
+ return today.toISOString().split("T")[0];
360
+ };
361
+ const getMinTime = () => {
362
+ if (!selectedDate) return void 0;
363
+ const today = /* @__PURE__ */ new Date();
364
+ const selectedDateObj = new Date(selectedDate);
365
+ if (selectedDateObj.toDateString() === today.toDateString()) {
366
+ const minTime = new Date(today.getTime() + 5 * 60 * 1e3);
367
+ return `${String(minTime.getHours()).padStart(2, "0")}:${String(minTime.getMinutes()).padStart(2, "0")}`;
368
+ }
369
+ return void 0;
370
+ };
371
+ const handleSchedule = useCallback2(async () => {
372
+ if (!selectedDate || !selectedTime) {
373
+ setError("Please select both date and time");
374
+ return;
375
+ }
376
+ setIsLoading(true);
377
+ setError(null);
378
+ try {
379
+ const scheduledAt = /* @__PURE__ */ new Date(`${selectedDate}T${selectedTime}:00`);
380
+ if (scheduledAt <= /* @__PURE__ */ new Date()) {
381
+ setError("Scheduled time must be in the future");
382
+ setIsLoading(false);
383
+ return;
384
+ }
385
+ const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
386
+ method: "POST",
387
+ headers: { "Content-Type": "application/json" },
388
+ body: JSON.stringify({
389
+ scheduledAt: scheduledAt.toISOString(),
390
+ timezone
391
+ })
392
+ });
393
+ const data = await response.json();
394
+ if (!data.success) {
395
+ setError(data.error || "Failed to schedule broadcast");
396
+ return;
397
+ }
398
+ onScheduled?.();
399
+ onClose();
400
+ window.location.reload();
401
+ } catch (err) {
402
+ setError("Network error. Please try again.");
403
+ } finally {
404
+ setIsLoading(false);
405
+ }
406
+ }, [selectedDate, selectedTime, timezone, broadcastId, onScheduled, onClose]);
407
+ const commonTimezones = [
408
+ "America/New_York",
409
+ "America/Chicago",
410
+ "America/Denver",
411
+ "America/Los_Angeles",
412
+ "Europe/London",
413
+ "Europe/Paris",
414
+ "Asia/Tokyo",
415
+ "Australia/Sydney"
416
+ ];
417
+ return /* @__PURE__ */ jsx5(
418
+ "div",
419
+ {
420
+ style: {
421
+ position: "fixed",
422
+ top: 0,
423
+ left: 0,
424
+ right: 0,
425
+ bottom: 0,
426
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
427
+ display: "flex",
428
+ alignItems: "center",
429
+ justifyContent: "center",
430
+ zIndex: 1e4
431
+ },
432
+ onClick: (e) => {
433
+ if (e.target === e.currentTarget) onClose();
434
+ },
435
+ children: /* @__PURE__ */ jsxs4(
436
+ "div",
437
+ {
438
+ style: {
439
+ backgroundColor: "var(--theme-elevation-0, #fff)",
440
+ borderRadius: "8px",
441
+ padding: "24px",
442
+ maxWidth: "400px",
443
+ width: "100%",
444
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.15)"
445
+ },
446
+ children: [
447
+ /* @__PURE__ */ jsx5(
448
+ "h2",
449
+ {
450
+ style: {
451
+ margin: "0 0 16px 0",
452
+ fontSize: "18px",
453
+ fontWeight: "600",
454
+ color: "var(--theme-elevation-1000, #000)"
455
+ },
456
+ children: "Schedule Broadcast"
457
+ }
458
+ ),
459
+ error && /* @__PURE__ */ jsx5(
460
+ "div",
461
+ {
462
+ style: {
463
+ padding: "12px",
464
+ marginBottom: "16px",
465
+ backgroundColor: "var(--theme-error-100, #fef2f2)",
466
+ color: "var(--theme-error-500, #ef4444)",
467
+ borderRadius: "4px",
468
+ fontSize: "14px"
469
+ },
470
+ children: error
471
+ }
472
+ ),
473
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
474
+ /* @__PURE__ */ jsx5(
475
+ "label",
476
+ {
477
+ style: {
478
+ display: "block",
479
+ marginBottom: "4px",
480
+ fontSize: "14px",
481
+ fontWeight: "500",
482
+ color: "var(--theme-elevation-800, #333)"
483
+ },
484
+ children: "Date"
485
+ }
486
+ ),
487
+ /* @__PURE__ */ jsx5(
488
+ "input",
489
+ {
490
+ type: "date",
491
+ value: selectedDate,
492
+ onChange: (e) => setSelectedDate(e.target.value),
493
+ min: getMinDate(),
494
+ style: {
495
+ width: "100%",
496
+ padding: "8px 12px",
497
+ border: "1px solid var(--theme-elevation-150, #ddd)",
498
+ borderRadius: "4px",
499
+ fontSize: "14px",
500
+ backgroundColor: "var(--theme-input-bg, #fff)",
501
+ color: "var(--theme-elevation-1000, #000)"
502
+ }
503
+ }
504
+ )
505
+ ] }),
506
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "16px" }, children: [
507
+ /* @__PURE__ */ jsx5(
508
+ "label",
509
+ {
510
+ style: {
511
+ display: "block",
512
+ marginBottom: "4px",
513
+ fontSize: "14px",
514
+ fontWeight: "500",
515
+ color: "var(--theme-elevation-800, #333)"
516
+ },
517
+ children: "Time"
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx5(
521
+ "input",
522
+ {
523
+ type: "time",
524
+ value: selectedTime,
525
+ onChange: (e) => setSelectedTime(e.target.value),
526
+ min: getMinTime(),
527
+ style: {
528
+ width: "100%",
529
+ padding: "8px 12px",
530
+ border: "1px solid var(--theme-elevation-150, #ddd)",
531
+ borderRadius: "4px",
532
+ fontSize: "14px",
533
+ backgroundColor: "var(--theme-input-bg, #fff)",
534
+ color: "var(--theme-elevation-1000, #000)"
535
+ }
536
+ }
537
+ )
538
+ ] }),
539
+ /* @__PURE__ */ jsxs4("div", { style: { marginBottom: "24px" }, children: [
540
+ /* @__PURE__ */ jsx5(
541
+ "label",
542
+ {
543
+ style: {
544
+ display: "block",
545
+ marginBottom: "4px",
546
+ fontSize: "14px",
547
+ fontWeight: "500",
548
+ color: "var(--theme-elevation-800, #333)"
549
+ },
550
+ children: "Timezone"
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsxs4(
554
+ "select",
555
+ {
556
+ value: timezone,
557
+ onChange: (e) => setTimezone(e.target.value),
558
+ style: {
559
+ width: "100%",
560
+ padding: "8px 12px",
561
+ border: "1px solid var(--theme-elevation-150, #ddd)",
562
+ borderRadius: "4px",
563
+ fontSize: "14px",
564
+ backgroundColor: "var(--theme-input-bg, #fff)",
565
+ color: "var(--theme-elevation-1000, #000)"
566
+ },
567
+ children: [
568
+ commonTimezones.includes(timezone) ? null : /* @__PURE__ */ jsx5("option", { value: timezone, children: timezone }),
569
+ commonTimezones.map((tz) => /* @__PURE__ */ jsx5("option", { value: tz, children: tz.replace("_", " ") }, tz))
570
+ ]
571
+ }
572
+ )
573
+ ] }),
574
+ /* @__PURE__ */ jsxs4(
575
+ "div",
576
+ {
577
+ style: {
578
+ display: "flex",
579
+ gap: "12px",
580
+ justifyContent: "flex-end"
581
+ },
582
+ children: [
583
+ /* @__PURE__ */ jsx5(
584
+ "button",
585
+ {
586
+ onClick: onClose,
587
+ disabled: isLoading,
588
+ style: {
589
+ padding: "8px 16px",
590
+ border: "1px solid var(--theme-elevation-150, #ddd)",
591
+ borderRadius: "4px",
592
+ backgroundColor: "transparent",
593
+ color: "var(--theme-elevation-800, #333)",
594
+ fontSize: "14px",
595
+ cursor: "pointer"
596
+ },
597
+ children: "Cancel"
598
+ }
599
+ ),
600
+ /* @__PURE__ */ jsx5(
601
+ "button",
602
+ {
603
+ onClick: handleSchedule,
604
+ disabled: isLoading || !selectedDate || !selectedTime,
605
+ style: {
606
+ padding: "8px 16px",
607
+ border: "none",
608
+ borderRadius: "4px",
609
+ backgroundColor: isLoading || !selectedDate || !selectedTime ? "var(--theme-elevation-200, #ccc)" : "var(--theme-success-500, #22c55e)",
610
+ color: "#fff",
611
+ fontSize: "14px",
612
+ fontWeight: "500",
613
+ cursor: isLoading || !selectedDate || !selectedTime ? "not-allowed" : "pointer"
614
+ },
615
+ children: isLoading ? "Scheduling..." : "Schedule"
616
+ }
617
+ )
618
+ ]
619
+ }
620
+ )
621
+ ]
622
+ }
623
+ )
624
+ }
625
+ );
626
+ };
627
+
628
+ // src/components/Broadcasts/BroadcastScheduleButton.tsx
629
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
630
+ var BroadcastScheduleButton = ({
631
+ broadcastId,
632
+ sendStatus = "draft",
633
+ providerId
634
+ }) => {
635
+ const [showModal, setShowModal] = useState4(false);
636
+ if (sendStatus !== "draft") {
637
+ return null;
638
+ }
639
+ if (!providerId) {
640
+ return /* @__PURE__ */ jsxs5(
641
+ "button",
642
+ {
643
+ disabled: true,
644
+ title: "Save the broadcast first to sync with the email provider",
645
+ style: {
646
+ padding: "8px 16px",
647
+ border: "none",
648
+ borderRadius: "4px",
649
+ backgroundColor: "var(--theme-elevation-200, #ccc)",
650
+ color: "var(--theme-elevation-500, #666)",
651
+ fontSize: "14px",
652
+ fontWeight: "500",
653
+ cursor: "not-allowed",
654
+ display: "inline-flex",
655
+ alignItems: "center",
656
+ gap: "6px"
657
+ },
658
+ children: [
659
+ /* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
660
+ "Schedule (save first)"
661
+ ]
662
+ }
663
+ );
664
+ }
665
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
666
+ /* @__PURE__ */ jsxs5(
667
+ "button",
668
+ {
669
+ onClick: () => setShowModal(true),
670
+ style: {
671
+ padding: "8px 16px",
672
+ border: "none",
673
+ borderRadius: "4px",
674
+ backgroundColor: "var(--theme-success-500, #22c55e)",
675
+ color: "#fff",
676
+ fontSize: "14px",
677
+ fontWeight: "500",
678
+ cursor: "pointer",
679
+ display: "inline-flex",
680
+ alignItems: "center",
681
+ gap: "6px"
682
+ },
683
+ children: [
684
+ /* @__PURE__ */ jsx6("span", { children: "\u{1F4C5}" }),
685
+ "Schedule Send"
686
+ ]
687
+ }
688
+ ),
689
+ showModal && /* @__PURE__ */ jsx6(
690
+ ScheduleModal,
691
+ {
692
+ broadcastId,
693
+ onClose: () => setShowModal(false)
694
+ }
695
+ )
696
+ ] });
697
+ };
698
+
699
+ // src/components/Broadcasts/CancelScheduleButton.tsx
700
+ import { useState as useState5, useCallback as useCallback3 } from "react";
701
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
702
+ var CancelScheduleButton = ({
703
+ broadcastId,
704
+ sendStatus = "draft",
705
+ scheduledAt
706
+ }) => {
707
+ const [isLoading, setIsLoading] = useState5(false);
708
+ if (sendStatus !== "scheduled") {
709
+ return null;
710
+ }
711
+ const handleCancel = useCallback3(async () => {
712
+ const formattedDate = scheduledAt ? new Date(scheduledAt).toLocaleString() : "unknown time";
713
+ const confirmed = window.confirm(
714
+ `Are you sure you want to cancel this scheduled broadcast?
715
+
716
+ It was scheduled for: ${formattedDate}
717
+
718
+ The broadcast will be returned to draft status.`
719
+ );
720
+ if (!confirmed) return;
721
+ setIsLoading(true);
722
+ try {
723
+ const response = await fetch(`/api/broadcasts/${broadcastId}/schedule`, {
724
+ method: "DELETE"
725
+ });
726
+ const data = await response.json();
727
+ if (!data.success) {
728
+ alert(data.error || "Failed to cancel schedule");
729
+ return;
730
+ }
731
+ window.location.reload();
732
+ } catch (err) {
733
+ alert("Network error. Please try again.");
734
+ } finally {
735
+ setIsLoading(false);
736
+ }
737
+ }, [broadcastId, scheduledAt]);
738
+ const formattedScheduledAt = scheduledAt ? new Date(scheduledAt).toLocaleString() : null;
739
+ return /* @__PURE__ */ jsxs6(
740
+ "div",
741
+ {
742
+ style: {
743
+ display: "flex",
744
+ alignItems: "center",
745
+ gap: "16px"
746
+ },
747
+ children: [
748
+ formattedScheduledAt && /* @__PURE__ */ jsxs6(
749
+ "span",
750
+ {
751
+ style: {
752
+ fontSize: "14px",
753
+ color: "var(--theme-elevation-600, #666)"
754
+ },
755
+ children: [
756
+ "Scheduled for: ",
757
+ /* @__PURE__ */ jsx7("strong", { children: formattedScheduledAt })
758
+ ]
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsxs6(
762
+ "button",
763
+ {
764
+ onClick: handleCancel,
765
+ disabled: isLoading,
766
+ style: {
767
+ padding: "8px 16px",
768
+ border: "1px solid var(--theme-error-500, #ef4444)",
769
+ borderRadius: "4px",
770
+ backgroundColor: "transparent",
771
+ color: "var(--theme-error-500, #ef4444)",
772
+ fontSize: "14px",
773
+ fontWeight: "500",
774
+ cursor: isLoading ? "not-allowed" : "pointer",
775
+ display: "inline-flex",
776
+ alignItems: "center",
777
+ gap: "6px",
778
+ opacity: isLoading ? 0.6 : 1
779
+ },
780
+ children: [
781
+ /* @__PURE__ */ jsx7("span", { children: "\u2715" }),
782
+ isLoading ? "Cancelling..." : "Cancel Schedule"
783
+ ]
784
+ }
785
+ )
786
+ ]
787
+ }
788
+ );
789
+ };
790
+
791
+ // src/components/Broadcasts/BroadcastScheduleField.tsx
792
+ import { jsx as jsx8 } from "react/jsx-runtime";
793
+ var BroadcastScheduleField = () => {
794
+ const { id } = useDocumentInfo();
795
+ const sendStatusField = useFormFields3(([fields]) => fields.sendStatus);
796
+ const providerIdField = useFormFields3(([fields]) => fields.providerId);
797
+ const scheduledAtField = useFormFields3(([fields]) => fields.scheduledAt);
798
+ const sendStatus = sendStatusField?.value;
799
+ const providerId = providerIdField?.value;
800
+ const scheduledAt = scheduledAtField?.value;
801
+ if (!id) {
802
+ return /* @__PURE__ */ jsx8(
803
+ "div",
804
+ {
805
+ style: {
806
+ padding: "16px",
807
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
808
+ borderRadius: "4px",
809
+ fontSize: "14px",
810
+ color: "var(--theme-elevation-600, #666)"
811
+ },
812
+ children: "Save the broadcast to enable scheduling options."
813
+ }
814
+ );
815
+ }
816
+ if (sendStatus === "sent" || sendStatus === "sending") {
817
+ return /* @__PURE__ */ jsx8(
818
+ "div",
819
+ {
820
+ style: {
821
+ padding: "16px",
822
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
823
+ borderRadius: "4px",
824
+ fontSize: "14px",
825
+ color: "var(--theme-elevation-600, #666)"
826
+ },
827
+ children: sendStatus === "sent" ? "This broadcast has been sent and cannot be rescheduled." : "This broadcast is currently being sent."
828
+ }
829
+ );
830
+ }
831
+ if (sendStatus === "failed") {
832
+ return /* @__PURE__ */ jsx8(
833
+ "div",
834
+ {
835
+ style: {
836
+ padding: "16px",
837
+ backgroundColor: "var(--theme-error-100, #fef2f2)",
838
+ borderRadius: "4px",
839
+ fontSize: "14px",
840
+ color: "var(--theme-error-600, #dc2626)"
841
+ },
842
+ children: "This broadcast failed to send. Edit and save to return it to draft status."
843
+ }
844
+ );
845
+ }
846
+ return /* @__PURE__ */ jsx8(
847
+ "div",
848
+ {
849
+ style: {
850
+ padding: "16px",
851
+ backgroundColor: "var(--theme-elevation-50, #f9f9f9)",
852
+ borderRadius: "4px"
853
+ },
854
+ children: sendStatus === "scheduled" ? /* @__PURE__ */ jsx8(
855
+ CancelScheduleButton,
856
+ {
857
+ broadcastId: String(id),
858
+ sendStatus,
859
+ scheduledAt
860
+ }
861
+ ) : /* @__PURE__ */ jsx8(
862
+ BroadcastScheduleButton,
863
+ {
864
+ broadcastId: String(id),
865
+ sendStatus,
866
+ providerId
867
+ }
868
+ )
869
+ }
870
+ );
871
+ };
335
872
  export {
336
873
  BroadcastInlinePreview,
874
+ BroadcastScheduleButton,
875
+ BroadcastScheduleField,
876
+ CancelScheduleButton,
337
877
  EmailPreview,
878
+ ScheduleModal,
338
879
  StatusBadge,
339
880
  WebhookConfiguration
340
881
  };
@@ -886,6 +886,20 @@ var createBroadcastInlinePreviewField = () => {
886
886
  };
887
887
  };
888
888
 
889
+ // src/fields/broadcastSchedule.ts
890
+ var createBroadcastScheduleField = () => {
891
+ return {
892
+ name: "scheduleControls",
893
+ type: "ui",
894
+ label: "Scheduling",
895
+ admin: {
896
+ components: {
897
+ Field: "payload-plugin-newsletter/components#BroadcastScheduleField"
898
+ }
899
+ }
900
+ };
901
+ };
902
+
889
903
  // src/utils/emailSafeHtml.ts
890
904
  var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
891
905
  var EMAIL_SAFE_CONFIG = {
@@ -2080,6 +2094,8 @@ var createBroadcastsCollection = (pluginConfig) => {
2080
2094
  }
2081
2095
  }
2082
2096
  },
2097
+ // Scheduling controls - shows schedule/cancel buttons based on status
2098
+ createBroadcastScheduleField(),
2083
2099
  {
2084
2100
  name: "settings",
2085
2101
  type: "group",