strapi-plugin-magic-mail 1.0.1

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