strapi-plugin-magic-mail 3.1.0 → 4.0.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +630 -1154
  3. package/dist/admin/App-Bi6mzEff.mjs +10973 -0
  4. package/dist/admin/App-aaEU9cpu.js +10976 -0
  5. package/dist/admin/PluginSettings-BR2-PVHx.mjs +705 -0
  6. package/dist/admin/PluginSettings-DbTkOzCh.js +707 -0
  7. package/dist/admin/analyticsLogs-BCY0Zv3Y.mjs +685 -0
  8. package/dist/admin/analyticsLogs-DNU09hoB.js +686 -0
  9. package/dist/admin/de-B5bPRK6l.js +717 -0
  10. package/dist/admin/de-BqYqMhOn.mjs +717 -0
  11. package/dist/admin/en-8xANOQdO.mjs +717 -0
  12. package/dist/admin/en-Bk5eVrtc.js +717 -0
  13. package/dist/admin/es-BfPsCsam.js +717 -0
  14. package/dist/admin/es-DgpTufJ_.mjs +717 -0
  15. package/dist/admin/fr-BuotP0Gc.mjs +717 -0
  16. package/dist/admin/fr-IQ3KPUoS.js +717 -0
  17. package/dist/admin/index-QGqbGw6k.js +102 -0
  18. package/dist/admin/index-qvFJ-a0Q.mjs +103 -0
  19. package/dist/admin/index.js +2 -99
  20. package/dist/admin/index.mjs +2 -99
  21. package/dist/admin/pt-BI63Lncs.js +717 -0
  22. package/dist/admin/pt-DBZu2cDY.mjs +717 -0
  23. package/dist/server/index.js +4623 -2214
  24. package/dist/server/index.mjs +4623 -2214
  25. package/package.json +30 -19
  26. package/strapi-admin.js +3 -2
  27. package/strapi-server.js +3 -2
  28. package/dist/admin/App-Bmav09Ol.mjs +0 -7156
  29. package/dist/admin/App-mX8QtU1Q.js +0 -7177
  30. package/dist/admin/PluginSettings-D5F5y7n2.mjs +0 -386
  31. package/dist/admin/PluginSettings-DE2PMxlS.js +0 -388
  32. package/dist/admin/StyledButtons-aq1EWrQ-.mjs +0 -433
  33. package/dist/admin/StyledButtons-jHF7TXal.js +0 -434
  34. package/dist/admin/de-BXyjQhJQ.mjs +0 -43
  35. package/dist/admin/de-VtWzTjWV.js +0 -43
  36. package/dist/admin/en-DL_aZYfg.js +0 -43
  37. package/dist/admin/en-DMdnprlB.mjs +0 -43
  38. package/dist/admin/es-BJ1KgkMA.js +0 -43
  39. package/dist/admin/es-g3CHX4iu.mjs +0 -43
  40. package/dist/admin/fr-Bmh5-45j.js +0 -43
  41. package/dist/admin/fr-CN1X3dM-.mjs +0 -43
  42. package/dist/admin/pt-DJ6GNCKb.js +0 -43
  43. package/dist/admin/pt-m8_Gnsqe.mjs +0 -43
  44. package/pics/EmailAccountDashboard.png +0 -0
  45. package/pics/Emailaddccount1.png +0 -0
  46. package/pics/RoutingEmailTemplates.png +0 -0
  47. package/pics/Templates.png +0 -0
  48. package/pics/codesnippet.png +0 -0
  49. package/pics/emailAnalytics.png +0 -0
  50. package/pics/emailDesigner.png +0 -0
  51. package/pics/emailTemplateSendTest.png +0 -0
  52. package/pics/emailVersionHistory.png +0 -0
  53. package/pics/emailaddaccount2.png +0 -0
  54. package/pics/emailaddaccount3.png +0 -0
