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