strapi-plugin-magic-mail 2.2.4 → 2.2.6
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/README.md +0 -2
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -3
- package/admin/jsconfig.json +0 -10
- package/admin/src/components/AddAccountModal.jsx +0 -1943
- package/admin/src/components/Initializer.jsx +0 -14
- package/admin/src/components/LicenseGuard.jsx +0 -475
- package/admin/src/components/PluginIcon.jsx +0 -5
- package/admin/src/hooks/useAuthRefresh.js +0 -44
- package/admin/src/hooks/useLicense.js +0 -158
- package/admin/src/index.js +0 -87
- package/admin/src/pages/Analytics.jsx +0 -762
- package/admin/src/pages/App.jsx +0 -111
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
- package/admin/src/pages/HomePage.jsx +0 -1170
- package/admin/src/pages/LicensePage.jsx +0 -430
- package/admin/src/pages/RoutingRules.jsx +0 -1141
- package/admin/src/pages/Settings.jsx +0 -603
- package/admin/src/pluginId.js +0 -3
- package/admin/src/translations/de.json +0 -71
- package/admin/src/translations/en.json +0 -70
- package/admin/src/translations/es.json +0 -71
- package/admin/src/translations/fr.json +0 -71
- package/admin/src/translations/pt.json +0 -71
- package/admin/src/utils/fetchWithRetry.js +0 -123
- package/admin/src/utils/getTranslation.js +0 -5
- package/admin/src/utils/theme.js +0 -85
- package/server/jsconfig.json +0 -10
- package/server/src/bootstrap.js +0 -157
- package/server/src/config/features.js +0 -260
- package/server/src/config/index.js +0 -9
- package/server/src/content-types/email-account/schema.json +0 -93
- package/server/src/content-types/email-event/index.js +0 -8
- package/server/src/content-types/email-event/schema.json +0 -57
- package/server/src/content-types/email-link/index.js +0 -8
- package/server/src/content-types/email-link/schema.json +0 -49
- package/server/src/content-types/email-log/index.js +0 -8
- package/server/src/content-types/email-log/schema.json +0 -106
- package/server/src/content-types/email-template/schema.json +0 -74
- package/server/src/content-types/email-template-version/schema.json +0 -60
- package/server/src/content-types/index.js +0 -33
- package/server/src/content-types/routing-rule/schema.json +0 -59
- package/server/src/controllers/accounts.js +0 -229
- package/server/src/controllers/analytics.js +0 -361
- package/server/src/controllers/controller.js +0 -26
- package/server/src/controllers/email-designer.js +0 -474
- package/server/src/controllers/index.js +0 -21
- package/server/src/controllers/license.js +0 -269
- package/server/src/controllers/oauth.js +0 -474
- package/server/src/controllers/routing-rules.js +0 -129
- package/server/src/controllers/test.js +0 -301
- package/server/src/destroy.js +0 -27
- package/server/src/index.js +0 -25
- package/server/src/middlewares/index.js +0 -3
- package/server/src/policies/index.js +0 -3
- package/server/src/register.js +0 -5
- package/server/src/routes/admin.js +0 -469
- package/server/src/routes/content-api.js +0 -37
- package/server/src/routes/index.js +0 -9
- package/server/src/services/account-manager.js +0 -329
- package/server/src/services/analytics.js +0 -512
- package/server/src/services/email-designer.js +0 -717
- package/server/src/services/email-router.js +0 -1446
- package/server/src/services/index.js +0 -17
- package/server/src/services/license-guard.js +0 -423
- package/server/src/services/oauth.js +0 -515
- package/server/src/services/service.js +0 -7
- package/server/src/utils/encryption.js +0 -81
- package/server/src/utils/logger.js +0 -84
|
@@ -1,1424 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { useFetchClient, useNotification } from '@strapi/strapi/admin';
|
|
3
|
-
import { useNavigate, useLocation } from 'react-router-dom';
|
|
4
|
-
import { useAuthRefresh } from '../../hooks/useAuthRefresh';
|
|
5
|
-
import styled from 'styled-components';
|
|
6
|
-
import {
|
|
7
|
-
Typography,
|
|
8
|
-
Button,
|
|
9
|
-
Field,
|
|
10
|
-
Tabs,
|
|
11
|
-
Textarea,
|
|
12
|
-
SingleSelect,
|
|
13
|
-
SingleSelectOption,
|
|
14
|
-
Loader,
|
|
15
|
-
Toggle,
|
|
16
|
-
} from '@strapi/design-system';
|
|
17
|
-
import {
|
|
18
|
-
ArrowLeftIcon,
|
|
19
|
-
CheckIcon,
|
|
20
|
-
ArrowDownTrayIcon,
|
|
21
|
-
ArrowUpTrayIcon,
|
|
22
|
-
ClockIcon,
|
|
23
|
-
ArrowUturnLeftIcon,
|
|
24
|
-
XMarkIcon,
|
|
25
|
-
TrashIcon,
|
|
26
|
-
CodeBracketIcon,
|
|
27
|
-
} from '@heroicons/react/24/outline';
|
|
28
|
-
import { useLicense } from '../../hooks/useLicense';
|
|
29
|
-
import * as ReactEmailEditor from 'react-email-editor';
|
|
30
|
-
|
|
31
|
-
const EmailEditorComponent =
|
|
32
|
-
ReactEmailEditor.EmailEditor ||
|
|
33
|
-
ReactEmailEditor.default ||
|
|
34
|
-
ReactEmailEditor;
|
|
35
|
-
|
|
36
|
-
if (!EmailEditorComponent) {
|
|
37
|
-
console.error('[MagicMail] Failed to resolve EmailEditor component export', ReactEmailEditor);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Standard Email Template for Core Emails (when no design exists)
|
|
41
|
-
const STANDARD_EMAIL_TEMPLATE = {
|
|
42
|
-
counters: { u_row: 2, u_content_text: 1, u_content_image: 1, u_column: 2 },
|
|
43
|
-
body: {
|
|
44
|
-
values: {
|
|
45
|
-
backgroundColor: '#ffffff',
|
|
46
|
-
linkStyle: {
|
|
47
|
-
body: true,
|
|
48
|
-
linkHoverColor: '#0000ee',
|
|
49
|
-
linkHoverUnderline: true,
|
|
50
|
-
linkColor: '#0000ee',
|
|
51
|
-
linkUnderline: true,
|
|
52
|
-
},
|
|
53
|
-
contentWidth: '500px',
|
|
54
|
-
backgroundImage: { repeat: false, center: true, fullWidth: true, url: '', cover: false },
|
|
55
|
-
contentAlign: 'center',
|
|
56
|
-
textColor: '#000000',
|
|
57
|
-
_meta: { htmlID: 'u_body', htmlClassNames: 'u_body' },
|
|
58
|
-
fontFamily: { label: 'Arial', value: 'arial,helvetica,sans-serif' },
|
|
59
|
-
preheaderText: '',
|
|
60
|
-
},
|
|
61
|
-
rows: [
|
|
62
|
-
{
|
|
63
|
-
cells: [1],
|
|
64
|
-
values: {
|
|
65
|
-
backgroundImage: { cover: false, url: '', repeat: false, fullWidth: true, center: true },
|
|
66
|
-
hideDesktop: false,
|
|
67
|
-
selectable: true,
|
|
68
|
-
columnsBackgroundColor: '',
|
|
69
|
-
hideable: true,
|
|
70
|
-
backgroundColor: '',
|
|
71
|
-
padding: '0px',
|
|
72
|
-
columns: false,
|
|
73
|
-
_meta: { htmlID: 'u_row_2', htmlClassNames: 'u_row' },
|
|
74
|
-
deletable: true,
|
|
75
|
-
displayCondition: null,
|
|
76
|
-
duplicatable: true,
|
|
77
|
-
draggable: true,
|
|
78
|
-
},
|
|
79
|
-
columns: [
|
|
80
|
-
{
|
|
81
|
-
contents: [
|
|
82
|
-
{
|
|
83
|
-
values: {
|
|
84
|
-
hideDesktop: false,
|
|
85
|
-
duplicatable: true,
|
|
86
|
-
deletable: true,
|
|
87
|
-
linkStyle: {
|
|
88
|
-
linkHoverUnderline: true,
|
|
89
|
-
linkColor: '#0000ee',
|
|
90
|
-
inherit: true,
|
|
91
|
-
linkUnderline: true,
|
|
92
|
-
linkHoverColor: '#0000ee',
|
|
93
|
-
},
|
|
94
|
-
hideable: true,
|
|
95
|
-
lineHeight: '140%',
|
|
96
|
-
draggable: true,
|
|
97
|
-
containerPadding: '10px',
|
|
98
|
-
text: '<p style="font-size: 14px; line-height: 140%; text-align: center;"><span style="font-size: 14px; line-height: 19.6px;">__PLACEHOLDER__</span></p>',
|
|
99
|
-
_meta: { htmlID: 'u_content_text_1', htmlClassNames: 'u_content_text' },
|
|
100
|
-
textAlign: 'left',
|
|
101
|
-
selectable: true,
|
|
102
|
-
},
|
|
103
|
-
type: 'text',
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
values: {
|
|
107
|
-
border: {},
|
|
108
|
-
_meta: { htmlClassNames: 'u_column', htmlID: 'u_column_2' },
|
|
109
|
-
backgroundColor: '',
|
|
110
|
-
padding: '0px',
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
schemaVersion: 6,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Styled components
|
|
121
|
-
const Container = styled.div`
|
|
122
|
-
min-height: 100vh;
|
|
123
|
-
display: flex;
|
|
124
|
-
flex-direction: column;
|
|
125
|
-
background: ${props => props.theme.colors.neutral100};
|
|
126
|
-
`;
|
|
127
|
-
|
|
128
|
-
const Header = styled.div`
|
|
129
|
-
padding: 24px;
|
|
130
|
-
background: ${props => props.theme.colors.neutral0};
|
|
131
|
-
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
132
|
-
`;
|
|
133
|
-
|
|
134
|
-
const HeaderRow = styled.div`
|
|
135
|
-
display: flex;
|
|
136
|
-
justify-content: space-between;
|
|
137
|
-
align-items: flex-start;
|
|
138
|
-
margin-bottom: 16px;
|
|
139
|
-
`;
|
|
140
|
-
|
|
141
|
-
const HeaderLeft = styled.div`
|
|
142
|
-
display: flex;
|
|
143
|
-
gap: 12px;
|
|
144
|
-
align-items: flex-start;
|
|
145
|
-
`;
|
|
146
|
-
|
|
147
|
-
const TitleContainer = styled.div`
|
|
148
|
-
display: flex;
|
|
149
|
-
flex-direction: column;
|
|
150
|
-
gap: 4px;
|
|
151
|
-
`;
|
|
152
|
-
|
|
153
|
-
const HeaderRight = styled.div`
|
|
154
|
-
display: flex;
|
|
155
|
-
gap: 8px;
|
|
156
|
-
`;
|
|
157
|
-
|
|
158
|
-
const SettingsRow = styled.div`
|
|
159
|
-
display: flex;
|
|
160
|
-
gap: 16px;
|
|
161
|
-
align-items: flex-end;
|
|
162
|
-
`;
|
|
163
|
-
|
|
164
|
-
const FieldWrapper = styled.div`
|
|
165
|
-
flex: ${(props) => props.flex || 'initial'};
|
|
166
|
-
width: ${(props) => props.width || 'auto'};
|
|
167
|
-
`;
|
|
168
|
-
|
|
169
|
-
const ToggleWrapper = styled.div`
|
|
170
|
-
padding-top: 28px;
|
|
171
|
-
display: flex;
|
|
172
|
-
gap: 12px;
|
|
173
|
-
align-items: center;
|
|
174
|
-
|
|
175
|
-
/* Custom green styling for active toggle */
|
|
176
|
-
button[aria-checked="true"] {
|
|
177
|
-
background-color: #22C55E !important;
|
|
178
|
-
border-color: #22C55E !important;
|
|
179
|
-
|
|
180
|
-
span {
|
|
181
|
-
background-color: white !important;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
button[aria-checked="false"] {
|
|
186
|
-
background-color: #E5E7EB !important;
|
|
187
|
-
border-color: #D1D5DB !important;
|
|
188
|
-
|
|
189
|
-
span {
|
|
190
|
-
background-color: white !important;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/* Label styling based on state */
|
|
195
|
-
p {
|
|
196
|
-
color: ${props => props.$isActive ? '#22C55E' : '#6B7280'};
|
|
197
|
-
font-weight: 600;
|
|
198
|
-
transition: color 0.2s;
|
|
199
|
-
}
|
|
200
|
-
`;
|
|
201
|
-
|
|
202
|
-
const TabsWrapper = styled.div`
|
|
203
|
-
flex: 1;
|
|
204
|
-
display: flex;
|
|
205
|
-
flex-direction: column;
|
|
206
|
-
`;
|
|
207
|
-
|
|
208
|
-
const TabListWrapper = styled.div`
|
|
209
|
-
padding: 0 24px;
|
|
210
|
-
background: ${props => props.theme.colors.neutral0};
|
|
211
|
-
border-bottom: 1px solid ${props => props.theme.colors.neutral200};
|
|
212
|
-
`;
|
|
213
|
-
|
|
214
|
-
const StyledTabsRoot = styled(Tabs.Root)`
|
|
215
|
-
display: flex;
|
|
216
|
-
flex-direction: column;
|
|
217
|
-
height: 100%;
|
|
218
|
-
`;
|
|
219
|
-
|
|
220
|
-
const StyledTabsContent = styled(Tabs.Content)`
|
|
221
|
-
flex: 1;
|
|
222
|
-
display: flex;
|
|
223
|
-
flex-direction: column;
|
|
224
|
-
`;
|
|
225
|
-
|
|
226
|
-
const TabContentWrapper = styled.div`
|
|
227
|
-
height: calc(100vh - 240px);
|
|
228
|
-
background: ${props => props.theme.colors.neutral0};
|
|
229
|
-
position: relative;
|
|
230
|
-
`;
|
|
231
|
-
|
|
232
|
-
const TextTabContent = styled.div`
|
|
233
|
-
padding: 20px;
|
|
234
|
-
height: calc(100vh - 240px);
|
|
235
|
-
|
|
236
|
-
textarea {
|
|
237
|
-
width: 100%;
|
|
238
|
-
height: 100%;
|
|
239
|
-
min-height: 500px;
|
|
240
|
-
font-family: monospace;
|
|
241
|
-
}
|
|
242
|
-
`;
|
|
243
|
-
|
|
244
|
-
const LoadingContainer = styled.div`
|
|
245
|
-
padding: 80px 20px;
|
|
246
|
-
display: flex;
|
|
247
|
-
justify-content: center;
|
|
248
|
-
align-items: center;
|
|
249
|
-
`;
|
|
250
|
-
|
|
251
|
-
const EditorCanvas = styled.div`
|
|
252
|
-
min-height: calc(100vh - 240px);
|
|
253
|
-
`;
|
|
254
|
-
|
|
255
|
-
const DesignerLoadingContainer = styled(LoadingContainer)`
|
|
256
|
-
width: 100%;
|
|
257
|
-
min-height: calc(100vh - 240px);
|
|
258
|
-
padding: 40px 20px;
|
|
259
|
-
`;
|
|
260
|
-
|
|
261
|
-
const HiddenInput = styled.input`
|
|
262
|
-
display: none;
|
|
263
|
-
`;
|
|
264
|
-
|
|
265
|
-
const SaveButton = styled(Button)`
|
|
266
|
-
background: linear-gradient(135deg, #22C55E 0%, #16A34A 100%);
|
|
267
|
-
border: none;
|
|
268
|
-
color: white;
|
|
269
|
-
font-weight: 600;
|
|
270
|
-
font-size: 13px;
|
|
271
|
-
padding: 8px 16px;
|
|
272
|
-
height: 36px;
|
|
273
|
-
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
|
|
274
|
-
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
275
|
-
|
|
276
|
-
&:hover {
|
|
277
|
-
transform: translateY(-1px);
|
|
278
|
-
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
|
|
279
|
-
background: linear-gradient(135deg, #16A34A 0%, #15803D 100%);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
&:active {
|
|
283
|
-
transform: translateY(0);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
&:disabled {
|
|
287
|
-
opacity: 0.6;
|
|
288
|
-
cursor: not-allowed;
|
|
289
|
-
&:hover {
|
|
290
|
-
transform: none;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
svg {
|
|
295
|
-
width: 14px;
|
|
296
|
-
height: 14px;
|
|
297
|
-
}
|
|
298
|
-
`;
|
|
299
|
-
|
|
300
|
-
const ImportExportButton = styled.span`
|
|
301
|
-
display: inline-flex;
|
|
302
|
-
align-items: center;
|
|
303
|
-
justify-content: center;
|
|
304
|
-
gap: 6px;
|
|
305
|
-
padding: 8px 16px;
|
|
306
|
-
height: 36px;
|
|
307
|
-
background: ${props => props.theme.colors.neutral0};
|
|
308
|
-
border: 1px solid ${props => props.theme.colors.neutral200};
|
|
309
|
-
border-radius: 4px;
|
|
310
|
-
color: ${props => props.theme.colors.neutral800};
|
|
311
|
-
font-weight: 500;
|
|
312
|
-
font-size: 13px;
|
|
313
|
-
cursor: pointer;
|
|
314
|
-
transition: all 200ms;
|
|
315
|
-
white-space: nowrap;
|
|
316
|
-
|
|
317
|
-
&:hover {
|
|
318
|
-
background: ${props => props.theme.colors.neutral100};
|
|
319
|
-
border-color: ${props => props.theme.colors.primary600};
|
|
320
|
-
color: ${props => props.theme.colors.primary600};
|
|
321
|
-
transform: translateY(-1px);
|
|
322
|
-
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.15);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
&:active {
|
|
326
|
-
transform: translateY(0);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
svg {
|
|
330
|
-
width: 14px;
|
|
331
|
-
height: 14px;
|
|
332
|
-
}
|
|
333
|
-
`;
|
|
334
|
-
|
|
335
|
-
const ImportLabel = styled.label`
|
|
336
|
-
cursor: pointer;
|
|
337
|
-
display: inline-block;
|
|
338
|
-
`;
|
|
339
|
-
|
|
340
|
-
const BackButton = styled.button`
|
|
341
|
-
background: ${props => props.theme.colors.neutral0};
|
|
342
|
-
border: 1px solid ${props => props.theme.colors.neutral200};
|
|
343
|
-
border-radius: 4px;
|
|
344
|
-
padding: 8px 10px;
|
|
345
|
-
height: 36px;
|
|
346
|
-
cursor: pointer;
|
|
347
|
-
display: flex;
|
|
348
|
-
align-items: center;
|
|
349
|
-
justify-content: center;
|
|
350
|
-
transition: all 200ms;
|
|
351
|
-
|
|
352
|
-
&:hover {
|
|
353
|
-
background: ${props => props.theme.colors.neutral100};
|
|
354
|
-
border-color: ${props => props.theme.colors.neutral300};
|
|
355
|
-
transform: translateY(-1px);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
&:active {
|
|
359
|
-
transform: translateY(0);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
svg {
|
|
363
|
-
width: 16px;
|
|
364
|
-
height: 16px;
|
|
365
|
-
}
|
|
366
|
-
`;
|
|
367
|
-
|
|
368
|
-
const VersionButton = styled.button`
|
|
369
|
-
background: ${props => props.theme.colors.neutral0};
|
|
370
|
-
border: 1px solid ${props => props.theme.colors.neutral200};
|
|
371
|
-
border-radius: 4px;
|
|
372
|
-
padding: 8px 16px;
|
|
373
|
-
height: 36px;
|
|
374
|
-
cursor: pointer;
|
|
375
|
-
display: inline-flex;
|
|
376
|
-
align-items: center;
|
|
377
|
-
justify-content: center;
|
|
378
|
-
gap: 6px;
|
|
379
|
-
transition: all 200ms;
|
|
380
|
-
font-size: 13px;
|
|
381
|
-
font-weight: 500;
|
|
382
|
-
color: ${props => props.theme.colors.neutral800};
|
|
383
|
-
white-space: nowrap;
|
|
384
|
-
|
|
385
|
-
&:hover {
|
|
386
|
-
background: ${props => props.theme.colors.neutral100};
|
|
387
|
-
border-color: ${props => props.theme.colors.primary600};
|
|
388
|
-
color: ${props => props.theme.colors.primary600};
|
|
389
|
-
transform: translateY(-1px);
|
|
390
|
-
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.15);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
&:active {
|
|
394
|
-
transform: translateY(0);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
svg {
|
|
398
|
-
width: 14px;
|
|
399
|
-
height: 14px;
|
|
400
|
-
}
|
|
401
|
-
`;
|
|
402
|
-
|
|
403
|
-
// Version History Modal
|
|
404
|
-
const VersionModal = styled.div`
|
|
405
|
-
position: fixed;
|
|
406
|
-
top: 0;
|
|
407
|
-
right: ${props => props.$isOpen ? '0' : '-450px'};
|
|
408
|
-
width: 450px;
|
|
409
|
-
height: 100vh;
|
|
410
|
-
background: ${props => props.theme.colors.neutral0};
|
|
411
|
-
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
|
412
|
-
z-index: 9999;
|
|
413
|
-
transition: right 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
414
|
-
display: flex;
|
|
415
|
-
flex-direction: column;
|
|
416
|
-
`;
|
|
417
|
-
|
|
418
|
-
const VersionModalOverlay = styled.div`
|
|
419
|
-
position: fixed;
|
|
420
|
-
top: 0;
|
|
421
|
-
left: 0;
|
|
422
|
-
right: 0;
|
|
423
|
-
bottom: 0;
|
|
424
|
-
background: rgba(0, 0, 0, 0.4);
|
|
425
|
-
z-index: 9998;
|
|
426
|
-
opacity: ${props => props.$isOpen ? '1' : '0'};
|
|
427
|
-
pointer-events: ${props => props.$isOpen ? 'auto' : 'none'};
|
|
428
|
-
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
429
|
-
`;
|
|
430
|
-
|
|
431
|
-
const VersionModalHeader = styled.div`
|
|
432
|
-
padding: 24px;
|
|
433
|
-
border-bottom: 1px solid ${props => props.theme.colors.neutral200};
|
|
434
|
-
display: flex;
|
|
435
|
-
justify-content: space-between;
|
|
436
|
-
align-items: center;
|
|
437
|
-
`;
|
|
438
|
-
|
|
439
|
-
const VersionModalContent = styled.div`
|
|
440
|
-
flex: 1;
|
|
441
|
-
overflow-y: auto;
|
|
442
|
-
padding: 16px;
|
|
443
|
-
`;
|
|
444
|
-
|
|
445
|
-
const VersionItem = styled.div`
|
|
446
|
-
padding: 16px;
|
|
447
|
-
border: 1px solid ${props => props.theme.colors.neutral200};
|
|
448
|
-
border-radius: 8px;
|
|
449
|
-
margin-bottom: 12px;
|
|
450
|
-
transition: all 150ms;
|
|
451
|
-
|
|
452
|
-
&:hover {
|
|
453
|
-
border-color: ${props => props.theme.colors.primary600};
|
|
454
|
-
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.15);
|
|
455
|
-
}
|
|
456
|
-
`;
|
|
457
|
-
|
|
458
|
-
const VersionItemHeader = styled.div`
|
|
459
|
-
display: flex;
|
|
460
|
-
justify-content: space-between;
|
|
461
|
-
align-items: center;
|
|
462
|
-
margin-bottom: 8px;
|
|
463
|
-
`;
|
|
464
|
-
|
|
465
|
-
const VersionNumber = styled.div`
|
|
466
|
-
font-weight: 600;
|
|
467
|
-
color: ${props => props.theme.colors.neutral800};
|
|
468
|
-
display: flex;
|
|
469
|
-
align-items: center;
|
|
470
|
-
gap: 8px;
|
|
471
|
-
`;
|
|
472
|
-
|
|
473
|
-
const VersionBadge = styled.span`
|
|
474
|
-
background: linear-gradient(135deg, #0EA5E9 0%, #0284C7 100%);
|
|
475
|
-
color: white;
|
|
476
|
-
padding: 2px 8px;
|
|
477
|
-
border-radius: 4px;
|
|
478
|
-
font-size: 12px;
|
|
479
|
-
font-weight: 600;
|
|
480
|
-
`;
|
|
481
|
-
|
|
482
|
-
const VersionDate = styled.div`
|
|
483
|
-
font-size: 13px;
|
|
484
|
-
color: ${props => props.theme.colors.neutral600};
|
|
485
|
-
`;
|
|
486
|
-
|
|
487
|
-
const VersionMeta = styled.div`
|
|
488
|
-
font-size: 13px;
|
|
489
|
-
color: ${props => props.theme.colors.neutral600};
|
|
490
|
-
margin-bottom: 12px;
|
|
491
|
-
`;
|
|
492
|
-
|
|
493
|
-
const VersionActions = styled.div`
|
|
494
|
-
display: flex;
|
|
495
|
-
gap: 8px;
|
|
496
|
-
`;
|
|
497
|
-
|
|
498
|
-
const RestoreButton = styled(Button)`
|
|
499
|
-
background: linear-gradient(135deg, #22C55E 0%, #16A34A 100%);
|
|
500
|
-
border: none;
|
|
501
|
-
color: white;
|
|
502
|
-
font-size: 13px;
|
|
503
|
-
padding: 8px 16px;
|
|
504
|
-
|
|
505
|
-
&:hover {
|
|
506
|
-
background: linear-gradient(135deg, #4ADE80 0%, #22C55E 100%);
|
|
507
|
-
transform: translateY(-1px);
|
|
508
|
-
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
|
509
|
-
border-color: transparent;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
svg {
|
|
513
|
-
width: 14px;
|
|
514
|
-
height: 14px;
|
|
515
|
-
}
|
|
516
|
-
`;
|
|
517
|
-
|
|
518
|
-
const DeleteButton = styled(Button)`
|
|
519
|
-
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
|
|
520
|
-
border: none;
|
|
521
|
-
color: white;
|
|
522
|
-
font-size: 13px;
|
|
523
|
-
padding: 8px 16px;
|
|
524
|
-
|
|
525
|
-
&:hover {
|
|
526
|
-
background: linear-gradient(135deg, #F87171 0%, #EF4444 100%);
|
|
527
|
-
transform: translateY(-1px);
|
|
528
|
-
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
|
529
|
-
border-color: transparent;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
svg {
|
|
533
|
-
width: 14px;
|
|
534
|
-
height: 14px;
|
|
535
|
-
}
|
|
536
|
-
`;
|
|
537
|
-
|
|
538
|
-
const CloseButton = styled.button`
|
|
539
|
-
background: none;
|
|
540
|
-
border: none;
|
|
541
|
-
cursor: pointer;
|
|
542
|
-
padding: 4px;
|
|
543
|
-
display: flex;
|
|
544
|
-
align-items: center;
|
|
545
|
-
justify-content: center;
|
|
546
|
-
color: ${props => props.theme.colors.neutral600};
|
|
547
|
-
transition: all 150ms;
|
|
548
|
-
|
|
549
|
-
&:hover {
|
|
550
|
-
color: ${props => props.theme.colors.neutral800};
|
|
551
|
-
background: ${props => props.theme.colors.neutral100};
|
|
552
|
-
border-radius: 4px;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
svg {
|
|
556
|
-
width: 20px;
|
|
557
|
-
height: 20px;
|
|
558
|
-
}
|
|
559
|
-
`;
|
|
560
|
-
|
|
561
|
-
const EmptyVersions = styled.div`
|
|
562
|
-
text-align: center;
|
|
563
|
-
padding: 60px 20px;
|
|
564
|
-
color: ${props => props.theme.colors.neutral600};
|
|
565
|
-
display: flex;
|
|
566
|
-
flex-direction: column;
|
|
567
|
-
align-items: center;
|
|
568
|
-
gap: 8px;
|
|
569
|
-
|
|
570
|
-
svg {
|
|
571
|
-
width: 64px;
|
|
572
|
-
height: 64px;
|
|
573
|
-
margin-bottom: 16px;
|
|
574
|
-
color: ${props => props.theme.colors.neutral300};
|
|
575
|
-
}
|
|
576
|
-
`;
|
|
577
|
-
|
|
578
|
-
const EditorPage = () => {
|
|
579
|
-
useAuthRefresh(); // Initialize token auto-refresh
|
|
580
|
-
const location = useLocation();
|
|
581
|
-
const { get, post, put } = useFetchClient();
|
|
582
|
-
const { toggleNotification } = useNotification();
|
|
583
|
-
const navigate = useNavigate();
|
|
584
|
-
const { hasFeature } = useLicense();
|
|
585
|
-
const emailEditorRef = useRef(null);
|
|
586
|
-
|
|
587
|
-
// Extract ID from pathname
|
|
588
|
-
const pathname = location.pathname;
|
|
589
|
-
const coreMatch = pathname.match(/\/designer\/core\/(.+)$/);
|
|
590
|
-
const templateMatch = pathname.match(/\/designer\/(.+)$/);
|
|
591
|
-
|
|
592
|
-
const isCoreEmail = !!coreMatch;
|
|
593
|
-
const coreEmailType = coreMatch ? coreMatch[1] : null;
|
|
594
|
-
const id = !isCoreEmail && templateMatch ? templateMatch[1] : null;
|
|
595
|
-
|
|
596
|
-
const isNewTemplate = id === 'new';
|
|
597
|
-
|
|
598
|
-
const [loading, setLoading] = useState(!isNewTemplate && !isCoreEmail);
|
|
599
|
-
const [saving, setSaving] = useState(false);
|
|
600
|
-
const [activeTab, setActiveTab] = useState('html');
|
|
601
|
-
const [editorLoaded, setEditorLoaded] = useState(false);
|
|
602
|
-
|
|
603
|
-
const [templateData, setTemplateData] = useState({
|
|
604
|
-
templateReferenceId: '',
|
|
605
|
-
name: '',
|
|
606
|
-
subject: '',
|
|
607
|
-
category: 'custom',
|
|
608
|
-
isActive: true,
|
|
609
|
-
design: null,
|
|
610
|
-
bodyHtml: '',
|
|
611
|
-
bodyText: '',
|
|
612
|
-
tags: [],
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
const canVersion = hasFeature('email-designer-versioning');
|
|
616
|
-
|
|
617
|
-
// Version History State
|
|
618
|
-
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
|
619
|
-
const [versions, setVersions] = useState([]);
|
|
620
|
-
const [loadingVersions, setLoadingVersions] = useState(false);
|
|
621
|
-
|
|
622
|
-
useEffect(() => {
|
|
623
|
-
if (isCoreEmail) {
|
|
624
|
-
fetchCoreTemplate();
|
|
625
|
-
} else if (!isNewTemplate && id) {
|
|
626
|
-
fetchTemplate();
|
|
627
|
-
}
|
|
628
|
-
}, [id, isCoreEmail, coreEmailType]);
|
|
629
|
-
|
|
630
|
-
const fetchCoreTemplate = async () => {
|
|
631
|
-
setLoading(true);
|
|
632
|
-
try {
|
|
633
|
-
const response = await get(`/magic-mail/designer/core/${coreEmailType}`);
|
|
634
|
-
const coreTemplate = response.data?.data;
|
|
635
|
-
|
|
636
|
-
let design = coreTemplate?.design;
|
|
637
|
-
|
|
638
|
-
// Convert old HTML message to Unlayer design if no design exists
|
|
639
|
-
if (!design && coreTemplate?.message) {
|
|
640
|
-
let message = coreTemplate.message;
|
|
641
|
-
|
|
642
|
-
// Check if message contains HTML body tag
|
|
643
|
-
if (message.match(/<body/)) {
|
|
644
|
-
const parser = new DOMParser();
|
|
645
|
-
const parsedDocument = parser.parseFromString(message, 'text/html');
|
|
646
|
-
message = parsedDocument.body.innerText;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Strip HTML tags except for specific ones
|
|
650
|
-
message = message
|
|
651
|
-
.replace(/<(?!\/?(?:a|img|strong|b|i|%|%=)\b)[^>]+>/gi, '')
|
|
652
|
-
.replace(/"/g, "'")
|
|
653
|
-
.replace(/\n/g, '<br />');
|
|
654
|
-
|
|
655
|
-
// Create design from template
|
|
656
|
-
const templateStr = JSON.stringify(STANDARD_EMAIL_TEMPLATE);
|
|
657
|
-
design = JSON.parse(templateStr.replace('__PLACEHOLDER__', message));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
setTemplateData({
|
|
661
|
-
templateReferenceId: '',
|
|
662
|
-
name: coreEmailType === 'reset-password' ? 'Reset Password' : 'Email Confirmation',
|
|
663
|
-
subject: coreTemplate?.subject || '',
|
|
664
|
-
category: 'transactional',
|
|
665
|
-
isActive: true,
|
|
666
|
-
design: design,
|
|
667
|
-
bodyHtml: coreTemplate?.bodyHtml || coreTemplate?.message || '',
|
|
668
|
-
bodyText: coreTemplate?.bodyText || '',
|
|
669
|
-
tags: [],
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
// Load design into editor after a short delay
|
|
673
|
-
setTimeout(() => {
|
|
674
|
-
if (design && emailEditorRef.current?.editor) {
|
|
675
|
-
emailEditorRef.current.editor.loadDesign(design);
|
|
676
|
-
}
|
|
677
|
-
}, 600);
|
|
678
|
-
} catch (error) {
|
|
679
|
-
console.error('[MagicMail] Error loading core template:', error);
|
|
680
|
-
toggleNotification({
|
|
681
|
-
type: 'danger',
|
|
682
|
-
message: 'Failed to load core template',
|
|
683
|
-
});
|
|
684
|
-
} finally {
|
|
685
|
-
setLoading(false);
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
const fetchTemplate = async () => {
|
|
690
|
-
setLoading(true);
|
|
691
|
-
try {
|
|
692
|
-
const response = await get(`/magic-mail/designer/templates/${id}`);
|
|
693
|
-
const template = response.data?.data;
|
|
694
|
-
setTemplateData(template);
|
|
695
|
-
|
|
696
|
-
// Load design into editor
|
|
697
|
-
setTimeout(() => {
|
|
698
|
-
if (template.design && emailEditorRef.current?.editor) {
|
|
699
|
-
emailEditorRef.current.editor.loadDesign(template.design);
|
|
700
|
-
}
|
|
701
|
-
}, 500);
|
|
702
|
-
} catch (error) {
|
|
703
|
-
toggleNotification({ type: 'danger', message: 'Failed to load template' });
|
|
704
|
-
navigate('/plugins/magic-mail/designer');
|
|
705
|
-
} finally {
|
|
706
|
-
setLoading(false);
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// Load version history
|
|
711
|
-
const fetchVersions = async () => {
|
|
712
|
-
if (!id || isNewTemplate || isCoreEmail) return;
|
|
713
|
-
|
|
714
|
-
setLoadingVersions(true);
|
|
715
|
-
try {
|
|
716
|
-
const response = await get(`/magic-mail/designer/templates/${id}/versions`);
|
|
717
|
-
if (response.data?.success) {
|
|
718
|
-
setVersions(response.data.data || []);
|
|
719
|
-
}
|
|
720
|
-
} catch (error) {
|
|
721
|
-
console.error('[Version History] Error loading versions:', error);
|
|
722
|
-
toggleNotification({
|
|
723
|
-
type: 'danger',
|
|
724
|
-
message: 'Failed to load version history',
|
|
725
|
-
});
|
|
726
|
-
} finally {
|
|
727
|
-
setLoadingVersions(false);
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
// Restore version
|
|
732
|
-
const handleRestoreVersion = async (versionId, versionNumber) => {
|
|
733
|
-
if (!window.confirm(`Restore template to Version #${versionNumber}? Current content will be saved as a new version.`)) {
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
try {
|
|
738
|
-
const response = await post(`/magic-mail/designer/templates/${id}/versions/${versionId}/restore`);
|
|
739
|
-
|
|
740
|
-
if (response.data?.success) {
|
|
741
|
-
toggleNotification({
|
|
742
|
-
type: 'success',
|
|
743
|
-
message: `Restored to Version #${versionNumber}`,
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// Reload template data
|
|
747
|
-
await fetchTemplate();
|
|
748
|
-
|
|
749
|
-
// Reload versions
|
|
750
|
-
await fetchVersions();
|
|
751
|
-
|
|
752
|
-
// Close modal
|
|
753
|
-
setShowVersionHistory(false);
|
|
754
|
-
}
|
|
755
|
-
} catch (error) {
|
|
756
|
-
console.error('[Version History] Error restoring version:', error);
|
|
757
|
-
toggleNotification({
|
|
758
|
-
type: 'danger',
|
|
759
|
-
message: 'Failed to restore version',
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
// Delete version
|
|
765
|
-
const handleDeleteVersion = async (versionId, versionNumber) => {
|
|
766
|
-
if (!window.confirm(`Delete Version #${versionNumber}? This action cannot be undone.`)) {
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
try {
|
|
771
|
-
const response = await post(`/magic-mail/designer/templates/${id}/versions/${versionId}/delete`);
|
|
772
|
-
|
|
773
|
-
if (response.data?.success) {
|
|
774
|
-
toggleNotification({
|
|
775
|
-
type: 'success',
|
|
776
|
-
message: `Version #${versionNumber} deleted`,
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// Reload versions
|
|
780
|
-
await fetchVersions();
|
|
781
|
-
}
|
|
782
|
-
} catch (error) {
|
|
783
|
-
console.error('[Version History] Error deleting version:', error);
|
|
784
|
-
toggleNotification({
|
|
785
|
-
type: 'danger',
|
|
786
|
-
message: 'Failed to delete version',
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
// Delete all versions
|
|
792
|
-
const handleDeleteAllVersions = async () => {
|
|
793
|
-
if (versions.length === 0) {
|
|
794
|
-
toggleNotification({
|
|
795
|
-
type: 'info',
|
|
796
|
-
message: 'No versions to delete',
|
|
797
|
-
});
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (!window.confirm(`Delete ALL ${versions.length} versions? This action cannot be undone.`)) {
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
const response = await post(`/magic-mail/designer/templates/${id}/versions/delete-all`);
|
|
807
|
-
|
|
808
|
-
if (response.data?.success) {
|
|
809
|
-
toggleNotification({
|
|
810
|
-
type: 'success',
|
|
811
|
-
message: `Deleted ${versions.length} versions`,
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
// Reload versions
|
|
815
|
-
await fetchVersions();
|
|
816
|
-
}
|
|
817
|
-
} catch (error) {
|
|
818
|
-
console.error('[Version History] Error deleting all versions:', error);
|
|
819
|
-
toggleNotification({
|
|
820
|
-
type: 'danger',
|
|
821
|
-
message: 'Failed to delete all versions',
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
// Open version history and load versions
|
|
827
|
-
const handleOpenVersionHistory = () => {
|
|
828
|
-
setShowVersionHistory(true);
|
|
829
|
-
fetchVersions();
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
const handleSave = async () => {
|
|
833
|
-
// Validation (skip for core emails)
|
|
834
|
-
if (!isCoreEmail) {
|
|
835
|
-
if (!templateData.templateReferenceId) {
|
|
836
|
-
toggleNotification({ type: 'warning', message: 'Reference ID is required' });
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
if (!templateData.name) {
|
|
840
|
-
toggleNotification({ type: 'warning', message: 'Name is required' });
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (!templateData.subject) {
|
|
846
|
-
toggleNotification({ type: 'warning', message: 'Subject is required' });
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
setSaving(true);
|
|
851
|
-
|
|
852
|
-
try {
|
|
853
|
-
let design = templateData.design;
|
|
854
|
-
let bodyHtml = templateData.bodyHtml;
|
|
855
|
-
|
|
856
|
-
if (activeTab === 'html' && emailEditorRef.current?.editor) {
|
|
857
|
-
await new Promise((resolve) => {
|
|
858
|
-
emailEditorRef.current.editor.exportHtml((data) => {
|
|
859
|
-
design = data.design;
|
|
860
|
-
bodyHtml = data.html;
|
|
861
|
-
resolve();
|
|
862
|
-
});
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Core emails - save to Strapi config
|
|
867
|
-
if (isCoreEmail) {
|
|
868
|
-
const corePayload = {
|
|
869
|
-
subject: templateData.subject,
|
|
870
|
-
design,
|
|
871
|
-
message: bodyHtml, // Send as 'message' not 'bodyHtml'
|
|
872
|
-
bodyText: activeTab === 'text' ? templateData.bodyText : '', // Include text version
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
await put(`/magic-mail/designer/core/${coreEmailType}`, corePayload);
|
|
876
|
-
|
|
877
|
-
toggleNotification({
|
|
878
|
-
type: 'success',
|
|
879
|
-
message: 'Core email template saved!',
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
setSaving(false);
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const payload = {
|
|
887
|
-
...templateData,
|
|
888
|
-
design,
|
|
889
|
-
bodyHtml,
|
|
890
|
-
templateReferenceId: parseInt(templateData.templateReferenceId),
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
let response;
|
|
894
|
-
if (isNewTemplate) {
|
|
895
|
-
response = await post('/magic-mail/designer/templates', payload);
|
|
896
|
-
} else {
|
|
897
|
-
response = await put(`/magic-mail/designer/templates/${id}`, payload);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
toggleNotification({
|
|
901
|
-
type: 'success',
|
|
902
|
-
message: isNewTemplate ? 'Template created!' : 'Template saved!',
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
if (isNewTemplate && response.data?.data?.id) {
|
|
906
|
-
navigate(`/plugins/magic-mail/designer/${response.data.data.id}`);
|
|
907
|
-
}
|
|
908
|
-
} catch (error) {
|
|
909
|
-
toggleNotification({
|
|
910
|
-
type: 'danger',
|
|
911
|
-
message: error.response?.data?.message || 'Failed to save',
|
|
912
|
-
});
|
|
913
|
-
} finally {
|
|
914
|
-
setSaving(false);
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
|
|
918
|
-
const handleExportDesign = async () => {
|
|
919
|
-
if (!emailEditorRef.current?.editor) return;
|
|
920
|
-
|
|
921
|
-
emailEditorRef.current.editor.exportHtml((data) => {
|
|
922
|
-
const dataStr = JSON.stringify(data.design, null, 2);
|
|
923
|
-
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
924
|
-
const url = URL.createObjectURL(dataBlob);
|
|
925
|
-
const link = document.createElement('a');
|
|
926
|
-
link.href = url;
|
|
927
|
-
link.download = `${templateData.name || 'template'}-design.json`;
|
|
928
|
-
link.click();
|
|
929
|
-
URL.revokeObjectURL(url);
|
|
930
|
-
toggleNotification({ type: 'success', message: 'Design exported!' });
|
|
931
|
-
});
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const handleImportDesign = (event) => {
|
|
935
|
-
const file = event.target.files[0];
|
|
936
|
-
if (!file) return;
|
|
937
|
-
|
|
938
|
-
const reader = new FileReader();
|
|
939
|
-
reader.onload = (e) => {
|
|
940
|
-
try {
|
|
941
|
-
const design = JSON.parse(e.target.result);
|
|
942
|
-
if (emailEditorRef.current?.editor) {
|
|
943
|
-
emailEditorRef.current.editor.loadDesign(design);
|
|
944
|
-
toggleNotification({ type: 'success', message: 'Design imported!' });
|
|
945
|
-
}
|
|
946
|
-
} catch (error) {
|
|
947
|
-
toggleNotification({ type: 'danger', message: 'Invalid design file' });
|
|
948
|
-
}
|
|
949
|
-
};
|
|
950
|
-
reader.readAsText(file);
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
const onEditorReady = () => {
|
|
954
|
-
setEditorLoaded(true);
|
|
955
|
-
if (templateData.design && emailEditorRef.current?.editor) {
|
|
956
|
-
setTimeout(() => {
|
|
957
|
-
emailEditorRef.current.editor.loadDesign(templateData.design);
|
|
958
|
-
}, 100);
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
|
|
962
|
-
if (loading) {
|
|
963
|
-
return (
|
|
964
|
-
<Container>
|
|
965
|
-
<LoadingContainer>
|
|
966
|
-
<Loader>Loading template...</Loader>
|
|
967
|
-
</LoadingContainer>
|
|
968
|
-
</Container>
|
|
969
|
-
);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return (
|
|
973
|
-
<Container>
|
|
974
|
-
{/* Header */}
|
|
975
|
-
<Header>
|
|
976
|
-
<HeaderRow>
|
|
977
|
-
<HeaderLeft>
|
|
978
|
-
<BackButton onClick={() => navigate('/plugins/magic-mail/designer')}>
|
|
979
|
-
<ArrowLeftIcon />
|
|
980
|
-
</BackButton>
|
|
981
|
-
<TitleContainer>
|
|
982
|
-
<Typography variant="alpha">
|
|
983
|
-
{isCoreEmail
|
|
984
|
-
? `${coreEmailType === 'reset-password' ? 'Reset Password' : 'Email Confirmation'}`
|
|
985
|
-
: isNewTemplate
|
|
986
|
-
? 'New Template'
|
|
987
|
-
: `${templateData.name}`
|
|
988
|
-
}
|
|
989
|
-
</Typography>
|
|
990
|
-
{canVersion && !isNewTemplate && !isCoreEmail && (
|
|
991
|
-
<Typography variant="pi" textColor="neutral600">
|
|
992
|
-
Versioning enabled
|
|
993
|
-
</Typography>
|
|
994
|
-
)}
|
|
995
|
-
{isCoreEmail && (
|
|
996
|
-
<Typography variant="pi" textColor="neutral600">
|
|
997
|
-
Core Strapi Email Template
|
|
998
|
-
</Typography>
|
|
999
|
-
)}
|
|
1000
|
-
</TitleContainer>
|
|
1001
|
-
</HeaderLeft>
|
|
1002
|
-
|
|
1003
|
-
<HeaderRight>
|
|
1004
|
-
<ImportLabel>
|
|
1005
|
-
<ImportExportButton>
|
|
1006
|
-
<ArrowUpTrayIcon />
|
|
1007
|
-
Import Design
|
|
1008
|
-
</ImportExportButton>
|
|
1009
|
-
<HiddenInput type="file" accept=".json" onChange={handleImportDesign} />
|
|
1010
|
-
</ImportLabel>
|
|
1011
|
-
<ImportExportButton onClick={handleExportDesign} as="button">
|
|
1012
|
-
<ArrowDownTrayIcon />
|
|
1013
|
-
Export Design
|
|
1014
|
-
</ImportExportButton>
|
|
1015
|
-
{!isCoreEmail && !isNewTemplate && canVersion && (
|
|
1016
|
-
<VersionButton onClick={handleOpenVersionHistory}>
|
|
1017
|
-
<ClockIcon />
|
|
1018
|
-
Version History
|
|
1019
|
-
</VersionButton>
|
|
1020
|
-
)}
|
|
1021
|
-
<SaveButton
|
|
1022
|
-
startIcon={<CheckIcon />}
|
|
1023
|
-
onClick={handleSave}
|
|
1024
|
-
loading={saving}
|
|
1025
|
-
disabled={saving}
|
|
1026
|
-
>
|
|
1027
|
-
{saving ? 'Saving...' : 'Save Template'}
|
|
1028
|
-
</SaveButton>
|
|
1029
|
-
</HeaderRight>
|
|
1030
|
-
</HeaderRow>
|
|
1031
|
-
|
|
1032
|
-
{/* Settings */}
|
|
1033
|
-
<SettingsRow>
|
|
1034
|
-
{!isCoreEmail && (
|
|
1035
|
-
<FieldWrapper width="150px">
|
|
1036
|
-
<Field.Root required>
|
|
1037
|
-
<Field.Label>Reference ID</Field.Label>
|
|
1038
|
-
<Field.Input
|
|
1039
|
-
type="number"
|
|
1040
|
-
value={templateData.templateReferenceId}
|
|
1041
|
-
onChange={(e) =>
|
|
1042
|
-
setTemplateData({ ...templateData, templateReferenceId: e.target.value })
|
|
1043
|
-
}
|
|
1044
|
-
placeholder="100"
|
|
1045
|
-
/>
|
|
1046
|
-
</Field.Root>
|
|
1047
|
-
</FieldWrapper>
|
|
1048
|
-
)}
|
|
1049
|
-
|
|
1050
|
-
{!isCoreEmail && (
|
|
1051
|
-
<FieldWrapper flex="1">
|
|
1052
|
-
<Field.Root required>
|
|
1053
|
-
<Field.Label>Name</Field.Label>
|
|
1054
|
-
<Field.Input
|
|
1055
|
-
value={templateData.name}
|
|
1056
|
-
onChange={(e) => setTemplateData({ ...templateData, name: e.target.value })}
|
|
1057
|
-
placeholder="Welcome Email"
|
|
1058
|
-
/>
|
|
1059
|
-
</Field.Root>
|
|
1060
|
-
</FieldWrapper>
|
|
1061
|
-
)}
|
|
1062
|
-
|
|
1063
|
-
<FieldWrapper flex="1">
|
|
1064
|
-
<Field.Root required>
|
|
1065
|
-
<Field.Label>Subject</Field.Label>
|
|
1066
|
-
<Field.Input
|
|
1067
|
-
value={templateData.subject}
|
|
1068
|
-
onChange={(e) => setTemplateData({ ...templateData, subject: e.target.value })}
|
|
1069
|
-
placeholder="Welcome {{user.firstName}}!"
|
|
1070
|
-
/>
|
|
1071
|
-
</Field.Root>
|
|
1072
|
-
</FieldWrapper>
|
|
1073
|
-
|
|
1074
|
-
{!isCoreEmail && (
|
|
1075
|
-
<FieldWrapper width="180px">
|
|
1076
|
-
<Field.Root>
|
|
1077
|
-
<Field.Label>Category</Field.Label>
|
|
1078
|
-
<SingleSelect
|
|
1079
|
-
value={templateData.category}
|
|
1080
|
-
onChange={(value) => setTemplateData({ ...templateData, category: value })}
|
|
1081
|
-
>
|
|
1082
|
-
<SingleSelectOption value="transactional">Transactional</SingleSelectOption>
|
|
1083
|
-
<SingleSelectOption value="marketing">Marketing</SingleSelectOption>
|
|
1084
|
-
<SingleSelectOption value="notification">Notification</SingleSelectOption>
|
|
1085
|
-
<SingleSelectOption value="custom">Custom</SingleSelectOption>
|
|
1086
|
-
</SingleSelect>
|
|
1087
|
-
</Field.Root>
|
|
1088
|
-
</FieldWrapper>
|
|
1089
|
-
)}
|
|
1090
|
-
|
|
1091
|
-
{!isCoreEmail && (
|
|
1092
|
-
<ToggleWrapper $isActive={templateData.isActive}>
|
|
1093
|
-
<Toggle
|
|
1094
|
-
checked={templateData.isActive}
|
|
1095
|
-
onChange={() =>
|
|
1096
|
-
setTemplateData({ ...templateData, isActive: !templateData.isActive })
|
|
1097
|
-
}
|
|
1098
|
-
/>
|
|
1099
|
-
<Typography variant="omega">
|
|
1100
|
-
{templateData.isActive ? 'Active' : 'Inactive'}
|
|
1101
|
-
</Typography>
|
|
1102
|
-
</ToggleWrapper>
|
|
1103
|
-
)}
|
|
1104
|
-
</SettingsRow>
|
|
1105
|
-
</Header>
|
|
1106
|
-
|
|
1107
|
-
{/* Editor */}
|
|
1108
|
-
<TabsWrapper>
|
|
1109
|
-
<StyledTabsRoot value={activeTab} onValueChange={setActiveTab}>
|
|
1110
|
-
<TabListWrapper>
|
|
1111
|
-
<Tabs.List>
|
|
1112
|
-
<Tabs.Trigger value="html">✨ Visual Designer</Tabs.Trigger>
|
|
1113
|
-
<Tabs.Trigger value="text">📝 Plain Text</Tabs.Trigger>
|
|
1114
|
-
</Tabs.List>
|
|
1115
|
-
</TabListWrapper>
|
|
1116
|
-
|
|
1117
|
-
<StyledTabsContent value="html">
|
|
1118
|
-
<TabContentWrapper>
|
|
1119
|
-
{!editorLoaded && (
|
|
1120
|
-
<DesignerLoadingContainer>
|
|
1121
|
-
<Loader>Loading Email Designer...</Loader>
|
|
1122
|
-
</DesignerLoadingContainer>
|
|
1123
|
-
)}
|
|
1124
|
-
<EditorCanvas
|
|
1125
|
-
style={{
|
|
1126
|
-
visibility: editorLoaded ? 'visible' : 'hidden',
|
|
1127
|
-
pointerEvents: editorLoaded ? 'auto' : 'none',
|
|
1128
|
-
}}
|
|
1129
|
-
>
|
|
1130
|
-
<EmailEditorComponent
|
|
1131
|
-
ref={emailEditorRef}
|
|
1132
|
-
onReady={onEditorReady}
|
|
1133
|
-
minHeight="calc(100vh - 240px)"
|
|
1134
|
-
options={{
|
|
1135
|
-
// Display mode
|
|
1136
|
-
displayMode: 'email',
|
|
1137
|
-
locale: 'en',
|
|
1138
|
-
projectId: 1, // Required for some features
|
|
1139
|
-
|
|
1140
|
-
// Merge Tags Config
|
|
1141
|
-
mergeTagsConfig: {
|
|
1142
|
-
autocompleteTriggerChar: '@',
|
|
1143
|
-
sort: false,
|
|
1144
|
-
delimiter: ['{{', '}}'],
|
|
1145
|
-
},
|
|
1146
|
-
|
|
1147
|
-
// Appearance
|
|
1148
|
-
appearance: {
|
|
1149
|
-
theme: 'modern_light',
|
|
1150
|
-
panels: {
|
|
1151
|
-
tools: { dock: 'left' }
|
|
1152
|
-
}
|
|
1153
|
-
},
|
|
1154
|
-
|
|
1155
|
-
// Features - Enable responsive preview
|
|
1156
|
-
features: {
|
|
1157
|
-
preview: true,
|
|
1158
|
-
previewInBrowser: true,
|
|
1159
|
-
textEditor: {
|
|
1160
|
-
enabled: true,
|
|
1161
|
-
spellChecker: true,
|
|
1162
|
-
tables: true,
|
|
1163
|
-
cleanPaste: true,
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
|
|
1167
|
-
// Fonts
|
|
1168
|
-
fonts: {
|
|
1169
|
-
showDefaultFonts: true,
|
|
1170
|
-
customFonts: [
|
|
1171
|
-
{
|
|
1172
|
-
label: 'Inter',
|
|
1173
|
-
value: "'Inter', sans-serif",
|
|
1174
|
-
url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
|
|
1175
|
-
}
|
|
1176
|
-
]
|
|
1177
|
-
},
|
|
1178
|
-
|
|
1179
|
-
// Tools configuration - minimal, let Unlayer show all
|
|
1180
|
-
tools: {
|
|
1181
|
-
image: {
|
|
1182
|
-
properties: {
|
|
1183
|
-
src: {
|
|
1184
|
-
value: {
|
|
1185
|
-
url: 'https://picsum.photos/600/350',
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
},
|
|
1189
|
-
},
|
|
1190
|
-
},
|
|
1191
|
-
|
|
1192
|
-
// Merge Tags with extended support
|
|
1193
|
-
mergeTags: {
|
|
1194
|
-
user: {
|
|
1195
|
-
name: 'User',
|
|
1196
|
-
mergeTags: {
|
|
1197
|
-
firstName: {
|
|
1198
|
-
name: 'First Name',
|
|
1199
|
-
value: '{{user.firstName}}',
|
|
1200
|
-
sample: 'John',
|
|
1201
|
-
},
|
|
1202
|
-
lastName: {
|
|
1203
|
-
name: 'Last Name',
|
|
1204
|
-
value: '{{user.lastName}}',
|
|
1205
|
-
sample: 'Doe',
|
|
1206
|
-
},
|
|
1207
|
-
email: {
|
|
1208
|
-
name: 'Email',
|
|
1209
|
-
value: '{{user.email}}',
|
|
1210
|
-
sample: 'john@example.com',
|
|
1211
|
-
},
|
|
1212
|
-
username: {
|
|
1213
|
-
name: 'Username',
|
|
1214
|
-
value: '{{user.username}}',
|
|
1215
|
-
sample: 'johndoe',
|
|
1216
|
-
},
|
|
1217
|
-
},
|
|
1218
|
-
},
|
|
1219
|
-
company: {
|
|
1220
|
-
name: 'Company',
|
|
1221
|
-
mergeTags: {
|
|
1222
|
-
name: {
|
|
1223
|
-
name: 'Name',
|
|
1224
|
-
value: '{{company.name}}',
|
|
1225
|
-
sample: 'ACME Corp',
|
|
1226
|
-
},
|
|
1227
|
-
url: {
|
|
1228
|
-
name: 'Website',
|
|
1229
|
-
value: '{{company.url}}',
|
|
1230
|
-
sample: 'https://acme.com',
|
|
1231
|
-
},
|
|
1232
|
-
address: {
|
|
1233
|
-
name: 'Address',
|
|
1234
|
-
value: '{{company.address}}',
|
|
1235
|
-
sample: '123 Main St, City',
|
|
1236
|
-
},
|
|
1237
|
-
},
|
|
1238
|
-
},
|
|
1239
|
-
order: {
|
|
1240
|
-
name: 'Order',
|
|
1241
|
-
mergeTags: {
|
|
1242
|
-
number: {
|
|
1243
|
-
name: 'Number',
|
|
1244
|
-
value: '{{order.number}}',
|
|
1245
|
-
sample: '#12345',
|
|
1246
|
-
},
|
|
1247
|
-
total: {
|
|
1248
|
-
name: 'Total',
|
|
1249
|
-
value: '{{order.total}}',
|
|
1250
|
-
sample: '$199.99',
|
|
1251
|
-
},
|
|
1252
|
-
date: {
|
|
1253
|
-
name: 'Date',
|
|
1254
|
-
value: '{{order.date}}',
|
|
1255
|
-
sample: '2024-01-15',
|
|
1256
|
-
},
|
|
1257
|
-
status: {
|
|
1258
|
-
name: 'Status',
|
|
1259
|
-
value: '{{order.status}}',
|
|
1260
|
-
sample: 'Shipped',
|
|
1261
|
-
},
|
|
1262
|
-
},
|
|
1263
|
-
},
|
|
1264
|
-
system: {
|
|
1265
|
-
name: 'System',
|
|
1266
|
-
mergeTags: {
|
|
1267
|
-
date: {
|
|
1268
|
-
name: 'Current Date',
|
|
1269
|
-
value: '{{system.date}}',
|
|
1270
|
-
sample: new Date().toLocaleDateString(),
|
|
1271
|
-
},
|
|
1272
|
-
year: {
|
|
1273
|
-
name: 'Current Year',
|
|
1274
|
-
value: '{{system.year}}',
|
|
1275
|
-
sample: new Date().getFullYear().toString(),
|
|
1276
|
-
},
|
|
1277
|
-
unsubscribe: {
|
|
1278
|
-
name: 'Unsubscribe Link',
|
|
1279
|
-
value: '{{system.unsubscribe}}',
|
|
1280
|
-
sample: 'https://example.com/unsubscribe',
|
|
1281
|
-
},
|
|
1282
|
-
},
|
|
1283
|
-
},
|
|
1284
|
-
},
|
|
1285
|
-
|
|
1286
|
-
// Special links
|
|
1287
|
-
specialLinks: {
|
|
1288
|
-
unsubscribe: {
|
|
1289
|
-
enabled: true,
|
|
1290
|
-
text: 'Unsubscribe',
|
|
1291
|
-
href: '{{system.unsubscribe}}'
|
|
1292
|
-
},
|
|
1293
|
-
webview: {
|
|
1294
|
-
enabled: true,
|
|
1295
|
-
text: 'View in browser',
|
|
1296
|
-
href: '{{system.webview}}'
|
|
1297
|
-
}
|
|
1298
|
-
},
|
|
1299
|
-
|
|
1300
|
-
// Custom CSS
|
|
1301
|
-
customCSS: [
|
|
1302
|
-
'.blockbuilder-content-email { font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; }'
|
|
1303
|
-
],
|
|
1304
|
-
|
|
1305
|
-
// Validation
|
|
1306
|
-
validator: {
|
|
1307
|
-
enabled: true,
|
|
1308
|
-
rules: {
|
|
1309
|
-
maxImageSize: 1024 * 1024, // 1MB
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}}
|
|
1313
|
-
/>
|
|
1314
|
-
</EditorCanvas>
|
|
1315
|
-
</TabContentWrapper>
|
|
1316
|
-
</StyledTabsContent>
|
|
1317
|
-
|
|
1318
|
-
<StyledTabsContent value="text">
|
|
1319
|
-
<TextTabContent>
|
|
1320
|
-
<Textarea
|
|
1321
|
-
value={templateData.bodyText}
|
|
1322
|
-
onChange={(e) => setTemplateData({ ...templateData, bodyText: e.target.value })}
|
|
1323
|
-
placeholder="Plain text version of your email... Use Mustache variables: {{user.firstName}} {{company.name}} {{order.total}}"
|
|
1324
|
-
/>
|
|
1325
|
-
</TextTabContent>
|
|
1326
|
-
</StyledTabsContent>
|
|
1327
|
-
</StyledTabsRoot>
|
|
1328
|
-
</TabsWrapper>
|
|
1329
|
-
|
|
1330
|
-
{/* Version History Modal */}
|
|
1331
|
-
<VersionModalOverlay $isOpen={showVersionHistory} onClick={() => setShowVersionHistory(false)} />
|
|
1332
|
-
<VersionModal $isOpen={showVersionHistory}>
|
|
1333
|
-
<VersionModalHeader>
|
|
1334
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
1335
|
-
<ClockIcon style={{ width: 20, height: 20, color: '#32324d' }} />
|
|
1336
|
-
<Typography variant="beta" fontWeight="bold">
|
|
1337
|
-
Version History
|
|
1338
|
-
</Typography>
|
|
1339
|
-
{versions.length > 0 && (
|
|
1340
|
-
<span style={{ fontSize: '12px', color: '#666687', marginLeft: '8px' }}>
|
|
1341
|
-
({versions.length})
|
|
1342
|
-
</span>
|
|
1343
|
-
)}
|
|
1344
|
-
</div>
|
|
1345
|
-
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
1346
|
-
{versions.length > 0 && (
|
|
1347
|
-
<DeleteButton
|
|
1348
|
-
size="S"
|
|
1349
|
-
startIcon={<TrashIcon />}
|
|
1350
|
-
onClick={handleDeleteAllVersions}
|
|
1351
|
-
>
|
|
1352
|
-
Delete All
|
|
1353
|
-
</DeleteButton>
|
|
1354
|
-
)}
|
|
1355
|
-
<CloseButton onClick={() => setShowVersionHistory(false)}>
|
|
1356
|
-
<XMarkIcon />
|
|
1357
|
-
</CloseButton>
|
|
1358
|
-
</div>
|
|
1359
|
-
</VersionModalHeader>
|
|
1360
|
-
|
|
1361
|
-
<VersionModalContent>
|
|
1362
|
-
{loadingVersions ? (
|
|
1363
|
-
<div style={{ textAlign: 'center', padding: '40px' }}>
|
|
1364
|
-
<Loader />
|
|
1365
|
-
</div>
|
|
1366
|
-
) : versions.length === 0 ? (
|
|
1367
|
-
<EmptyVersions>
|
|
1368
|
-
<ClockIcon />
|
|
1369
|
-
<Typography variant="beta">
|
|
1370
|
-
No Versions Yet
|
|
1371
|
-
</Typography>
|
|
1372
|
-
<Typography variant="omega" textColor="neutral600" style={{ maxWidth: '300px' }}>
|
|
1373
|
-
Versions are created automatically when you save changes
|
|
1374
|
-
</Typography>
|
|
1375
|
-
</EmptyVersions>
|
|
1376
|
-
) : (
|
|
1377
|
-
versions.map((version, index) => (
|
|
1378
|
-
<VersionItem key={version.id}>
|
|
1379
|
-
<VersionItemHeader>
|
|
1380
|
-
<VersionNumber>
|
|
1381
|
-
<VersionBadge>#{version.versionNumber || (versions.length - index)}</VersionBadge>
|
|
1382
|
-
{version.name}
|
|
1383
|
-
</VersionNumber>
|
|
1384
|
-
<VersionDate>
|
|
1385
|
-
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
|
1386
|
-
year: 'numeric',
|
|
1387
|
-
month: 'short',
|
|
1388
|
-
day: 'numeric',
|
|
1389
|
-
hour: '2-digit',
|
|
1390
|
-
minute: '2-digit',
|
|
1391
|
-
})}
|
|
1392
|
-
</VersionDate>
|
|
1393
|
-
</VersionItemHeader>
|
|
1394
|
-
|
|
1395
|
-
<VersionMeta>
|
|
1396
|
-
<strong>Subject:</strong> {version.subject || 'No subject'}
|
|
1397
|
-
</VersionMeta>
|
|
1398
|
-
|
|
1399
|
-
<VersionActions>
|
|
1400
|
-
<RestoreButton
|
|
1401
|
-
size="S"
|
|
1402
|
-
startIcon={<ArrowUturnLeftIcon />}
|
|
1403
|
-
onClick={() => handleRestoreVersion(version.id, version.versionNumber || (versions.length - index))}
|
|
1404
|
-
>
|
|
1405
|
-
Restore
|
|
1406
|
-
</RestoreButton>
|
|
1407
|
-
<DeleteButton
|
|
1408
|
-
size="S"
|
|
1409
|
-
startIcon={<TrashIcon />}
|
|
1410
|
-
onClick={() => handleDeleteVersion(version.id, version.versionNumber || (versions.length - index))}
|
|
1411
|
-
>
|
|
1412
|
-
Delete
|
|
1413
|
-
</DeleteButton>
|
|
1414
|
-
</VersionActions>
|
|
1415
|
-
</VersionItem>
|
|
1416
|
-
))
|
|
1417
|
-
)}
|
|
1418
|
-
</VersionModalContent>
|
|
1419
|
-
</VersionModal>
|
|
1420
|
-
</Container>
|
|
1421
|
-
);
|
|
1422
|
-
};
|
|
1423
|
-
|
|
1424
|
-
export default EditorPage;
|