@@ -0,0 +1,686 @@
1
+ "use strict";
2
+ const admin = require("@strapi/strapi/admin");
3
+ const index = require("./index-QGqbGw6k.js");
4
+ const jsxRuntime = require("react/jsx-runtime");
5
+ const React = require("react");
6
+ const designSystem = require("@strapi/design-system");
7
+ const outline = require("@heroicons/react/24/outline");
8
+ const styled = require("styled-components");
9
+ const reactRouterDom = require("react-router-dom");
10
+ const reactIntl = require("react-intl");
11
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
+ const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
+ const ROUTE_CAPABILITIES = [
14
+ ...index.pluginPermissions.read,
15
+ ...index.pluginPermissions.manage,
16
+ ...index.pluginPermissions.testSend
17
+ ];
18
+ const useMagicMailRBAC = () => {
19
+ const { allowedActions = {}, isLoading = false, error } = admin.useRBAC(ROUTE_CAPABILITIES) ?? {};
20
+ const settled = !isLoading && !error;
21
+ const allowed = (key) => settled && allowedActions?.[key] === true;
22
+ const canRead = allowed("canRead");
23
+ const canManage = allowed("canManage");
24
+ return {
25
+ canRead,
26
+ canManage,
27
+ canTestSend: allowed("canTestSend"),
28
+ isReadOnly: canRead && !canManage,
29
+ isLoading: Boolean(isLoading),
30
+ error,
31
+ permissions: index.pluginPermissions
32
+ };
33
+ };
34
+ const useMagicMailIntl = () => {
35
+ const intl = reactIntl.useIntl();
36
+ const t = React.useCallback(
37
+ (key, defaultMessage, values) => intl.formatMessage({ id: index.getTranslation(key), defaultMessage }, values),
38
+ [intl]
39
+ );
40
+ return { t, formatMessage: intl.formatMessage, locale: intl.locale };
41
+ };
42
+ const LIGHT_FALLBACK_COLORS = Object.freeze({
43
+ alternative100: "#f6ecfc",
44
+ alternative200: "#e0c1f4",
45
+ alternative500: "#ac73e6",
46
+ alternative600: "#9736e8",
47
+ alternative700: "#8312d1",
48
+ neutral0: "#ffffff",
49
+ neutral100: "#f6f6f9",
50
+ neutral150: "#eaeaef",
51
+ neutral200: "#dcdce4",
52
+ neutral300: "#c0c0cf",
53
+ neutral400: "#a5a5ba",
54
+ neutral500: "#8e8ea9",
55
+ neutral600: "#666687",
56
+ neutral700: "#4a4a6a",
57
+ neutral800: "#32324d",
58
+ neutral900: "#212134",
59
+ neutral1000: "#181826",
60
+ primary100: "#f0f0ff",
61
+ primary200: "#d9d8ff",
62
+ primary500: "#7b79ff",
63
+ primary600: "#4945ff",
64
+ primary700: "#271fe0",
65
+ secondary100: "#eaf5ff",
66
+ secondary200: "#b8e1ff",
67
+ secondary500: "#66b7f1",
68
+ secondary600: "#0c75af",
69
+ secondary700: "#006096",
70
+ success100: "#eafbe7",
71
+ success200: "#c6f0c2",
72
+ success500: "#5cb176",
73
+ success600: "#328048",
74
+ success700: "#2f6846",
75
+ warning100: "#fdf4dc",
76
+ warning200: "#fae7b9",
77
+ warning500: "#f29d41",
78
+ warning600: "#d9822f",
79
+ warning700: "#be5d01",
80
+ danger100: "#fcecea",
81
+ danger200: "#f5c0b8",
82
+ danger500: "#ee5e52",
83
+ danger600: "#d02b20",
84
+ danger700: "#b72b1a"
85
+ });
86
+ const FONT_SIZE_FALLBACK = Object.freeze([
87
+ "1.1rem",
88
+ // 0 -> 11px
89
+ "1.2rem",
90
+ // 1 -> 12px
91
+ "1.4rem",
92
+ // 2 -> 14px
93
+ "1.6rem",
94
+ // 3 -> 16px
95
+ "1.8rem",
96
+ // 4 -> 18px
97
+ "2rem",
98
+ // 5 -> 20px
99
+ "2.8rem",
100
+ // 6 -> 28px
101
+ "3.2rem"
102
+ // 7 -> 32px
103
+ ]);
104
+ const color = (token) => (props) => props?.theme?.colors?.[token] ?? LIGHT_FALLBACK_COLORS[token];
105
+ const fontSize = (step) => (props) => props?.theme?.fontSizes?.[step] ?? FONT_SIZE_FALLBACK[step];
106
+ const isDarkTheme = (themeObject) => themeObject?.colorScheme === "dark";
107
+ const byScheme = (light, dark) => (props) => isDarkTheme(props?.theme) ? dark : light;
108
+ const brand = Object.freeze({
109
+ primary: "#0369A1",
110
+ primaryHover: "#075985",
111
+ secondary: "#9333EA",
112
+ secondaryHover: "#7E22CE",
113
+ success: "#15803D",
114
+ successHover: "#166534",
115
+ warning: "#B45309",
116
+ warningHover: "#92400E",
117
+ danger: "#DC2626",
118
+ dangerHover: "#B91C1C",
119
+ gradient: "linear-gradient(135deg, #0369A1 0%, #9333EA 100%)",
120
+ gradientHover: "linear-gradient(135deg, #075985 0%, #7E22CE 100%)"
121
+ });
122
+ const ACCENT_VALUES = Object.freeze({
123
+ primary: ["#0369A1", "#7DD3FC"],
124
+ secondary: ["#7E22CE", "#D8B4FE"],
125
+ success: ["#15803D", "#86EFAC"],
126
+ warning: ["#B45309", "#FCD34D"],
127
+ danger: ["#B91C1C", "#FCA5A5"]
128
+ });
129
+ const accent = Object.freeze(
130
+ Object.fromEntries(
131
+ Object.entries(ACCENT_VALUES).map(([name, [light, dark]]) => [name, byScheme(light, dark)])
132
+ )
133
+ );
134
+ const theme = {
135
+ shadows: {
136
+ sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
137
+ md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
138
+ lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
139
+ xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)"
140
+ },
141
+ transitions: {
142
+ fast: "150ms cubic-bezier(0.4, 0, 0.2, 1)",
143
+ normal: "300ms cubic-bezier(0.4, 0, 0.2, 1)",
144
+ slow: "500ms cubic-bezier(0.4, 0, 0.2, 1)"
145
+ },
146
+ spacing: {
147
+ xs: "4px",
148
+ sm: "8px",
149
+ md: "16px",
150
+ lg: "24px",
151
+ xl: "32px",
152
+ "2xl": "48px"
153
+ },
154
+ borderRadius: {
155
+ md: "8px",
156
+ lg: "12px",
157
+ xl: "16px"
158
+ }
159
+ };
160
+ const reducedMotion = styled.css`
161
+ @media (prefers-reduced-motion: reduce) {
162
+ animation: none !important;
163
+ transition: none !important;
164
+
165
+ &::before,
166
+ &::after {
167
+ animation: none !important;
168
+ transition: none !important;
169
+ }
170
+
171
+ &:hover,
172
+ &:active,
173
+ &:focus-visible {
174
+ transform: none !important;
175
+ }
176
+ }
177
+ `;
178
+ const motionSafe = (block) => styled.css`
179
+ @media (prefers-reduced-motion: no-preference) {
180
+ ${block}
181
+ }
182
+ `;
183
+ const CONTROL_HEIGHT = "36px";
184
+ const CTA_HEIGHT = "52px";
185
+ const disabledState = styled.css`
186
+ &:disabled,
187
+ &[aria-disabled='true'] {
188
+ opacity: 0.6;
189
+ cursor: not-allowed;
190
+ }
191
+ `;
192
+ const textButtonBase = styled.css`
193
+ font-weight: 600;
194
+ padding: 8px 16px;
195
+ min-height: ${CONTROL_HEIGHT};
196
+ transition: all 0.2s ease;
197
+ ${disabledState}
198
+ ${reducedMotion}
199
+ `;
200
+ const GradientButton = styled__default.default(designSystem.Button)`
201
+ && {
202
+ ${textButtonBase}
203
+ background: ${brand.gradient};
204
+ color: #ffffff;
205
+ border: none;
206
+
207
+ &:hover:not(:disabled) {
208
+ background: ${brand.gradientHover};
209
+ transform: translateY(-1px);
210
+ box-shadow: 0 4px 12px rgba(3, 105, 161, 0.3);
211
+ }
212
+
213
+ &:active:not(:disabled) {
214
+ transform: translateY(0);
215
+ }
216
+ }
217
+ `;
218
+ const SecondaryButton = styled__default.default(designSystem.Button)`
219
+ && {
220
+ ${textButtonBase}
221
+ background: ${color("neutral0")};
222
+ color: ${accent.secondary};
223
+ border: 2px solid transparent;
224
+ background-image:
225
+ linear-gradient(${color("neutral0")}, ${color("neutral0")}), ${brand.gradient};
226
+ background-origin: border-box;
227
+ background-clip: padding-box, border-box;
228
+
229
+ &:hover:not(:disabled) {
230
+ background: ${brand.gradient};
231
+ background-clip: padding-box;
232
+ color: #ffffff;
233
+ transform: translateY(-1px);
234
+ box-shadow: 0 4px 12px rgba(147, 51, 234, 0.3);
235
+ }
236
+
237
+ &:active:not(:disabled) {
238
+ transform: translateY(0);
239
+ }
240
+ }
241
+ `;
242
+ const TertiaryButton = styled__default.default(designSystem.Button)`
243
+ && {
244
+ ${textButtonBase}
245
+ font-weight: 500;
246
+ background: transparent;
247
+ color: ${color("neutral700")};
248
+ border: 1px solid ${color("neutral200")};
249
+
250
+ &:hover:not(:disabled) {
251
+ background: ${color("neutral100")};
252
+ border-color: ${color("neutral300")};
253
+ color: ${color("neutral800")};
254
+ }
255
+ }
256
+ `;
257
+ const DangerButton = styled__default.default(designSystem.Button)`
258
+ && {
259
+ ${textButtonBase}
260
+ background: rgba(220, 38, 38, 0.12);
261
+ color: ${accent.danger};
262
+ border: 1px solid rgba(220, 38, 38, 0.3);
263
+
264
+ &:hover:not(:disabled) {
265
+ background: ${brand.danger};
266
+ color: #ffffff;
267
+ border-color: ${brand.danger};
268
+ }
269
+ }
270
+ `;
271
+ const iconButtonBase = styled.css`
272
+ padding: 8px;
273
+ min-width: ${CONTROL_HEIGHT};
274
+ min-height: ${CONTROL_HEIGHT};
275
+ width: ${CONTROL_HEIGHT};
276
+ height: ${CONTROL_HEIGHT};
277
+ border-radius: 10px;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ transition: all 0.2s ease;
282
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
283
+
284
+ svg {
285
+ width: 18px;
286
+ height: 18px;
287
+ }
288
+
289
+ &:hover:not(:disabled) {
290
+ color: #ffffff;
291
+ transform: translateY(-1px);
292
+ }
293
+
294
+ ${disabledState}
295
+ ${reducedMotion}
296
+ `;
297
+ const createIconButton = ({ tint, fg, hover, hoverBorder }) => {
298
+ const StyledIconButton = styled__default.default(designSystem.Button)`
299
+ && {
300
+ ${iconButtonBase}
301
+ background: linear-gradient(135deg, rgba(${tint}, 0.06) 0%, rgba(${tint}, 0.14) 100%);
302
+ color: ${fg};
303
+ border: 1px solid rgba(${tint}, 0.32);
304
+
305
+ &:hover:not(:disabled) {
306
+ background: ${hover};
307
+ border-color: ${hoverBorder};
308
+ box-shadow: 0 4px 12px rgba(${tint}, 0.3);
309
+ }
310
+ }
311
+ `;
312
+ const IconOnlyButton = React.forwardRef(({ title, ...props }, ref) => {
313
+ const label = props["aria-label"] ?? title;
314
+ const button = /* @__PURE__ */ jsxRuntime.jsx(StyledIconButton, { ref, "aria-label": label, ...props });
315
+ return label ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tooltip, { label, children: button }) : button;
316
+ });
317
+ return IconOnlyButton;
318
+ };
319
+ const IconButton = createIconButton({
320
+ tint: "128, 128, 128",
321
+ fg: color("neutral600"),
322
+ hover: `linear-gradient(135deg, ${brand.primary} 0%, ${brand.primaryHover} 100%)`,
323
+ hoverBorder: brand.primary
324
+ });
325
+ IconButton.displayName = "IconButton";
326
+ const IconButtonDanger = createIconButton({
327
+ tint: "220, 38, 38",
328
+ fg: accent.danger,
329
+ hover: `linear-gradient(135deg, ${brand.danger} 0%, ${brand.dangerHover} 100%)`,
330
+ hoverBorder: brand.danger
331
+ });
332
+ IconButtonDanger.displayName = "IconButtonDanger";
333
+ const IconButtonPrimary = createIconButton({
334
+ tint: "3, 105, 161",
335
+ fg: accent.primary,
336
+ hover: `linear-gradient(135deg, ${brand.primary} 0%, ${brand.primaryHover} 100%)`,
337
+ hoverBorder: brand.primary
338
+ });
339
+ IconButtonPrimary.displayName = "IconButtonPrimary";
340
+ const IconButtonSuccess = createIconButton({
341
+ tint: "21, 128, 61",
342
+ fg: accent.success,
343
+ hover: `linear-gradient(135deg, ${brand.success} 0%, ${brand.successHover} 100%)`,
344
+ hoverBorder: brand.success
345
+ });
346
+ IconButtonSuccess.displayName = "IconButtonSuccess";
347
+ const IconButtonWarning = createIconButton({
348
+ tint: "180, 83, 9",
349
+ fg: accent.warning,
350
+ hover: `linear-gradient(135deg, ${brand.warning} 0%, ${brand.warningHover} 100%)`,
351
+ hoverBorder: brand.warning
352
+ });
353
+ IconButtonWarning.displayName = "IconButtonWarning";
354
+ const IconButtonPurple = createIconButton({
355
+ tint: "147, 51, 234",
356
+ fg: accent.secondary,
357
+ hover: `linear-gradient(135deg, ${brand.secondary} 0%, ${brand.secondaryHover} 100%)`,
358
+ hoverBorder: brand.secondary
359
+ });
360
+ IconButtonPurple.displayName = "IconButtonPurple";
361
+ const CTAButton = styled__default.default(designSystem.Button)`
362
+ && {
363
+ background: ${brand.gradient};
364
+ color: #ffffff;
365
+ font-weight: 700;
366
+ font-size: ${fontSize(3)};
367
+ border: none;
368
+ padding: 14px 28px;
369
+ min-height: ${CTA_HEIGHT};
370
+ border-radius: 12px;
371
+ transition: all 0.2s ease;
372
+ box-shadow: 0 4px 14px rgba(3, 105, 161, 0.25);
373
+
374
+ &:hover:not(:disabled) {
375
+ background: ${brand.gradientHover};
376
+ transform: translateY(-2px);
377
+ box-shadow: 0 6px 20px rgba(3, 105, 161, 0.35);
378
+ }
379
+
380
+ &:active:not(:disabled) {
381
+ transform: translateY(0);
382
+ }
383
+
384
+ ${disabledState}
385
+ ${reducedMotion}
386
+ }
387
+ `;
388
+ const useReturnFocus = (isOpen, getFallback) => {
389
+ const openerRef = React.useRef(null);
390
+ const fallbackRef = React.useRef(getFallback);
391
+ React.useLayoutEffect(() => {
392
+ fallbackRef.current = getFallback;
393
+ }, [getFallback]);
394
+ React.useLayoutEffect(() => {
395
+ if (!isOpen) return void 0;
396
+ const active = document.activeElement;
397
+ const opener = active && active !== document.body ? active : null;
398
+ openerRef.current = opener;
399
+ return () => {
400
+ window.setTimeout(() => {
401
+ const current = document.activeElement;
402
+ if (current && current !== document.body) return;
403
+ const target = opener?.isConnected ? opener : fallbackRef.current?.();
404
+ target?.focus?.();
405
+ }, 0);
406
+ };
407
+ }, [isOpen]);
408
+ return React.useCallback((event) => {
409
+ const opener = openerRef.current;
410
+ openerRef.current = null;
411
+ const target = opener?.isConnected ? opener : fallbackRef.current?.();
412
+ if (!target || typeof target.focus !== "function") return;
413
+ event.preventDefault();
414
+ target.focus();
415
+ }, []);
416
+ };
417
+ const WarningIcon = styled__default.default(outline.ExclamationTriangleIcon)`
418
+ width: 24px;
419
+ height: 24px;
420
+ color: ${(p) => p.$danger ? accent.danger(p) : accent.warning(p)};
421
+ `;
422
+ const ConfirmDialog = ({
423
+ isOpen,
424
+ onClose,
425
+ onConfirm,
426
+ title,
427
+ description,
428
+ children,
429
+ confirmLabel,
430
+ cancelLabel,
431
+ variant = "danger",
432
+ isLoading = false,
433
+ confirmIcon,
434
+ getReturnFocusFallback
435
+ }) => {
436
+ const { t } = useMagicMailIntl();
437
+ const [isPending, setIsPending] = React.useState(false);
438
+ const mounted = React.useRef(true);
439
+ const returnFocus = useReturnFocus(Boolean(isOpen), getReturnFocusFallback);
440
+ React.useEffect(() => {
441
+ mounted.current = true;
442
+ return () => {
443
+ mounted.current = false;
444
+ };
445
+ }, []);
446
+ const busy = isLoading || isPending;
447
+ const danger = variant === "danger";
448
+ const ConfirmButton = danger ? DangerButton : GradientButton;
449
+ const handleConfirm = async () => {
450
+ if (busy) return;
451
+ const result = onConfirm?.();
452
+ if (result && typeof result.then === "function") {
453
+ setIsPending(true);
454
+ try {
455
+ await result;
456
+ } catch {
457
+ } finally {
458
+ if (mounted.current) setIsPending(false);
459
+ }
460
+ }
461
+ };
462
+ const handleOpenChange = (open) => {
463
+ if (!open && !busy) onClose?.();
464
+ };
465
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Root, { open: Boolean(isOpen), onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { onCloseAutoFocus: returnFocus, children: [
466
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Header, { textAlign: "center", children: title }),
467
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { icon: /* @__PURE__ */ jsxRuntime.jsx(WarningIcon, { "aria-hidden": "true", $danger: danger }), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, width: "100%", alignItems: "stretch", children: [
468
+ description ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Description, { textAlign: "center", children: description }) : null,
469
+ typeof children === "string" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", textAlign: "center", children }) : children
470
+ ] }) }),
471
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
472
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(TertiaryButton, { fullWidth: true, disabled: busy, children: cancelLabel ?? t("common.cancel", "Cancel") }) }),
473
+ /* @__PURE__ */ jsxRuntime.jsx(
474
+ ConfirmButton,
475
+ {
476
+ fullWidth: true,
477
+ onClick: handleConfirm,
478
+ loading: busy,
479
+ startIcon: busy ? void 0 : confirmIcon,
480
+ children: confirmLabel ?? (danger ? t("common.delete", "Delete") : t("common.confirm", "Confirm"))
481
+ }
482
+ )
483
+ ] })
484
+ ] }) });
485
+ };
486
+ const UnsavedChangesDialog = ({ guard, title, description, confirmLabel }) => {
487
+ const { t } = useMagicMailIntl();
488
+ return /* @__PURE__ */ jsxRuntime.jsx(
489
+ ConfirmDialog,
490
+ {
491
+ isOpen: guard.isOpen,
492
+ onClose: guard.cancel,
493
+ onConfirm: guard.confirm,
494
+ title: title ?? t("common.unsaved.title", "Discard unsaved changes?"),
495
+ description: description ?? t(
496
+ "common.unsaved.description",
497
+ "You have unsaved changes. If you continue, they will be lost."
498
+ ),
499
+ confirmLabel: confirmLabel ?? t("common.unsaved.confirm", "Discard changes"),
500
+ cancelLabel: t("common.unsaved.cancel", "Keep editing")
501
+ }
502
+ );
503
+ };
504
+ const shouldBlockNavigation = (isDirty, transition = {}) => {
505
+ if (!isDirty) return false;
506
+ const { currentLocation, nextLocation } = transition;
507
+ if (!nextLocation) return false;
508
+ return currentLocation?.pathname !== nextLocation.pathname || (currentLocation?.search ?? "") !== (nextLocation.search ?? "");
509
+ };
510
+ const attachBeforeUnload = (active, win = typeof window === "undefined" ? void 0 : window) => {
511
+ if (!active || typeof win?.addEventListener !== "function") return () => {
512
+ };
513
+ const handler = (event) => {
514
+ event.preventDefault();
515
+ event.returnValue = "";
516
+ return "";
517
+ };
518
+ win.addEventListener("beforeunload", handler);
519
+ return () => win.removeEventListener("beforeunload", handler);
520
+ };
521
+ const useDiscardState = (isDirty) => {
522
+ const dirtyRef = React.useRef(Boolean(isDirty));
523
+ React.useLayoutEffect(() => {
524
+ dirtyRef.current = Boolean(isDirty);
525
+ }, [isDirty]);
526
+ const [pendingAction, setPendingAction] = React.useState(null);
527
+ const runGuarded = React.useCallback((action) => {
528
+ if (typeof action !== "function") return;
529
+ if (dirtyRef.current) {
530
+ setPendingAction(() => action);
531
+ } else {
532
+ action();
533
+ }
534
+ }, []);
535
+ return { dirtyRef, pendingAction, setPendingAction, runGuarded };
536
+ };
537
+ const useDiscardGuard = (isDirty) => {
538
+ const { pendingAction, setPendingAction, runGuarded } = useDiscardState(isDirty);
539
+ const confirm = React.useCallback(() => {
540
+ const action = pendingAction;
541
+ setPendingAction(null);
542
+ if (action) action();
543
+ }, [pendingAction, setPendingAction]);
544
+ const cancel = React.useCallback(() => setPendingAction(null), [setPendingAction]);
545
+ return { isDirty: Boolean(isDirty), isOpen: pendingAction !== null, confirm, cancel, runGuarded };
546
+ };
547
+ const useUnsavedChangesGuard = (isDirty) => {
548
+ const { dirtyRef, pendingAction, setPendingAction, runGuarded } = useDiscardState(isDirty);
549
+ const bypassRef = React.useRef(false);
550
+ const blocker = reactRouterDom.useBlocker(
551
+ React.useCallback(
552
+ (transition) => !bypassRef.current && shouldBlockNavigation(dirtyRef.current, transition),
553
+ [dirtyRef]
554
+ )
555
+ );
556
+ React.useEffect(() => attachBeforeUnload(Boolean(isDirty)), [isDirty]);
557
+ React.useEffect(() => {
558
+ if (blocker.state === "blocked" && !isDirty) blocker.proceed();
559
+ }, [blocker, isDirty]);
560
+ const confirm = React.useCallback(() => {
561
+ if (blocker.state === "blocked") blocker.proceed();
562
+ const action = pendingAction;
563
+ setPendingAction(null);
564
+ if (action) action();
565
+ }, [blocker, pendingAction, setPendingAction]);
566
+ const cancel = React.useCallback(() => {
567
+ if (blocker.state === "blocked") blocker.reset();
568
+ setPendingAction(null);
569
+ }, [blocker, setPendingAction]);
570
+ const bypass = React.useCallback((action) => {
571
+ bypassRef.current = true;
572
+ try {
573
+ return action();
574
+ } finally {
575
+ bypassRef.current = false;
576
+ }
577
+ }, []);
578
+ return {
579
+ isDirty: Boolean(isDirty),
580
+ isOpen: blocker.state === "blocked" || pendingAction !== null,
581
+ confirm,
582
+ cancel,
583
+ runGuarded,
584
+ bypass
585
+ };
586
+ };
587
+ const MAX_EMAIL_LENGTH = 254;
588
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
589
+ function isValidEmailAddress(value) {
590
+ return typeof value === "string" && value.length <= MAX_EMAIL_LENGTH && value === value.trim() && EMAIL_PATTERN.test(value);
591
+ }
592
+ function isOptionalEmailAddressValid(value) {
593
+ return typeof value === "string" && (value.trim() === "" || isValidEmailAddress(value));
594
+ }
595
+ const PluginPageTitle = ({ children }) => {
596
+ const { t } = useMagicMailIntl();
597
+ return /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Title, { children: t("common.pageTitle", "{page} | MagicMail", { page: children }) });
598
+ };
599
+ const SectionHeading = styled__default.default(designSystem.Typography).attrs((props) => ({
600
+ tag: props.tag ?? "h2",
601
+ variant: props.variant ?? "delta",
602
+ textColor: props.textColor ?? "neutral700"
603
+ }))`
604
+ && {
605
+ font-size: 1.5rem;
606
+ font-weight: 600;
607
+ }
608
+ `;
609
+ styled__default.default(designSystem.Typography).attrs((props) => ({
610
+ tag: props.tag ?? "h3",
611
+ variant: props.variant ?? "delta",
612
+ textColor: props.textColor ?? "neutral800"
613
+ }))``;
614
+ const LOGS_PAGE_SIZE = 25;
615
+ const MAX_SEARCH_LENGTH = 500;
616
+ const MAX_CLEAR_ROUNDS = 100;
617
+ const normalizeSearch = (value) => typeof value === "string" ? value.trim().slice(0, MAX_SEARCH_LENGTH) : "";
618
+ const buildEmailLogsUrl = ({ page = 1, pageSize = LOGS_PAGE_SIZE, search = "" } = {}) => {
619
+ const params = new URLSearchParams();
620
+ const safePage = Number.isInteger(page) && page > 0 ? page : 1;
621
+ const safeSize = Number.isInteger(pageSize) && pageSize > 0 ? Math.min(pageSize, 100) : LOGS_PAGE_SIZE;
622
+ params.set("page", String(safePage));
623
+ params.set("pageSize", String(safeSize));
624
+ const term = normalizeSearch(search);
625
+ if (term) params.set("search", term);
626
+ return `/magic-mail/analytics/emails?${params.toString()}`;
627
+ };
628
+ const clampPage = (page, pageCount) => {
629
+ const max = Number.isInteger(pageCount) && pageCount > 0 ? pageCount : 1;
630
+ if (!Number.isInteger(page) || page < 1) return 1;
631
+ return Math.min(page, max);
632
+ };
633
+ const clearAllEmailLogsInBatches = async (del, { maxRounds = MAX_CLEAR_ROUNDS } = {}) => {
634
+ let deletedCount = 0;
635
+ for (let round = 0; round < maxRounds; round += 1) {
636
+ const response = await del("/magic-mail/analytics/emails");
637
+ const data = response?.data ?? {};
638
+ const deleted = Number.isFinite(Number(data.deletedCount)) ? Number(data.deletedCount) : 0;
639
+ deletedCount += deleted;
640
+ if (data.hasMore !== true) return { deletedCount, complete: true };
641
+ if (deleted <= 0) return { deletedCount, complete: false };
642
+ }
643
+ return { deletedCount, complete: false };
644
+ };
645
+ const apiErrorMessage = (error) => {
646
+ const message = error?.response?.data?.error?.message ?? error?.response?.data?.message;
647
+ return typeof message === "string" && message.trim() ? message.trim() : null;
648
+ };
649
+ exports.CONTROL_HEIGHT = CONTROL_HEIGHT;
650
+ exports.CTAButton = CTAButton;
651
+ exports.CTA_HEIGHT = CTA_HEIGHT;
652
+ exports.ConfirmDialog = ConfirmDialog;
653
+ exports.DangerButton = DangerButton;
654
+ exports.GradientButton = GradientButton;
655
+ exports.IconButton = IconButton;
656
+ exports.IconButtonDanger = IconButtonDanger;
657
+ exports.IconButtonPrimary = IconButtonPrimary;
658
+ exports.IconButtonPurple = IconButtonPurple;
659
+ exports.IconButtonSuccess = IconButtonSuccess;
660
+ exports.LOGS_PAGE_SIZE = LOGS_PAGE_SIZE;
661
+ exports.PluginPageTitle = PluginPageTitle;
662
+ exports.SecondaryButton = SecondaryButton;
663
+ exports.SectionHeading = SectionHeading;
664
+ exports.TertiaryButton = TertiaryButton;
665
+ exports.UnsavedChangesDialog = UnsavedChangesDialog;
666
+ exports.accent = accent;
667
+ exports.apiErrorMessage = apiErrorMessage;
668
+ exports.brand = brand;
669
+ exports.buildEmailLogsUrl = buildEmailLogsUrl;
670
+ exports.byScheme = byScheme;
671
+ exports.clampPage = clampPage;
672
+ exports.clearAllEmailLogsInBatches = clearAllEmailLogsInBatches;
673
+ exports.color = color;
674
+ exports.fontSize = fontSize;
675
+ exports.isDarkTheme = isDarkTheme;
676
+ exports.isOptionalEmailAddressValid = isOptionalEmailAddressValid;
677
+ exports.isValidEmailAddress = isValidEmailAddress;
678
+ exports.motionSafe = motionSafe;
679
+ exports.normalizeSearch = normalizeSearch;
680
+ exports.reducedMotion = reducedMotion;
681
+ exports.theme = theme;
682
+ exports.useDiscardGuard = useDiscardGuard;
683
+ exports.useMagicMailIntl = useMagicMailIntl;
684
+ exports.useMagicMailRBAC = useMagicMailRBAC;
685
+ exports.useReturnFocus = useReturnFocus;
686
+ exports.useUnsavedChangesGuard = useUnsavedChangesGuard;