strapi-plugin-magic-mail 2.2.4 → 2.2.5

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 (70) hide show
  1. package/dist/server/index.js +1 -1
  2. package/dist/server/index.mjs +1 -1
  3. package/package.json +1 -3
  4. package/admin/jsconfig.json +0 -10
  5. package/admin/src/components/AddAccountModal.jsx +0 -1943
  6. package/admin/src/components/Initializer.jsx +0 -14
  7. package/admin/src/components/LicenseGuard.jsx +0 -475
  8. package/admin/src/components/PluginIcon.jsx +0 -5
  9. package/admin/src/hooks/useAuthRefresh.js +0 -44
  10. package/admin/src/hooks/useLicense.js +0 -158
  11. package/admin/src/index.js +0 -87
  12. package/admin/src/pages/Analytics.jsx +0 -762
  13. package/admin/src/pages/App.jsx +0 -111
  14. package/admin/src/pages/EmailDesigner/EditorPage.jsx +0 -1424
  15. package/admin/src/pages/EmailDesigner/TemplateList.jsx +0 -1807
  16. package/admin/src/pages/HomePage.jsx +0 -1170
  17. package/admin/src/pages/LicensePage.jsx +0 -430
  18. package/admin/src/pages/RoutingRules.jsx +0 -1141
  19. package/admin/src/pages/Settings.jsx +0 -603
  20. package/admin/src/pluginId.js +0 -3
  21. package/admin/src/translations/de.json +0 -71
  22. package/admin/src/translations/en.json +0 -70
  23. package/admin/src/translations/es.json +0 -71
  24. package/admin/src/translations/fr.json +0 -71
  25. package/admin/src/translations/pt.json +0 -71
  26. package/admin/src/utils/fetchWithRetry.js +0 -123
  27. package/admin/src/utils/getTranslation.js +0 -5
  28. package/admin/src/utils/theme.js +0 -85
  29. package/server/jsconfig.json +0 -10
  30. package/server/src/bootstrap.js +0 -157
  31. package/server/src/config/features.js +0 -260
  32. package/server/src/config/index.js +0 -9
  33. package/server/src/content-types/email-account/schema.json +0 -93
  34. package/server/src/content-types/email-event/index.js +0 -8
  35. package/server/src/content-types/email-event/schema.json +0 -57
  36. package/server/src/content-types/email-link/index.js +0 -8
  37. package/server/src/content-types/email-link/schema.json +0 -49
  38. package/server/src/content-types/email-log/index.js +0 -8
  39. package/server/src/content-types/email-log/schema.json +0 -106
  40. package/server/src/content-types/email-template/schema.json +0 -74
  41. package/server/src/content-types/email-template-version/schema.json +0 -60
  42. package/server/src/content-types/index.js +0 -33
  43. package/server/src/content-types/routing-rule/schema.json +0 -59
  44. package/server/src/controllers/accounts.js +0 -229
  45. package/server/src/controllers/analytics.js +0 -361
  46. package/server/src/controllers/controller.js +0 -26
  47. package/server/src/controllers/email-designer.js +0 -474
  48. package/server/src/controllers/index.js +0 -21
  49. package/server/src/controllers/license.js +0 -269
  50. package/server/src/controllers/oauth.js +0 -474
  51. package/server/src/controllers/routing-rules.js +0 -129
  52. package/server/src/controllers/test.js +0 -301
  53. package/server/src/destroy.js +0 -27
  54. package/server/src/index.js +0 -25
  55. package/server/src/middlewares/index.js +0 -3
  56. package/server/src/policies/index.js +0 -3
  57. package/server/src/register.js +0 -5
  58. package/server/src/routes/admin.js +0 -469
  59. package/server/src/routes/content-api.js +0 -37
  60. package/server/src/routes/index.js +0 -9
  61. package/server/src/services/account-manager.js +0 -329
  62. package/server/src/services/analytics.js +0 -512
  63. package/server/src/services/email-designer.js +0 -717
  64. package/server/src/services/email-router.js +0 -1446
  65. package/server/src/services/index.js +0 -17
  66. package/server/src/services/license-guard.js +0 -423
  67. package/server/src/services/oauth.js +0 -515
  68. package/server/src/services/service.js +0 -7
  69. package/server/src/utils/encryption.js +0 -81
  70. 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...&#10;&#10;Use Mustache variables:&#10;{{user.firstName}}&#10;{{company.name}}&#10;{{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;