strapi-plugin-magic-mail 3.0.4 → 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.
- package/CHANGELOG.md +192 -0
- package/README.md +625 -1336
- package/dist/admin/App-Bi6mzEff.mjs +10973 -0
- package/dist/admin/App-aaEU9cpu.js +10976 -0
- package/dist/admin/PluginSettings-BR2-PVHx.mjs +705 -0
- package/dist/admin/PluginSettings-DbTkOzCh.js +707 -0
- package/dist/admin/analyticsLogs-BCY0Zv3Y.mjs +685 -0
- package/dist/admin/analyticsLogs-DNU09hoB.js +686 -0
- package/dist/admin/de-B5bPRK6l.js +717 -0
- package/dist/admin/de-BqYqMhOn.mjs +717 -0
- package/dist/admin/en-8xANOQdO.mjs +717 -0
- package/dist/admin/en-Bk5eVrtc.js +717 -0
- package/dist/admin/es-BfPsCsam.js +717 -0
- package/dist/admin/es-DgpTufJ_.mjs +717 -0
- package/dist/admin/fr-BuotP0Gc.mjs +717 -0
- package/dist/admin/fr-IQ3KPUoS.js +717 -0
- package/dist/admin/index-QGqbGw6k.js +102 -0
- package/dist/admin/index-qvFJ-a0Q.mjs +103 -0
- package/dist/admin/index.js +2 -99
- package/dist/admin/index.mjs +2 -99
- package/dist/admin/pt-BI63Lncs.js +717 -0
- package/dist/admin/pt-DBZu2cDY.mjs +717 -0
- package/dist/server/index.js +4655 -3374
- package/dist/server/index.mjs +4655 -3374
- package/package.json +54 -48
- package/strapi-admin.js +3 -2
- package/strapi-server.js +3 -2
- package/dist/admin/App-D3TchJYB.js +0 -8028
- package/dist/admin/App-DkyWmEgv.mjs +0 -8007
- package/dist/admin/PluginSettings-B6TdA1qd.js +0 -388
- package/dist/admin/PluginSettings-DTqdZ-nW.mjs +0 -386
- package/dist/admin/StyledButtons-aq1EWrQ-.mjs +0 -433
- package/dist/admin/StyledButtons-jHF7TXal.js +0 -434
- package/dist/admin/de-DKEFnBGC.js +0 -63
- package/dist/admin/de-SKsLY2de.mjs +0 -63
- package/dist/admin/en-CvtZE1HH.js +0 -63
- package/dist/admin/en-Ds2Q7fue.mjs +0 -63
- package/dist/admin/es-BEvzLtTg.mjs +0 -63
- package/dist/admin/es-BbLLes3D.js +0 -63
- package/dist/admin/fr-CRpwImX6.mjs +0 -63
- package/dist/admin/fr-nH9uN6s9.js +0 -63
- package/dist/admin/pt-CjOe2N_n.mjs +0 -63
- package/dist/admin/pt-qt_48NxL.js +0 -63
- package/pics/EmailAccountDashboard.png +0 -0
- package/pics/Emailaddccount1.png +0 -0
- package/pics/RoutingEmailTemplates.png +0 -0
- package/pics/Templates.png +0 -0
- package/pics/codesnippet.png +0 -0
- package/pics/emailAnalytics.png +0 -0
- package/pics/emailDesigner.png +0 -0
- package/pics/emailTemplateSendTest.png +0 -0
- package/pics/emailVersionHistory.png +0 -0
- package/pics/emailaddaccount2.png +0 -0
- package/pics/emailaddaccount3.png +0 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect, useMemo } from "react";
|
|
3
|
+
import { Loader, Typography, Flex, Switch, Box, Field, TextInput } from "@strapi/design-system";
|
|
4
|
+
import { Page, useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
5
|
+
import { a as isValidEmailAddress, j as useMagicMailRBAC, u as useMagicMailIntl, w as useUnsavedChangesGuard, P as PluginPageTitle, T as TertiaryButton, G as GradientButton, U as UnsavedChangesDialog, B as apiErrorMessage, m as motionSafe, f as fontSize, c as color, h as accent } from "./analyticsLogs-BCY0Zv3Y.mjs";
|
|
6
|
+
import { p as pluginPermissions } from "./index-qvFJ-a0Q.mjs";
|
|
7
|
+
import { ExclamationTriangleIcon, ArrowPathIcon, LinkIcon, EyeIcon, EnvelopeIcon, UserIcon } from "@heroicons/react/24/outline";
|
|
8
|
+
import styled, { css, keyframes } from "styled-components";
|
|
9
|
+
const SETTINGS_DEFAULTS = Object.freeze({
|
|
10
|
+
enableLinkTracking: true,
|
|
11
|
+
enableOpenTracking: true,
|
|
12
|
+
trackingBaseUrl: "",
|
|
13
|
+
defaultFromName: "",
|
|
14
|
+
defaultFromEmail: "",
|
|
15
|
+
unsubscribeUrl: "",
|
|
16
|
+
enableUnsubscribeHeader: true
|
|
17
|
+
});
|
|
18
|
+
const SETTINGS_KEYS = Object.freeze(Object.keys(SETTINGS_DEFAULTS));
|
|
19
|
+
const MAX_URL_LENGTH = 2048;
|
|
20
|
+
const MAX_NAME_LENGTH = 1e3;
|
|
21
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
22
|
+
const normalizeSettings = (data) => {
|
|
23
|
+
const source = data && typeof data === "object" ? data : {};
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const key of SETTINGS_KEYS) {
|
|
26
|
+
const fallback = SETTINGS_DEFAULTS[key];
|
|
27
|
+
const value = source[key];
|
|
28
|
+
if (typeof fallback === "boolean") {
|
|
29
|
+
result[key] = typeof value === "boolean" ? value : fallback;
|
|
30
|
+
} else {
|
|
31
|
+
result[key] = typeof value === "string" ? value : "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
const diffSettings = (initial, current) => {
|
|
37
|
+
const changes = {};
|
|
38
|
+
for (const key of SETTINGS_KEYS) {
|
|
39
|
+
if (initial?.[key] !== current?.[key]) {
|
|
40
|
+
const value = current?.[key];
|
|
41
|
+
changes[key] = typeof value === "string" ? value.trim() : value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return changes;
|
|
45
|
+
};
|
|
46
|
+
const parseUrl = (value) => {
|
|
47
|
+
try {
|
|
48
|
+
return new URL(value);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const isInsecureRemoteUrl = (value) => {
|
|
54
|
+
const url = typeof value === "string" ? parseUrl(value.trim()) : null;
|
|
55
|
+
return Boolean(url && url.protocol === "http:" && !LOCAL_HOSTS.has(url.hostname));
|
|
56
|
+
};
|
|
57
|
+
const checkUrl = (value, protocols) => {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (!trimmed) return null;
|
|
60
|
+
if (trimmed.length > MAX_URL_LENGTH || /\s/.test(trimmed)) return "invalidUrl";
|
|
61
|
+
const url = parseUrl(trimmed.split("{{email}}").join("user%40example.com"));
|
|
62
|
+
if (!url) return "invalidUrl";
|
|
63
|
+
if (!protocols.includes(url.protocol)) return "invalidProtocol";
|
|
64
|
+
return null;
|
|
65
|
+
};
|
|
66
|
+
const validateSettings = (settings) => {
|
|
67
|
+
const errors = {};
|
|
68
|
+
const trackingError = checkUrl(settings?.trackingBaseUrl ?? "", ["http:", "https:"]);
|
|
69
|
+
if (trackingError) errors.trackingBaseUrl = trackingError;
|
|
70
|
+
const unsubscribeError = checkUrl(settings?.unsubscribeUrl ?? "", ["http:", "https:", "mailto:"]);
|
|
71
|
+
if (unsubscribeError) errors.unsubscribeUrl = unsubscribeError;
|
|
72
|
+
const email = (settings?.defaultFromEmail ?? "").trim();
|
|
73
|
+
if (email && !isValidEmailAddress(email)) errors.defaultFromEmail = "invalidEmail";
|
|
74
|
+
const name = settings?.defaultFromName ?? "";
|
|
75
|
+
if (name.length > MAX_NAME_LENGTH || /[\r\n]/.test(name)) errors.defaultFromName = "invalidText";
|
|
76
|
+
return errors;
|
|
77
|
+
};
|
|
78
|
+
const fadeIn = keyframes`
|
|
79
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
80
|
+
to { opacity: 1; transform: translateY(0); }
|
|
81
|
+
`;
|
|
82
|
+
const mobile = "768px";
|
|
83
|
+
const PageContainer = styled(Box)`
|
|
84
|
+
${motionSafe(css`
|
|
85
|
+
animation: ${fadeIn} 0.4s ease-out;
|
|
86
|
+
`)}
|
|
87
|
+
max-width: 900px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
padding: 40px 32px;
|
|
90
|
+
|
|
91
|
+
@media screen and (max-width: ${mobile}) {
|
|
92
|
+
padding: 24px 16px;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
const PageHeader = styled(Flex)`
|
|
96
|
+
margin-bottom: 32px;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
align-items: flex-start;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
`;
|
|
101
|
+
const PageTitle = styled(Typography)`
|
|
102
|
+
font-size: ${fontSize(6)};
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
color: ${color("neutral800")};
|
|
105
|
+
display: block;
|
|
106
|
+
margin: 0;
|
|
107
|
+
`;
|
|
108
|
+
const PageSubtitle = styled(Typography)`
|
|
109
|
+
font-size: ${fontSize(2)};
|
|
110
|
+
color: ${color("neutral600")};
|
|
111
|
+
display: block;
|
|
112
|
+
margin: 0;
|
|
113
|
+
`;
|
|
114
|
+
const ActionBar = styled(Flex)`
|
|
115
|
+
margin-bottom: 32px;
|
|
116
|
+
padding: 16px 20px;
|
|
117
|
+
background: ${color("neutral0")};
|
|
118
|
+
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
gap: 12px;
|
|
123
|
+
`;
|
|
124
|
+
const ActionButtons = styled(Flex)`
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
`;
|
|
128
|
+
const SettingsSection = styled(Box)`
|
|
129
|
+
background: ${color("neutral0")};
|
|
130
|
+
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
131
|
+
border-radius: 12px;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
margin-bottom: 24px;
|
|
134
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
135
|
+
`;
|
|
136
|
+
const SectionHeader = styled(Flex)`
|
|
137
|
+
padding: 20px 24px;
|
|
138
|
+
background: ${color("neutral100")};
|
|
139
|
+
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
|
140
|
+
|
|
141
|
+
@media screen and (max-width: ${mobile}) {
|
|
142
|
+
padding: 16px;
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
const SectionIcon = styled(Box)`
|
|
146
|
+
width: 44px;
|
|
147
|
+
height: 44px;
|
|
148
|
+
border-radius: 10px;
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
margin-right: 16px;
|
|
153
|
+
background: ${(props) => props.$bgColor || "rgba(14, 165, 233, 0.12)"};
|
|
154
|
+
flex-shrink: 0;
|
|
155
|
+
|
|
156
|
+
svg {
|
|
157
|
+
width: 22px;
|
|
158
|
+
height: 22px;
|
|
159
|
+
color: ${(p) => (accent[p.$tone] ?? accent.primary)(p)};
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
const SectionContent = styled(Box)`
|
|
163
|
+
padding: 24px;
|
|
164
|
+
|
|
165
|
+
@media screen and (max-width: ${mobile}) {
|
|
166
|
+
padding: 16px;
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
const SettingRow = styled(Flex)`
|
|
170
|
+
padding: 16px 0;
|
|
171
|
+
border-bottom: 1px solid rgba(128, 128, 128, 0.15);
|
|
172
|
+
|
|
173
|
+
&:last-child {
|
|
174
|
+
border-bottom: none;
|
|
175
|
+
padding-bottom: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
&:first-child {
|
|
179
|
+
padding-top: 0;
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
const SettingInfo = styled(Flex)`
|
|
183
|
+
flex: 1;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
padding-right: 24px;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
align-items: flex-start;
|
|
188
|
+
gap: 4px;
|
|
189
|
+
|
|
190
|
+
@media screen and (max-width: ${mobile}) {
|
|
191
|
+
padding-right: 12px;
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
const SettingLabel = styled(Typography)`
|
|
195
|
+
font-size: ${fontSize(2)};
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
color: ${color("neutral800")};
|
|
198
|
+
display: block;
|
|
199
|
+
`;
|
|
200
|
+
const SettingDescription = styled(Typography)`
|
|
201
|
+
font-size: ${fontSize(1)};
|
|
202
|
+
color: ${color("neutral600")};
|
|
203
|
+
line-height: 1.5;
|
|
204
|
+
display: block;
|
|
205
|
+
`;
|
|
206
|
+
const SwitchWrapper = styled(Box)`
|
|
207
|
+
flex-shrink: 0;
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
|
|
211
|
+
/* Touch screens: the DS switch (40x24) grows to a 48x32 target with the
|
|
212
|
+
same proportions (as in the routing rule modal). */
|
|
213
|
+
@media (max-width: 767px) {
|
|
214
|
+
button[role='switch'] {
|
|
215
|
+
width: 48px;
|
|
216
|
+
height: 32px;
|
|
217
|
+
border-radius: 16px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
button[role='switch'] > span {
|
|
221
|
+
width: 24px;
|
|
222
|
+
height: 24px;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
const LoaderContainer = styled(Flex)`
|
|
227
|
+
min-height: 400px;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: center;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
gap: 16px;
|
|
232
|
+
`;
|
|
233
|
+
const ErrorState = styled(Flex)`
|
|
234
|
+
min-height: 320px;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
gap: 12px;
|
|
239
|
+
text-align: center;
|
|
240
|
+
padding: 32px 16px;
|
|
241
|
+
background: ${color("neutral0")};
|
|
242
|
+
border: 2px dashed rgba(128, 128, 128, 0.3);
|
|
243
|
+
border-radius: 12px;
|
|
244
|
+
|
|
245
|
+
/* Only the state icon; the Retry button keeps its neutral icon. */
|
|
246
|
+
& > svg {
|
|
247
|
+
width: 40px;
|
|
248
|
+
height: 40px;
|
|
249
|
+
color: ${accent.danger};
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
const InfoBox = styled(Box)`
|
|
253
|
+
background: rgba(14, 165, 233, 0.08);
|
|
254
|
+
border: 1px solid rgba(14, 165, 233, 0.3);
|
|
255
|
+
border-radius: 8px;
|
|
256
|
+
padding: 12px 16px;
|
|
257
|
+
margin-top: 16px;
|
|
258
|
+
`;
|
|
259
|
+
const CodeSnippet = styled.code`
|
|
260
|
+
background: #1e293b;
|
|
261
|
+
color: #e2e8f0;
|
|
262
|
+
padding: 2px 8px;
|
|
263
|
+
border-radius: 4px;
|
|
264
|
+
font-size: ${fontSize(1)};
|
|
265
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
266
|
+
overflow-wrap: anywhere;
|
|
267
|
+
`;
|
|
268
|
+
const ICON_18 = { width: 18, height: 18 };
|
|
269
|
+
const errorMessage = (t, code) => {
|
|
270
|
+
switch (code) {
|
|
271
|
+
case "invalidUrl":
|
|
272
|
+
return t(
|
|
273
|
+
"settings.errors.invalidUrl",
|
|
274
|
+
"Enter a valid URL, for example https://api.example.com"
|
|
275
|
+
);
|
|
276
|
+
case "invalidProtocol":
|
|
277
|
+
return t("settings.errors.invalidProtocol", "This URL type is not allowed here");
|
|
278
|
+
case "invalidEmail":
|
|
279
|
+
return t("settings.errors.invalidEmail", "Enter a valid email address");
|
|
280
|
+
case "invalidText":
|
|
281
|
+
return t("settings.errors.invalidText", "Use a single line of at most 1000 characters");
|
|
282
|
+
default:
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const PluginSettingsContent = () => {
|
|
287
|
+
const { canManage, isLoading: rbacLoading, error: rbacError } = useMagicMailRBAC();
|
|
288
|
+
const { t } = useMagicMailIntl();
|
|
289
|
+
const { get, put } = useFetchClient();
|
|
290
|
+
const { toggleNotification } = useNotification();
|
|
291
|
+
const [loading, setLoading] = useState(true);
|
|
292
|
+
const [loadError, setLoadError] = useState(null);
|
|
293
|
+
const [initialSettings, setInitialSettings] = useState(null);
|
|
294
|
+
const [settings, setSettings] = useState(SETTINGS_DEFAULTS);
|
|
295
|
+
const [saving, setSaving] = useState(false);
|
|
296
|
+
const mounted = useRef(true);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
mounted.current = true;
|
|
299
|
+
return () => {
|
|
300
|
+
mounted.current = false;
|
|
301
|
+
};
|
|
302
|
+
}, []);
|
|
303
|
+
const changes = useMemo(
|
|
304
|
+
() => initialSettings ? diffSettings(initialSettings, settings) : {},
|
|
305
|
+
[initialSettings, settings]
|
|
306
|
+
);
|
|
307
|
+
const hasChanges = Object.keys(changes).length > 0;
|
|
308
|
+
const errors = useMemo(() => validateSettings(settings), [settings]);
|
|
309
|
+
const isValid = Object.keys(errors).length === 0;
|
|
310
|
+
const guard = useUnsavedChangesGuard(hasChanges);
|
|
311
|
+
const [settingsReloadKey, setSettingsReloadKey] = useState(0);
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
let ignore = false;
|
|
314
|
+
const load = async () => {
|
|
315
|
+
try {
|
|
316
|
+
const response = await get("/magic-mail/settings");
|
|
317
|
+
if (ignore) return;
|
|
318
|
+
const data = response.data?.data;
|
|
319
|
+
if (!data || typeof data !== "object") throw new Error("Empty settings response");
|
|
320
|
+
const loaded = normalizeSettings(data);
|
|
321
|
+
setInitialSettings(loaded);
|
|
322
|
+
setSettings(loaded);
|
|
323
|
+
setLoadError(null);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (ignore) return;
|
|
326
|
+
console.error("[MagicMail] Failed to fetch plugin settings");
|
|
327
|
+
const message = apiErrorMessage(err) || t("settings.loadError", "Failed to load settings");
|
|
328
|
+
setLoadError(message);
|
|
329
|
+
toggleNotification({ type: "danger", message });
|
|
330
|
+
} finally {
|
|
331
|
+
if (!ignore) setLoading(false);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
load();
|
|
335
|
+
return () => {
|
|
336
|
+
ignore = true;
|
|
337
|
+
};
|
|
338
|
+
}, [get, t, toggleNotification, settingsReloadKey]);
|
|
339
|
+
const fetchSettings = () => {
|
|
340
|
+
setLoading(true);
|
|
341
|
+
setSettingsReloadKey((key) => key + 1);
|
|
342
|
+
};
|
|
343
|
+
const saveSettings = async () => {
|
|
344
|
+
if (!canManage || !initialSettings || !hasChanges || !isValid) return;
|
|
345
|
+
setSaving(true);
|
|
346
|
+
try {
|
|
347
|
+
const response = await put("/magic-mail/settings", changes);
|
|
348
|
+
const saved = response?.data?.data ? normalizeSettings(response.data.data) : { ...settings, ...changes };
|
|
349
|
+
if (!mounted.current) return;
|
|
350
|
+
setInitialSettings(saved);
|
|
351
|
+
setSettings(saved);
|
|
352
|
+
toggleNotification({
|
|
353
|
+
type: "success",
|
|
354
|
+
message: t("settings.saveSuccess", "Settings saved")
|
|
355
|
+
});
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error("[MagicMail] Failed to save plugin settings");
|
|
358
|
+
toggleNotification({
|
|
359
|
+
type: "danger",
|
|
360
|
+
message: apiErrorMessage(err) || t("settings.saveError", "Failed to save settings")
|
|
361
|
+
});
|
|
362
|
+
} finally {
|
|
363
|
+
if (mounted.current) setSaving(false);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const handleChange = (key, value) => {
|
|
367
|
+
if (!canManage) return;
|
|
368
|
+
setSettings((prev) => ({ ...prev, [key]: value }));
|
|
369
|
+
};
|
|
370
|
+
const pageTitle = /* @__PURE__ */ jsx(PluginPageTitle, { children: t("settings.pageTitle", "Plugin Settings") });
|
|
371
|
+
if (loading || rbacLoading) {
|
|
372
|
+
return /* @__PURE__ */ jsxs(PageContainer, { children: [
|
|
373
|
+
pageTitle,
|
|
374
|
+
/* @__PURE__ */ jsx(LoaderContainer, { children: /* @__PURE__ */ jsx(Loader, { children: t("settings.loading", "Loading settings...") }) })
|
|
375
|
+
] });
|
|
376
|
+
}
|
|
377
|
+
const header = /* @__PURE__ */ jsxs(PageHeader, { children: [
|
|
378
|
+
/* @__PURE__ */ jsx(PageTitle, { tag: "h1", children: t("settings.title", "Plugin Settings") }),
|
|
379
|
+
/* @__PURE__ */ jsx(PageSubtitle, { tag: "p", children: t(
|
|
380
|
+
"settings.subtitle",
|
|
381
|
+
"Configure email tracking, analytics, and default sender information"
|
|
382
|
+
) })
|
|
383
|
+
] });
|
|
384
|
+
if (loadError || !initialSettings) {
|
|
385
|
+
return /* @__PURE__ */ jsxs(PageContainer, { children: [
|
|
386
|
+
pageTitle,
|
|
387
|
+
header,
|
|
388
|
+
/* @__PURE__ */ jsxs(ErrorState, { role: "alert", children: [
|
|
389
|
+
/* @__PURE__ */ jsx(ExclamationTriangleIcon, { "aria-hidden": "true" }),
|
|
390
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", tag: "h2", children: t("settings.loadErrorTitle", "Settings could not be loaded") }),
|
|
391
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "neutral600", children: loadError || t("settings.loadError", "Failed to load settings") }),
|
|
392
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
|
|
393
|
+
"settings.loadErrorHint",
|
|
394
|
+
"Saving is disabled until the current settings have been loaded."
|
|
395
|
+
) }),
|
|
396
|
+
/* @__PURE__ */ jsx(
|
|
397
|
+
TertiaryButton,
|
|
398
|
+
{
|
|
399
|
+
startIcon: /* @__PURE__ */ jsx(ArrowPathIcon, { style: ICON_18, "aria-hidden": "true" }),
|
|
400
|
+
onClick: fetchSettings,
|
|
401
|
+
children: t("settings.retry", "Retry")
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
] })
|
|
405
|
+
] });
|
|
406
|
+
}
|
|
407
|
+
const statusText = rbacError ? t(
|
|
408
|
+
"common.permissions.loadError",
|
|
409
|
+
"Your permissions could not be loaded. Reload the page to try again."
|
|
410
|
+
) : !canManage ? t(
|
|
411
|
+
"settings.status.readOnly",
|
|
412
|
+
"Read-only access. You need the MagicMail manage permission to change settings."
|
|
413
|
+
) : !isValid ? t("settings.status.invalid", "Please correct the highlighted fields") : hasChanges ? t("settings.status.unsaved", "You have unsaved changes") : t("settings.status.saved", "All changes saved");
|
|
414
|
+
const showTrackingUrl = settings.enableLinkTracking || Boolean(errors.trackingBaseUrl);
|
|
415
|
+
const showUnsubscribeUrl = settings.enableUnsubscribeHeader || Boolean(errors.unsubscribeUrl);
|
|
416
|
+
return /* @__PURE__ */ jsxs(PageContainer, { children: [
|
|
417
|
+
pageTitle,
|
|
418
|
+
header,
|
|
419
|
+
/* @__PURE__ */ jsxs(ActionBar, { justifyContent: "space-between", alignItems: "center", children: [
|
|
420
|
+
/* @__PURE__ */ jsx(
|
|
421
|
+
Typography,
|
|
422
|
+
{
|
|
423
|
+
variant: "omega",
|
|
424
|
+
textColor: rbacError || !isValid && canManage ? "danger600" : "neutral600",
|
|
425
|
+
"aria-live": "polite",
|
|
426
|
+
children: statusText
|
|
427
|
+
}
|
|
428
|
+
),
|
|
429
|
+
/* @__PURE__ */ jsxs(ActionButtons, { children: [
|
|
430
|
+
/* @__PURE__ */ jsx(
|
|
431
|
+
TertiaryButton,
|
|
432
|
+
{
|
|
433
|
+
startIcon: /* @__PURE__ */ jsx(ArrowPathIcon, { style: ICON_18, "aria-hidden": "true" }),
|
|
434
|
+
onClick: () => guard.runGuarded(fetchSettings),
|
|
435
|
+
disabled: saving,
|
|
436
|
+
children: t("settings.refresh", "Refresh")
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
canManage && /* @__PURE__ */ jsx(
|
|
440
|
+
GradientButton,
|
|
441
|
+
{
|
|
442
|
+
onClick: saveSettings,
|
|
443
|
+
loading: saving,
|
|
444
|
+
disabled: !hasChanges || !canManage || !isValid,
|
|
445
|
+
children: t("settings.save", "Save Changes")
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
] })
|
|
449
|
+
] }),
|
|
450
|
+
/* @__PURE__ */ jsxs(SettingsSection, { tag: "section", "aria-labelledby": "mm-settings-link-tracking", children: [
|
|
451
|
+
/* @__PURE__ */ jsxs(SectionHeader, { alignItems: "center", children: [
|
|
452
|
+
/* @__PURE__ */ jsx(SectionIcon, { $bgColor: "rgba(37, 99, 235, 0.15)", $tone: "primary", children: /* @__PURE__ */ jsx(LinkIcon, { "aria-hidden": "true" }) }),
|
|
453
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
454
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", tag: "h2", id: "mm-settings-link-tracking", children: t("settings.linkTracking.title", "Link Tracking") }),
|
|
455
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
|
|
456
|
+
"settings.linkTracking.subtitle",
|
|
457
|
+
"Track when recipients click links in your emails"
|
|
458
|
+
) })
|
|
459
|
+
] })
|
|
460
|
+
] }),
|
|
461
|
+
/* @__PURE__ */ jsxs(SectionContent, { children: [
|
|
462
|
+
/* @__PURE__ */ jsxs(SettingRow, { alignItems: "center", children: [
|
|
463
|
+
/* @__PURE__ */ jsxs(SettingInfo, { children: [
|
|
464
|
+
/* @__PURE__ */ jsx(SettingLabel, { children: t("settings.linkTracking.label", "Enable Link Tracking") }),
|
|
465
|
+
/* @__PURE__ */ jsx(SettingDescription, { children: t(
|
|
466
|
+
"settings.linkTracking.description",
|
|
467
|
+
"Rewrite links in outgoing emails to measure click-through rates. Links with sensitive tokens are never rewritten."
|
|
468
|
+
) })
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsx(SwitchWrapper, { children: /* @__PURE__ */ jsx(
|
|
471
|
+
Switch,
|
|
472
|
+
{
|
|
473
|
+
checked: settings.enableLinkTracking,
|
|
474
|
+
disabled: !canManage,
|
|
475
|
+
onCheckedChange: (checked) => handleChange("enableLinkTracking", checked),
|
|
476
|
+
"aria-label": t("settings.linkTracking.label", "Enable Link Tracking"),
|
|
477
|
+
onLabel: t("settings.switch.on", "Enabled"),
|
|
478
|
+
offLabel: t("settings.switch.off", "Disabled"),
|
|
479
|
+
visibleLabels: false
|
|
480
|
+
}
|
|
481
|
+
) })
|
|
482
|
+
] }),
|
|
483
|
+
showTrackingUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
484
|
+
/* @__PURE__ */ jsx(Box, { style: { marginTop: "20px" }, children: /* @__PURE__ */ jsxs(
|
|
485
|
+
Field.Root,
|
|
486
|
+
{
|
|
487
|
+
name: "trackingBaseUrl",
|
|
488
|
+
error: errorMessage(t, errors.trackingBaseUrl),
|
|
489
|
+
hint: isInsecureRemoteUrl(settings.trackingBaseUrl) ? t(
|
|
490
|
+
"settings.trackingBaseUrl.httpWarning",
|
|
491
|
+
"This URL uses http. Use https in production so tracked links stay secure."
|
|
492
|
+
) : t(
|
|
493
|
+
"settings.trackingBaseUrl.hint",
|
|
494
|
+
"Public URL where your Strapi API is reachable. Leave empty to use the server URL."
|
|
495
|
+
),
|
|
496
|
+
children: [
|
|
497
|
+
/* @__PURE__ */ jsx(Field.Label, { children: t("settings.trackingBaseUrl.label", "Tracking Base URL") }),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
TextInput,
|
|
500
|
+
{
|
|
501
|
+
type: "url",
|
|
502
|
+
inputMode: "url",
|
|
503
|
+
autoComplete: "url",
|
|
504
|
+
placeholder: "https://api.example.com",
|
|
505
|
+
value: settings.trackingBaseUrl,
|
|
506
|
+
onChange: (e) => handleChange("trackingBaseUrl", e.target.value),
|
|
507
|
+
disabled: !canManage
|
|
508
|
+
}
|
|
509
|
+
),
|
|
510
|
+
/* @__PURE__ */ jsx(Field.Hint, {}),
|
|
511
|
+
/* @__PURE__ */ jsx(Field.Error, {})
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
) }),
|
|
515
|
+
/* @__PURE__ */ jsxs(InfoBox, { children: [
|
|
516
|
+
/* @__PURE__ */ jsx(
|
|
517
|
+
Typography,
|
|
518
|
+
{
|
|
519
|
+
variant: "pi",
|
|
520
|
+
textColor: "primary700",
|
|
521
|
+
fontWeight: "medium",
|
|
522
|
+
style: { display: "block" },
|
|
523
|
+
children: t("settings.override.title", "Per-Email Override")
|
|
524
|
+
}
|
|
525
|
+
),
|
|
526
|
+
/* @__PURE__ */ jsx(
|
|
527
|
+
Typography,
|
|
528
|
+
{
|
|
529
|
+
variant: "pi",
|
|
530
|
+
textColor: "primary600",
|
|
531
|
+
style: { marginTop: "4px", display: "block" },
|
|
532
|
+
children: t(
|
|
533
|
+
"settings.override.body",
|
|
534
|
+
"Disable tracking for sensitive emails (password resets, magic links) by passing {code} when sending.",
|
|
535
|
+
{ code: /* @__PURE__ */ jsx(CodeSnippet, { children: "skipLinkTracking: true" }, "code") }
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
] })
|
|
540
|
+
] })
|
|
541
|
+
] })
|
|
542
|
+
] }),
|
|
543
|
+
/* @__PURE__ */ jsxs(SettingsSection, { tag: "section", "aria-labelledby": "mm-settings-open-tracking", children: [
|
|
544
|
+
/* @__PURE__ */ jsxs(SectionHeader, { alignItems: "center", children: [
|
|
545
|
+
/* @__PURE__ */ jsx(SectionIcon, { $bgColor: "rgba(34, 197, 94, 0.15)", $tone: "success", children: /* @__PURE__ */ jsx(EyeIcon, { "aria-hidden": "true" }) }),
|
|
546
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
547
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", tag: "h2", id: "mm-settings-open-tracking", children: t("settings.openTracking.title", "Open Tracking") }),
|
|
548
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("settings.openTracking.subtitle", "Track when recipients open your emails") })
|
|
549
|
+
] })
|
|
550
|
+
] }),
|
|
551
|
+
/* @__PURE__ */ jsx(SectionContent, { children: /* @__PURE__ */ jsxs(SettingRow, { alignItems: "center", children: [
|
|
552
|
+
/* @__PURE__ */ jsxs(SettingInfo, { children: [
|
|
553
|
+
/* @__PURE__ */ jsx(SettingLabel, { children: t("settings.openTracking.label", "Enable Open Tracking") }),
|
|
554
|
+
/* @__PURE__ */ jsx(SettingDescription, { children: t(
|
|
555
|
+
"settings.openTracking.description",
|
|
556
|
+
"Add an invisible tracking pixel to emails to detect when they are opened. Some email clients block tracking pixels."
|
|
557
|
+
) })
|
|
558
|
+
] }),
|
|
559
|
+
/* @__PURE__ */ jsx(SwitchWrapper, { children: /* @__PURE__ */ jsx(
|
|
560
|
+
Switch,
|
|
561
|
+
{
|
|
562
|
+
checked: settings.enableOpenTracking,
|
|
563
|
+
disabled: !canManage,
|
|
564
|
+
onCheckedChange: (checked) => handleChange("enableOpenTracking", checked),
|
|
565
|
+
"aria-label": t("settings.openTracking.label", "Enable Open Tracking"),
|
|
566
|
+
onLabel: t("settings.switch.on", "Enabled"),
|
|
567
|
+
offLabel: t("settings.switch.off", "Disabled"),
|
|
568
|
+
visibleLabels: false
|
|
569
|
+
}
|
|
570
|
+
) })
|
|
571
|
+
] }) })
|
|
572
|
+
] }),
|
|
573
|
+
/* @__PURE__ */ jsxs(SettingsSection, { tag: "section", "aria-labelledby": "mm-settings-unsubscribe", children: [
|
|
574
|
+
/* @__PURE__ */ jsxs(SectionHeader, { alignItems: "center", children: [
|
|
575
|
+
/* @__PURE__ */ jsx(SectionIcon, { $bgColor: "rgba(245, 158, 11, 0.15)", $tone: "warning", children: /* @__PURE__ */ jsx(EnvelopeIcon, { "aria-hidden": "true" }) }),
|
|
576
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
577
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", tag: "h2", id: "mm-settings-unsubscribe", children: t("settings.unsubscribe.title", "Unsubscribe Settings") }),
|
|
578
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
|
|
579
|
+
"settings.unsubscribe.subtitle",
|
|
580
|
+
"List-Unsubscribe headers for one-click unsubscribe"
|
|
581
|
+
) })
|
|
582
|
+
] })
|
|
583
|
+
] }),
|
|
584
|
+
/* @__PURE__ */ jsxs(SectionContent, { children: [
|
|
585
|
+
/* @__PURE__ */ jsxs(SettingRow, { alignItems: "center", children: [
|
|
586
|
+
/* @__PURE__ */ jsxs(SettingInfo, { children: [
|
|
587
|
+
/* @__PURE__ */ jsx(SettingLabel, { children: t("settings.unsubscribe.label", "Enable List-Unsubscribe Header") }),
|
|
588
|
+
/* @__PURE__ */ jsx(SettingDescription, { children: t(
|
|
589
|
+
"settings.unsubscribe.description",
|
|
590
|
+
"Add RFC 8058 unsubscribe headers to non-marketing emails too. Marketing emails always get them when an unsubscribe URL is known. This enables the Unsubscribe button in clients such as Gmail and Outlook."
|
|
591
|
+
) })
|
|
592
|
+
] }),
|
|
593
|
+
/* @__PURE__ */ jsx(SwitchWrapper, { children: /* @__PURE__ */ jsx(
|
|
594
|
+
Switch,
|
|
595
|
+
{
|
|
596
|
+
checked: settings.enableUnsubscribeHeader,
|
|
597
|
+
disabled: !canManage,
|
|
598
|
+
onCheckedChange: (checked) => handleChange("enableUnsubscribeHeader", checked),
|
|
599
|
+
"aria-label": t("settings.unsubscribe.label", "Enable List-Unsubscribe Header"),
|
|
600
|
+
onLabel: t("settings.switch.on", "Enabled"),
|
|
601
|
+
offLabel: t("settings.switch.off", "Disabled"),
|
|
602
|
+
visibleLabels: false
|
|
603
|
+
}
|
|
604
|
+
) })
|
|
605
|
+
] }),
|
|
606
|
+
showUnsubscribeUrl && /* @__PURE__ */ jsx(Box, { style: { marginTop: "20px" }, children: /* @__PURE__ */ jsxs(
|
|
607
|
+
Field.Root,
|
|
608
|
+
{
|
|
609
|
+
name: "unsubscribeUrl",
|
|
610
|
+
error: errorMessage(t, errors.unsubscribeUrl),
|
|
611
|
+
hint: t(
|
|
612
|
+
"settings.unsubscribeUrl.hint",
|
|
613
|
+
"Default unsubscribe link (https, http or mailto) used when a send does not pass its own. Without a URL, no List-Unsubscribe header is added."
|
|
614
|
+
),
|
|
615
|
+
children: [
|
|
616
|
+
/* @__PURE__ */ jsx(Field.Label, { children: t("settings.unsubscribeUrl.label", "Unsubscribe URL") }),
|
|
617
|
+
/* @__PURE__ */ jsx(
|
|
618
|
+
TextInput,
|
|
619
|
+
{
|
|
620
|
+
type: "text",
|
|
621
|
+
inputMode: "url",
|
|
622
|
+
placeholder: "https://example.com/unsubscribe?email={{email}}",
|
|
623
|
+
value: settings.unsubscribeUrl,
|
|
624
|
+
onChange: (e) => handleChange("unsubscribeUrl", e.target.value),
|
|
625
|
+
disabled: !canManage
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
/* @__PURE__ */ jsx(Field.Hint, {}),
|
|
629
|
+
/* @__PURE__ */ jsx(Field.Error, {})
|
|
630
|
+
]
|
|
631
|
+
}
|
|
632
|
+
) })
|
|
633
|
+
] })
|
|
634
|
+
] }),
|
|
635
|
+
/* @__PURE__ */ jsxs(SettingsSection, { tag: "section", "aria-labelledby": "mm-settings-sender", children: [
|
|
636
|
+
/* @__PURE__ */ jsxs(SectionHeader, { alignItems: "center", children: [
|
|
637
|
+
/* @__PURE__ */ jsx(SectionIcon, { $bgColor: "rgba(147, 51, 234, 0.12)", $tone: "secondary", children: /* @__PURE__ */ jsx(UserIcon, { "aria-hidden": "true" }) }),
|
|
638
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
639
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", tag: "h2", id: "mm-settings-sender", children: t("settings.sender.title", "Default Sender") }),
|
|
640
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
|
|
641
|
+
"settings.sender.subtitle",
|
|
642
|
+
"Fallback sender information when not specified per email"
|
|
643
|
+
) })
|
|
644
|
+
] })
|
|
645
|
+
] }),
|
|
646
|
+
/* @__PURE__ */ jsx(SectionContent, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 5, children: [
|
|
647
|
+
/* @__PURE__ */ jsxs(
|
|
648
|
+
Field.Root,
|
|
649
|
+
{
|
|
650
|
+
name: "defaultFromName",
|
|
651
|
+
error: errorMessage(t, errors.defaultFromName),
|
|
652
|
+
hint: t("settings.fromName.hint", "The sender name shown in recipients' inboxes"),
|
|
653
|
+
children: [
|
|
654
|
+
/* @__PURE__ */ jsx(Field.Label, { children: t("settings.fromName.label", "Default From Name") }),
|
|
655
|
+
/* @__PURE__ */ jsx(
|
|
656
|
+
TextInput,
|
|
657
|
+
{
|
|
658
|
+
autoComplete: "organization",
|
|
659
|
+
placeholder: t("settings.fromName.placeholder", "Your Company"),
|
|
660
|
+
value: settings.defaultFromName,
|
|
661
|
+
onChange: (e) => handleChange("defaultFromName", e.target.value),
|
|
662
|
+
disabled: !canManage
|
|
663
|
+
}
|
|
664
|
+
),
|
|
665
|
+
/* @__PURE__ */ jsx(Field.Hint, {}),
|
|
666
|
+
/* @__PURE__ */ jsx(Field.Error, {})
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
/* @__PURE__ */ jsxs(
|
|
671
|
+
Field.Root,
|
|
672
|
+
{
|
|
673
|
+
name: "defaultFromEmail",
|
|
674
|
+
error: errorMessage(t, errors.defaultFromEmail),
|
|
675
|
+
hint: t(
|
|
676
|
+
"settings.fromEmail.hint",
|
|
677
|
+
"Reference only. Emails are always sent from the selected account's address."
|
|
678
|
+
),
|
|
679
|
+
children: [
|
|
680
|
+
/* @__PURE__ */ jsx(Field.Label, { children: t("settings.fromEmail.label", "Default From Email") }),
|
|
681
|
+
/* @__PURE__ */ jsx(
|
|
682
|
+
TextInput,
|
|
683
|
+
{
|
|
684
|
+
type: "email",
|
|
685
|
+
autoComplete: "email",
|
|
686
|
+
placeholder: "noreply@example.com",
|
|
687
|
+
value: settings.defaultFromEmail,
|
|
688
|
+
onChange: (e) => handleChange("defaultFromEmail", e.target.value),
|
|
689
|
+
disabled: !canManage
|
|
690
|
+
}
|
|
691
|
+
),
|
|
692
|
+
/* @__PURE__ */ jsx(Field.Hint, {}),
|
|
693
|
+
/* @__PURE__ */ jsx(Field.Error, {})
|
|
694
|
+
]
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
] }) })
|
|
698
|
+
] }),
|
|
699
|
+
/* @__PURE__ */ jsx(UnsavedChangesDialog, { guard })
|
|
700
|
+
] });
|
|
701
|
+
};
|
|
702
|
+
const PluginSettingsPage = () => /* @__PURE__ */ jsx(Page.Protect, { permissions: pluginPermissions.read, children: /* @__PURE__ */ jsx(Page.Main, { children: /* @__PURE__ */ jsx(PluginSettingsContent, {}) }) });
|
|
703
|
+
export {
|
|
704
|
+
PluginSettingsPage as default
|
|
705
|
+
};
|