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.
- package/COPYRIGHT_NOTICE.txt +13 -0
- package/LICENSE +22 -0
- package/README.md +1420 -0
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/AddAccountModal.jsx +1943 -0
- package/admin/src/components/Initializer.jsx +14 -0
- package/admin/src/components/LicenseGuard.jsx +475 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/admin/src/hooks/useAuthRefresh.js +44 -0
- package/admin/src/hooks/useLicense.js +158 -0
- package/admin/src/index.js +86 -0
- package/admin/src/pages/Analytics.jsx +762 -0
- package/admin/src/pages/App.jsx +111 -0
- package/admin/src/pages/EmailDesigner/EditorPage.jsx +1405 -0
- package/admin/src/pages/EmailDesigner/TemplateList.jsx +1807 -0
- package/admin/src/pages/HomePage.jsx +1233 -0
- package/admin/src/pages/LicensePage.jsx +424 -0
- package/admin/src/pages/RoutingRules.jsx +1141 -0
- package/admin/src/pages/Settings.jsx +603 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +71 -0
- package/admin/src/translations/en.json +70 -0
- package/admin/src/translations/es.json +71 -0
- package/admin/src/translations/fr.json +71 -0
- package/admin/src/translations/pt.json +71 -0
- package/admin/src/utils/fetchWithRetry.js +123 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/dist/_chunks/App-B-Gp4Vbr.js +7568 -0
- package/dist/_chunks/App-BymMjoGM.mjs +7543 -0
- package/dist/_chunks/LicensePage-Bl02myMx.mjs +342 -0
- package/dist/_chunks/LicensePage-CJXwPnEe.js +344 -0
- package/dist/_chunks/Settings-C_TmKwcz.mjs +400 -0
- package/dist/_chunks/Settings-zuFQ3pnn.js +402 -0
- package/dist/_chunks/de-CN-G9j1S.js +64 -0
- package/dist/_chunks/de-DS04rP54.mjs +64 -0
- package/dist/_chunks/en-BDc7Jk8u.js +64 -0
- package/dist/_chunks/en-BEFQJXvR.mjs +64 -0
- package/dist/_chunks/es-BpV1MIdm.js +64 -0
- package/dist/_chunks/es-DQHwzPpP.mjs +64 -0
- package/dist/_chunks/fr-BG1WfEVm.mjs +64 -0
- package/dist/_chunks/fr-vpziIpRp.js +64 -0
- package/dist/_chunks/pt-CMoGrOib.mjs +64 -0
- package/dist/_chunks/pt-ODpAhDNa.js +64 -0
- package/dist/admin/index.js +89 -0
- package/dist/admin/index.mjs +90 -0
- package/dist/server/index.js +6214 -0
- package/dist/server/index.mjs +6208 -0
- package/package.json +113 -0
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +153 -0
- package/server/src/config/features.js +260 -0
- package/server/src/config/index.js +6 -0
- package/server/src/content-types/email-account/schema.json +93 -0
- package/server/src/content-types/email-event/index.js +8 -0
- package/server/src/content-types/email-event/schema.json +57 -0
- package/server/src/content-types/email-link/index.js +8 -0
- package/server/src/content-types/email-link/schema.json +49 -0
- package/server/src/content-types/email-log/index.js +8 -0
- package/server/src/content-types/email-log/schema.json +106 -0
- package/server/src/content-types/email-template/schema.json +74 -0
- package/server/src/content-types/email-template-version/schema.json +60 -0
- package/server/src/content-types/index.js +33 -0
- package/server/src/content-types/routing-rule/schema.json +59 -0
- package/server/src/controllers/accounts.js +220 -0
- package/server/src/controllers/analytics.js +347 -0
- package/server/src/controllers/controller.js +26 -0
- package/server/src/controllers/email-designer.js +474 -0
- package/server/src/controllers/index.js +21 -0
- package/server/src/controllers/license.js +267 -0
- package/server/src/controllers/oauth.js +474 -0
- package/server/src/controllers/routing-rules.js +122 -0
- package/server/src/controllers/test.js +383 -0
- package/server/src/destroy.js +23 -0
- package/server/src/index.js +25 -0
- package/server/src/middlewares/index.js +3 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +5 -0
- package/server/src/routes/admin.js +469 -0
- package/server/src/routes/content-api.js +37 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/account-manager.js +277 -0
- package/server/src/services/analytics.js +496 -0
- package/server/src/services/email-designer.js +870 -0
- package/server/src/services/email-router.js +1420 -0
- package/server/src/services/index.js +17 -0
- package/server/src/services/license-guard.js +418 -0
- package/server/src/services/oauth.js +515 -0
- package/server/src/services/service.js +7 -0
- package/server/src/utils/encryption.js +81 -0
- package/strapi-admin.js +4 -0
- package/strapi-server.js +4 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useFetchClient, useNotification } from '@strapi/strapi/admin';
|
|
3
|
+
import { useAuthRefresh } from '../hooks/useAuthRefresh';
|
|
4
|
+
import styled, { keyframes, css } from 'styled-components';
|
|
5
|
+
import {
|
|
6
|
+
Box,
|
|
7
|
+
Button,
|
|
8
|
+
Flex,
|
|
9
|
+
Typography,
|
|
10
|
+
Loader,
|
|
11
|
+
Badge,
|
|
12
|
+
Modal,
|
|
13
|
+
Field,
|
|
14
|
+
TextInput,
|
|
15
|
+
Textarea,
|
|
16
|
+
NumberInput,
|
|
17
|
+
Toggle,
|
|
18
|
+
SingleSelect,
|
|
19
|
+
SingleSelectOption,
|
|
20
|
+
} from '@strapi/design-system';
|
|
21
|
+
import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system';
|
|
22
|
+
import {
|
|
23
|
+
PlusIcon,
|
|
24
|
+
PencilIcon,
|
|
25
|
+
TrashIcon,
|
|
26
|
+
CheckIcon,
|
|
27
|
+
Cog6ToothIcon,
|
|
28
|
+
SparklesIcon,
|
|
29
|
+
FunnelIcon,
|
|
30
|
+
MagnifyingGlassIcon,
|
|
31
|
+
} from '@heroicons/react/24/outline';
|
|
32
|
+
|
|
33
|
+
// ================ THEME (Exact copy from Email Accounts) ================
|
|
34
|
+
const theme = {
|
|
35
|
+
colors: {
|
|
36
|
+
primary: {
|
|
37
|
+
50: '#F0F9FF',
|
|
38
|
+
100: '#E0F2FE',
|
|
39
|
+
500: '#0EA5E9',
|
|
40
|
+
600: '#0284C7',
|
|
41
|
+
700: '#0369A1',
|
|
42
|
+
},
|
|
43
|
+
secondary: {
|
|
44
|
+
50: '#F5F3FF',
|
|
45
|
+
100: '#EDE9FE',
|
|
46
|
+
500: '#A855F7',
|
|
47
|
+
600: '#9333EA',
|
|
48
|
+
},
|
|
49
|
+
success: {
|
|
50
|
+
100: '#DCFCE7',
|
|
51
|
+
500: '#22C55E',
|
|
52
|
+
600: '#16A34A',
|
|
53
|
+
700: '#15803D',
|
|
54
|
+
},
|
|
55
|
+
warning: {
|
|
56
|
+
100: '#FEF3C7',
|
|
57
|
+
500: '#F59E0B',
|
|
58
|
+
600: '#D97706',
|
|
59
|
+
},
|
|
60
|
+
danger: {
|
|
61
|
+
100: '#FEE2E2',
|
|
62
|
+
500: '#EF4444',
|
|
63
|
+
600: '#DC2626',
|
|
64
|
+
},
|
|
65
|
+
neutral: {
|
|
66
|
+
0: '#FFFFFF',
|
|
67
|
+
50: '#F9FAFB',
|
|
68
|
+
100: '#F3F4F6',
|
|
69
|
+
200: '#E5E7EB',
|
|
70
|
+
600: '#4B5563',
|
|
71
|
+
700: '#374151',
|
|
72
|
+
800: '#1F2937',
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
shadows: {
|
|
76
|
+
sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
|
77
|
+
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
78
|
+
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
|
79
|
+
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
|
80
|
+
},
|
|
81
|
+
transitions: {
|
|
82
|
+
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
83
|
+
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
84
|
+
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)',
|
|
85
|
+
},
|
|
86
|
+
spacing: {
|
|
87
|
+
xs: '4px',
|
|
88
|
+
sm: '8px',
|
|
89
|
+
md: '16px',
|
|
90
|
+
lg: '24px',
|
|
91
|
+
xl: '32px',
|
|
92
|
+
'2xl': '48px',
|
|
93
|
+
},
|
|
94
|
+
borderRadius: {
|
|
95
|
+
md: '8px',
|
|
96
|
+
lg: '12px',
|
|
97
|
+
xl: '16px',
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ================ ANIMATIONS ================
|
|
102
|
+
const fadeIn = keyframes`
|
|
103
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
104
|
+
to { opacity: 1; transform: translateY(0); }
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const shimmer = keyframes`
|
|
108
|
+
0% { background-position: -200% 0; }
|
|
109
|
+
100% { background-position: 200% 0; }
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const float = keyframes`
|
|
113
|
+
0%, 100% { transform: translateY(0px); }
|
|
114
|
+
50% { transform: translateY(-5px); }
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
const pulse = keyframes`
|
|
118
|
+
0%, 100% { opacity: 1; }
|
|
119
|
+
50% { opacity: 0.5; }
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const FloatingEmoji = styled.div`
|
|
123
|
+
position: absolute;
|
|
124
|
+
bottom: 40px;
|
|
125
|
+
right: 40px;
|
|
126
|
+
font-size: 72px;
|
|
127
|
+
opacity: 0.08;
|
|
128
|
+
${css`animation: ${float} 4s ease-in-out infinite;`}
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// ================ RESPONSIVE BREAKPOINTS ================
|
|
132
|
+
const breakpoints = {
|
|
133
|
+
mobile: '768px',
|
|
134
|
+
tablet: '1024px',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ================ STYLED COMPONENTS ================
|
|
138
|
+
const Container = styled(Box)`
|
|
139
|
+
${css`animation: ${fadeIn} ${theme.transitions.slow};`}
|
|
140
|
+
min-height: 100vh;
|
|
141
|
+
max-width: 1440px;
|
|
142
|
+
margin: 0 auto;
|
|
143
|
+
padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
|
|
144
|
+
|
|
145
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
146
|
+
padding: 16px 12px 0;
|
|
147
|
+
}
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const Header = styled(Box)`
|
|
151
|
+
background: linear-gradient(135deg,
|
|
152
|
+
${theme.colors.secondary[600]} 0%,
|
|
153
|
+
${theme.colors.primary[600]} 100%
|
|
154
|
+
);
|
|
155
|
+
border-radius: ${theme.borderRadius.xl};
|
|
156
|
+
padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
|
|
157
|
+
margin-bottom: ${theme.spacing.xl};
|
|
158
|
+
position: relative;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
box-shadow: ${theme.shadows.xl};
|
|
161
|
+
|
|
162
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
163
|
+
padding: 24px 20px;
|
|
164
|
+
border-radius: 12px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&::before {
|
|
168
|
+
content: '';
|
|
169
|
+
position: absolute;
|
|
170
|
+
top: 0;
|
|
171
|
+
left: -100%;
|
|
172
|
+
width: 200%;
|
|
173
|
+
height: 100%;
|
|
174
|
+
background: linear-gradient(
|
|
175
|
+
90deg,
|
|
176
|
+
transparent,
|
|
177
|
+
rgba(255, 255, 255, 0.15),
|
|
178
|
+
transparent
|
|
179
|
+
);
|
|
180
|
+
${css`animation: ${shimmer} 3s infinite;`}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
&::after {
|
|
184
|
+
content: '';
|
|
185
|
+
position: absolute;
|
|
186
|
+
top: 0;
|
|
187
|
+
right: 0;
|
|
188
|
+
width: 100%;
|
|
189
|
+
height: 100%;
|
|
190
|
+
background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
|
|
191
|
+
background-size: 15px 15px;
|
|
192
|
+
opacity: 0.3;
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
const HeaderContent = styled(Flex)`
|
|
197
|
+
position: relative;
|
|
198
|
+
z-index: 1;
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const Title = styled(Typography)`
|
|
202
|
+
color: ${theme.colors.neutral[0]};
|
|
203
|
+
font-size: 2rem;
|
|
204
|
+
font-weight: 700;
|
|
205
|
+
letter-spacing: -0.025em;
|
|
206
|
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: ${theme.spacing.sm};
|
|
210
|
+
|
|
211
|
+
svg {
|
|
212
|
+
width: 28px;
|
|
213
|
+
height: 28px;
|
|
214
|
+
${css`animation: ${float} 3s ease-in-out infinite;`}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
218
|
+
font-size: 1.5rem;
|
|
219
|
+
|
|
220
|
+
svg {
|
|
221
|
+
width: 22px;
|
|
222
|
+
height: 22px;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
const Subtitle = styled(Typography)`
|
|
228
|
+
color: rgba(255, 255, 255, 0.95);
|
|
229
|
+
font-size: 0.95rem;
|
|
230
|
+
font-weight: 400;
|
|
231
|
+
margin-top: ${theme.spacing.xs};
|
|
232
|
+
letter-spacing: 0.01em;
|
|
233
|
+
|
|
234
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
235
|
+
font-size: 0.85rem;
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
const StatsGrid = styled.div`
|
|
240
|
+
margin-bottom: ${theme.spacing.xl};
|
|
241
|
+
display: grid;
|
|
242
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
243
|
+
gap: ${theme.spacing.lg};
|
|
244
|
+
justify-content: center;
|
|
245
|
+
max-width: 1200px;
|
|
246
|
+
margin-left: auto;
|
|
247
|
+
margin-right: auto;
|
|
248
|
+
|
|
249
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
250
|
+
grid-template-columns: repeat(2, 1fr);
|
|
251
|
+
gap: 12px;
|
|
252
|
+
margin-bottom: 24px;
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
const StatCard = styled(Box)`
|
|
257
|
+
background: ${theme.colors.neutral[0]};
|
|
258
|
+
border-radius: ${theme.borderRadius.lg};
|
|
259
|
+
padding: 28px ${theme.spacing.lg};
|
|
260
|
+
position: relative;
|
|
261
|
+
overflow: hidden;
|
|
262
|
+
transition: all ${theme.transitions.normal};
|
|
263
|
+
${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
|
|
264
|
+
animation-delay: ${props => props.$delay || '0s'};
|
|
265
|
+
box-shadow: ${theme.shadows.sm};
|
|
266
|
+
border: 1px solid ${theme.colors.neutral[200]};
|
|
267
|
+
min-width: 200px;
|
|
268
|
+
flex: 1;
|
|
269
|
+
text-align: center;
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
align-items: center;
|
|
273
|
+
justify-content: center;
|
|
274
|
+
|
|
275
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
276
|
+
min-width: unset;
|
|
277
|
+
padding: 20px 12px;
|
|
278
|
+
|
|
279
|
+
&:hover {
|
|
280
|
+
transform: none;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
&:hover {
|
|
285
|
+
transform: translateY(-6px);
|
|
286
|
+
box-shadow: ${theme.shadows.xl};
|
|
287
|
+
border-color: ${props => props.$color || theme.colors.primary[500]};
|
|
288
|
+
|
|
289
|
+
.stat-icon {
|
|
290
|
+
transform: scale(1.15) rotate(5deg);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.stat-value {
|
|
294
|
+
transform: scale(1.08);
|
|
295
|
+
color: ${props => props.$color || theme.colors.primary[600]};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
|
|
300
|
+
const StatIcon = styled(Box)`
|
|
301
|
+
width: 68px;
|
|
302
|
+
height: 68px;
|
|
303
|
+
border-radius: ${theme.borderRadius.lg};
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: center;
|
|
307
|
+
background: ${props => props.$bg || theme.colors.primary[100]};
|
|
308
|
+
transition: all ${theme.transitions.normal};
|
|
309
|
+
margin: 0 auto 20px;
|
|
310
|
+
box-shadow: ${theme.shadows.sm};
|
|
311
|
+
|
|
312
|
+
svg {
|
|
313
|
+
width: 34px;
|
|
314
|
+
height: 34px;
|
|
315
|
+
color: ${props => props.$color || theme.colors.primary[600]};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
319
|
+
width: 48px;
|
|
320
|
+
height: 48px;
|
|
321
|
+
margin-bottom: 12px;
|
|
322
|
+
|
|
323
|
+
svg {
|
|
324
|
+
width: 24px;
|
|
325
|
+
height: 24px;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
const StatValue = styled(Typography)`
|
|
331
|
+
font-size: 2.75rem;
|
|
332
|
+
font-weight: 700;
|
|
333
|
+
color: ${theme.colors.neutral[800]};
|
|
334
|
+
line-height: 1;
|
|
335
|
+
margin-bottom: 10px;
|
|
336
|
+
transition: all ${theme.transitions.normal};
|
|
337
|
+
text-align: center;
|
|
338
|
+
|
|
339
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
340
|
+
font-size: 2rem;
|
|
341
|
+
margin-bottom: 6px;
|
|
342
|
+
}
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
const StatLabel = styled(Typography)`
|
|
346
|
+
font-size: 0.95rem;
|
|
347
|
+
color: ${theme.colors.neutral[600]};
|
|
348
|
+
font-weight: 500;
|
|
349
|
+
letter-spacing: 0.025em;
|
|
350
|
+
text-align: center;
|
|
351
|
+
|
|
352
|
+
@media screen and (max-width: ${breakpoints.mobile}) {
|
|
353
|
+
font-size: 0.8rem;
|
|
354
|
+
}
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
const RulesContainer = styled(Box)`
|
|
358
|
+
margin-top: ${theme.spacing.xl};
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const EmptyState = styled(Box)`
|
|
362
|
+
background: ${theme.colors.neutral[0]};
|
|
363
|
+
border-radius: ${theme.borderRadius.xl};
|
|
364
|
+
border: 2px dashed ${theme.colors.neutral[200]};
|
|
365
|
+
padding: 80px 32px;
|
|
366
|
+
text-align: center;
|
|
367
|
+
position: relative;
|
|
368
|
+
overflow: hidden;
|
|
369
|
+
min-height: 400px;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
|
|
374
|
+
/* Background Gradient */
|
|
375
|
+
&::before {
|
|
376
|
+
content: '';
|
|
377
|
+
position: absolute;
|
|
378
|
+
top: 0;
|
|
379
|
+
left: 0;
|
|
380
|
+
right: 0;
|
|
381
|
+
bottom: 0;
|
|
382
|
+
background: linear-gradient(135deg, ${theme.colors.secondary[50]} 0%, ${theme.colors.primary[50]} 100%);
|
|
383
|
+
opacity: 0.3;
|
|
384
|
+
z-index: 0;
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
|
|
388
|
+
const OnlineBadge = styled.div`
|
|
389
|
+
width: 12px;
|
|
390
|
+
height: 12px;
|
|
391
|
+
border-radius: 50%;
|
|
392
|
+
background: ${props => props.$active ? theme.colors.success[500] : theme.colors.neutral[400]};
|
|
393
|
+
display: inline-block;
|
|
394
|
+
margin-right: 8px;
|
|
395
|
+
${css`animation: ${props => props.$active ? pulse : 'none'} 2s ease-in-out infinite;`}
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
const StyledTable = styled(Table)`
|
|
399
|
+
thead {
|
|
400
|
+
background: ${theme.colors.neutral[50]};
|
|
401
|
+
border-bottom: 2px solid ${theme.colors.neutral[200]};
|
|
402
|
+
|
|
403
|
+
th {
|
|
404
|
+
font-weight: 600;
|
|
405
|
+
color: ${theme.colors.neutral[700]};
|
|
406
|
+
font-size: 0.875rem;
|
|
407
|
+
text-transform: uppercase;
|
|
408
|
+
letter-spacing: 0.025em;
|
|
409
|
+
padding: ${theme.spacing.lg} ${theme.spacing.lg};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
tbody tr {
|
|
414
|
+
transition: all ${theme.transitions.fast};
|
|
415
|
+
border-bottom: 1px solid ${theme.colors.neutral[100]};
|
|
416
|
+
|
|
417
|
+
&:last-child {
|
|
418
|
+
border-bottom: none;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
&:hover {
|
|
422
|
+
background: ${theme.colors.neutral[50]};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
td {
|
|
426
|
+
padding: ${theme.spacing.lg} ${theme.spacing.lg};
|
|
427
|
+
color: ${theme.colors.neutral[700]};
|
|
428
|
+
vertical-align: middle;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
|
|
433
|
+
const FilterBar = styled(Flex)`
|
|
434
|
+
background: ${theme.colors.neutral[0]};
|
|
435
|
+
padding: ${theme.spacing.md} ${theme.spacing.lg};
|
|
436
|
+
border-radius: ${theme.borderRadius.lg};
|
|
437
|
+
margin-bottom: ${theme.spacing.lg};
|
|
438
|
+
box-shadow: ${theme.shadows.sm};
|
|
439
|
+
border: 1px solid ${theme.colors.neutral[200]};
|
|
440
|
+
gap: ${theme.spacing.md};
|
|
441
|
+
align-items: center;
|
|
442
|
+
`;
|
|
443
|
+
|
|
444
|
+
const SearchInputWrapper = styled.div`
|
|
445
|
+
position: relative;
|
|
446
|
+
flex: 1;
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
`;
|
|
450
|
+
|
|
451
|
+
const SearchIcon = styled(MagnifyingGlassIcon)`
|
|
452
|
+
position: absolute;
|
|
453
|
+
left: 12px;
|
|
454
|
+
width: 16px;
|
|
455
|
+
height: 16px;
|
|
456
|
+
color: ${theme.colors.neutral[600]};
|
|
457
|
+
pointer-events: none;
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
const StyledSearchInput = styled.input`
|
|
461
|
+
width: 100%;
|
|
462
|
+
padding: 10px 12px 10px 40px;
|
|
463
|
+
border: 1px solid ${theme.colors.neutral[200]};
|
|
464
|
+
border-radius: ${theme.borderRadius.md};
|
|
465
|
+
font-size: 0.875rem;
|
|
466
|
+
transition: all ${theme.transitions.fast};
|
|
467
|
+
|
|
468
|
+
&:focus {
|
|
469
|
+
outline: none;
|
|
470
|
+
border-color: ${theme.colors.primary[500]};
|
|
471
|
+
box-shadow: 0 0 0 2px ${theme.colors.primary[100]};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
&::placeholder {
|
|
475
|
+
color: ${theme.colors.neutral[600]};
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
478
|
+
|
|
479
|
+
const RoutingRulesPage = () => {
|
|
480
|
+
useAuthRefresh(); // Initialize token auto-refresh
|
|
481
|
+
const { get, post, put, del } = useFetchClient();
|
|
482
|
+
const { toggleNotification } = useNotification();
|
|
483
|
+
const [loading, setLoading] = useState(true);
|
|
484
|
+
const [rules, setRules] = useState([]);
|
|
485
|
+
const [accounts, setAccounts] = useState([]);
|
|
486
|
+
const [showModal, setShowModal] = useState(false);
|
|
487
|
+
const [editingRule, setEditingRule] = useState(null);
|
|
488
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
489
|
+
const [filterStatus, setFilterStatus] = useState('all');
|
|
490
|
+
const [filterMatchType, setFilterMatchType] = useState('all');
|
|
491
|
+
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
fetchData();
|
|
494
|
+
}, []);
|
|
495
|
+
|
|
496
|
+
const fetchData = async () => {
|
|
497
|
+
setLoading(true);
|
|
498
|
+
try {
|
|
499
|
+
const [rulesRes, accountsRes] = await Promise.all([
|
|
500
|
+
get('/magic-mail/routing-rules'),
|
|
501
|
+
get('/magic-mail/accounts'),
|
|
502
|
+
]);
|
|
503
|
+
setRules(rulesRes.data.data || []);
|
|
504
|
+
setAccounts(accountsRes.data.data || []);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error('[magic-mail] Error fetching data:', err);
|
|
507
|
+
toggleNotification({
|
|
508
|
+
type: 'danger',
|
|
509
|
+
message: 'Failed to load routing rules',
|
|
510
|
+
});
|
|
511
|
+
} finally {
|
|
512
|
+
setLoading(false);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const deleteRule = async (ruleId, ruleName) => {
|
|
517
|
+
if (!confirm(`Delete routing rule "${ruleName}"?`)) return;
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
await del(`/magic-mail/routing-rules/${ruleId}`);
|
|
521
|
+
toggleNotification({
|
|
522
|
+
type: 'success',
|
|
523
|
+
message: 'Routing rule deleted successfully',
|
|
524
|
+
});
|
|
525
|
+
fetchData();
|
|
526
|
+
} catch (err) {
|
|
527
|
+
toggleNotification({
|
|
528
|
+
type: 'danger',
|
|
529
|
+
message: 'Failed to delete routing rule',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (loading) {
|
|
535
|
+
return (
|
|
536
|
+
<Flex justifyContent="center" alignItems="center" style={{ minHeight: '400px' }}>
|
|
537
|
+
<Loader>Loading Routing Rules...</Loader>
|
|
538
|
+
</Flex>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Calculate stats
|
|
543
|
+
const totalRules = rules.length;
|
|
544
|
+
const activeRules = rules.filter(r => r.isActive).length;
|
|
545
|
+
const highPriorityRules = rules.filter(r => r.priority >= 5).length;
|
|
546
|
+
|
|
547
|
+
// Filter and search logic
|
|
548
|
+
const filteredRules = rules.filter(rule => {
|
|
549
|
+
const matchesSearch =
|
|
550
|
+
rule.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
551
|
+
(rule.description || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
552
|
+
rule.matchValue.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
553
|
+
(rule.accountName || '').toLowerCase().includes(searchQuery.toLowerCase());
|
|
554
|
+
|
|
555
|
+
const matchesStatus =
|
|
556
|
+
filterStatus === 'all' ||
|
|
557
|
+
(filterStatus === 'active' && rule.isActive) ||
|
|
558
|
+
(filterStatus === 'inactive' && !rule.isActive);
|
|
559
|
+
|
|
560
|
+
const matchesType =
|
|
561
|
+
filterMatchType === 'all' ||
|
|
562
|
+
rule.matchType === filterMatchType;
|
|
563
|
+
|
|
564
|
+
return matchesSearch && matchesStatus && matchesType;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const uniqueMatchTypes = [...new Set(rules.map(r => r.matchType))].filter(Boolean);
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<Container>
|
|
571
|
+
{/* Hero Header */}
|
|
572
|
+
<Header>
|
|
573
|
+
<HeaderContent justifyContent="space-between" alignItems="center">
|
|
574
|
+
<Flex direction="column" alignItems="flex-start" gap={2}>
|
|
575
|
+
<Title>
|
|
576
|
+
<FunnelIcon />
|
|
577
|
+
Email Routing Rules
|
|
578
|
+
</Title>
|
|
579
|
+
<Subtitle>
|
|
580
|
+
Define intelligent routing rules to send emails through specific accounts based on conditions
|
|
581
|
+
</Subtitle>
|
|
582
|
+
</Flex>
|
|
583
|
+
</HeaderContent>
|
|
584
|
+
</Header>
|
|
585
|
+
|
|
586
|
+
{/* Quick Stats */}
|
|
587
|
+
<StatsGrid>
|
|
588
|
+
<StatCard $delay="0.1s" $color={theme.colors.secondary[600]}>
|
|
589
|
+
<StatIcon className="stat-icon" $bg={theme.colors.secondary[100]} $color={theme.colors.secondary[600]}>
|
|
590
|
+
<FunnelIcon />
|
|
591
|
+
</StatIcon>
|
|
592
|
+
<StatValue className="stat-value">{totalRules}</StatValue>
|
|
593
|
+
<StatLabel>Total Rules</StatLabel>
|
|
594
|
+
</StatCard>
|
|
595
|
+
|
|
596
|
+
<StatCard $delay="0.2s" $color={theme.colors.success[600]}>
|
|
597
|
+
<StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
|
|
598
|
+
<CheckIcon />
|
|
599
|
+
</StatIcon>
|
|
600
|
+
<StatValue className="stat-value">{activeRules}</StatValue>
|
|
601
|
+
<StatLabel>Active Rules</StatLabel>
|
|
602
|
+
</StatCard>
|
|
603
|
+
|
|
604
|
+
<StatCard $delay="0.3s" $color={theme.colors.warning[600]}>
|
|
605
|
+
<StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
|
|
606
|
+
<SparklesIcon />
|
|
607
|
+
</StatIcon>
|
|
608
|
+
<StatValue className="stat-value">{highPriorityRules}</StatValue>
|
|
609
|
+
<StatLabel>High Priority</StatLabel>
|
|
610
|
+
</StatCard>
|
|
611
|
+
</StatsGrid>
|
|
612
|
+
|
|
613
|
+
{/* Rules List or Empty State */}
|
|
614
|
+
{rules.length === 0 ? (
|
|
615
|
+
<EmptyState>
|
|
616
|
+
{/* Floating Emoji */}
|
|
617
|
+
<FloatingEmoji>
|
|
618
|
+
🎯
|
|
619
|
+
</FloatingEmoji>
|
|
620
|
+
|
|
621
|
+
<Flex direction="column" alignItems="center" gap={6} style={{ position: 'relative', zIndex: 1 }}>
|
|
622
|
+
<Box
|
|
623
|
+
style={{
|
|
624
|
+
width: '120px',
|
|
625
|
+
height: '120px',
|
|
626
|
+
borderRadius: '50%',
|
|
627
|
+
background: `linear-gradient(135deg, ${theme.colors.secondary[100]} 0%, ${theme.colors.primary[100]} 100%)`,
|
|
628
|
+
display: 'flex',
|
|
629
|
+
alignItems: 'center',
|
|
630
|
+
justifyContent: 'center',
|
|
631
|
+
boxShadow: theme.shadows.xl,
|
|
632
|
+
}}
|
|
633
|
+
>
|
|
634
|
+
<FunnelIcon style={{ width: '60px', height: '60px', color: theme.colors.secondary[600] }} />
|
|
635
|
+
</Box>
|
|
636
|
+
|
|
637
|
+
<Typography
|
|
638
|
+
variant="alpha"
|
|
639
|
+
style={{
|
|
640
|
+
fontSize: '1.75rem',
|
|
641
|
+
fontWeight: '700',
|
|
642
|
+
color: theme.colors.neutral[800],
|
|
643
|
+
marginBottom: '8px',
|
|
644
|
+
}}
|
|
645
|
+
>
|
|
646
|
+
No Routing Rules Yet
|
|
647
|
+
</Typography>
|
|
648
|
+
|
|
649
|
+
<Typography
|
|
650
|
+
variant="omega"
|
|
651
|
+
textColor="neutral600"
|
|
652
|
+
style={{
|
|
653
|
+
fontSize: '1rem',
|
|
654
|
+
maxWidth: '500px',
|
|
655
|
+
lineHeight: '1.6',
|
|
656
|
+
}}
|
|
657
|
+
>
|
|
658
|
+
Create your first routing rule to intelligently route emails based on type, recipient, subject, or custom conditions
|
|
659
|
+
</Typography>
|
|
660
|
+
|
|
661
|
+
<Button
|
|
662
|
+
startIcon={<PlusIcon style={{ width: 20, height: 20 }} />}
|
|
663
|
+
onClick={() => setShowModal(true)}
|
|
664
|
+
size="L"
|
|
665
|
+
>
|
|
666
|
+
Create First Rule
|
|
667
|
+
</Button>
|
|
668
|
+
</Flex>
|
|
669
|
+
</EmptyState>
|
|
670
|
+
) : (
|
|
671
|
+
<RulesContainer>
|
|
672
|
+
<Box style={{ marginBottom: theme.spacing.md }}>
|
|
673
|
+
<Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
|
|
674
|
+
<Typography variant="delta" style={{ fontSize: '1.5rem', fontWeight: 600, color: theme.colors.neutral[700] }}>
|
|
675
|
+
🎯 Routing Rules
|
|
676
|
+
</Typography>
|
|
677
|
+
<Button startIcon={<PlusIcon style={{ width: 16, height: 16 }} />} onClick={() => setShowModal(true)}>
|
|
678
|
+
Create Rule
|
|
679
|
+
</Button>
|
|
680
|
+
</Flex>
|
|
681
|
+
</Box>
|
|
682
|
+
|
|
683
|
+
{/* Filter Bar */}
|
|
684
|
+
<FilterBar>
|
|
685
|
+
{/* Search Input */}
|
|
686
|
+
<SearchInputWrapper>
|
|
687
|
+
<SearchIcon />
|
|
688
|
+
<StyledSearchInput
|
|
689
|
+
value={searchQuery}
|
|
690
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
691
|
+
placeholder="Search by name, description, or value..."
|
|
692
|
+
type="text"
|
|
693
|
+
/>
|
|
694
|
+
</SearchInputWrapper>
|
|
695
|
+
|
|
696
|
+
{/* Status Filter */}
|
|
697
|
+
<Box style={{ minWidth: '160px' }}>
|
|
698
|
+
<SingleSelect
|
|
699
|
+
value={filterStatus}
|
|
700
|
+
onChange={setFilterStatus}
|
|
701
|
+
placeholder="Status"
|
|
702
|
+
size="S"
|
|
703
|
+
>
|
|
704
|
+
<SingleSelectOption value="all">All Rules</SingleSelectOption>
|
|
705
|
+
<SingleSelectOption value="active">✅ Active</SingleSelectOption>
|
|
706
|
+
<SingleSelectOption value="inactive">❌ Inactive</SingleSelectOption>
|
|
707
|
+
</SingleSelect>
|
|
708
|
+
</Box>
|
|
709
|
+
|
|
710
|
+
{/* Match Type Filter */}
|
|
711
|
+
<Box style={{ minWidth: '160px' }}>
|
|
712
|
+
<SingleSelect
|
|
713
|
+
value={filterMatchType}
|
|
714
|
+
onChange={setFilterMatchType}
|
|
715
|
+
placeholder="Match Type"
|
|
716
|
+
size="S"
|
|
717
|
+
>
|
|
718
|
+
<SingleSelectOption value="all">All Types</SingleSelectOption>
|
|
719
|
+
{uniqueMatchTypes.map(type => (
|
|
720
|
+
<SingleSelectOption key={type} value={type}>
|
|
721
|
+
{type}
|
|
722
|
+
</SingleSelectOption>
|
|
723
|
+
))}
|
|
724
|
+
</SingleSelect>
|
|
725
|
+
</Box>
|
|
726
|
+
</FilterBar>
|
|
727
|
+
|
|
728
|
+
{/* Rules Table */}
|
|
729
|
+
{filteredRules.length > 0 ? (
|
|
730
|
+
<Box>
|
|
731
|
+
<StyledTable>
|
|
732
|
+
<Thead>
|
|
733
|
+
<Tr>
|
|
734
|
+
<Th>Status</Th>
|
|
735
|
+
<Th>Rule Name</Th>
|
|
736
|
+
<Th>Match Type</Th>
|
|
737
|
+
<Th>Match Value</Th>
|
|
738
|
+
<Th>Target Account</Th>
|
|
739
|
+
<Th>Priority</Th>
|
|
740
|
+
<Th>Actions</Th>
|
|
741
|
+
</Tr>
|
|
742
|
+
</Thead>
|
|
743
|
+
<Tbody>
|
|
744
|
+
{filteredRules.map((rule) => (
|
|
745
|
+
<Tr key={rule.id}>
|
|
746
|
+
{/* Status */}
|
|
747
|
+
<Td>
|
|
748
|
+
<Flex alignItems="center" gap={2}>
|
|
749
|
+
<OnlineBadge $active={rule.isActive} />
|
|
750
|
+
{rule.isActive ? (
|
|
751
|
+
<Badge backgroundColor="success600" textColor="neutral0" size="S">
|
|
752
|
+
Active
|
|
753
|
+
</Badge>
|
|
754
|
+
) : (
|
|
755
|
+
<Badge backgroundColor="neutral600" textColor="neutral0" size="S">
|
|
756
|
+
Inactive
|
|
757
|
+
</Badge>
|
|
758
|
+
)}
|
|
759
|
+
</Flex>
|
|
760
|
+
</Td>
|
|
761
|
+
|
|
762
|
+
{/* Name */}
|
|
763
|
+
<Td>
|
|
764
|
+
<Flex direction="column" alignItems="flex-start" gap={1}>
|
|
765
|
+
<Typography fontWeight="semiBold">
|
|
766
|
+
{rule.name}
|
|
767
|
+
</Typography>
|
|
768
|
+
{rule.description && (
|
|
769
|
+
<Typography variant="pi" textColor="neutral600">
|
|
770
|
+
{rule.description}
|
|
771
|
+
</Typography>
|
|
772
|
+
)}
|
|
773
|
+
</Flex>
|
|
774
|
+
</Td>
|
|
775
|
+
|
|
776
|
+
{/* Match Type */}
|
|
777
|
+
<Td>
|
|
778
|
+
<Badge size="S" variant="secondary">
|
|
779
|
+
{rule.matchType === 'emailType' && '📧 Email Type'}
|
|
780
|
+
{rule.matchType === 'recipient' && '👤 Recipient'}
|
|
781
|
+
{rule.matchType === 'subject' && '📝 Subject'}
|
|
782
|
+
{rule.matchType === 'template' && '🎨 Template'}
|
|
783
|
+
{rule.matchType === 'custom' && '⚙️ Custom'}
|
|
784
|
+
</Badge>
|
|
785
|
+
</Td>
|
|
786
|
+
|
|
787
|
+
{/* Match Value */}
|
|
788
|
+
<Td>
|
|
789
|
+
<Typography variant="pi" style={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
|
790
|
+
{rule.matchValue}
|
|
791
|
+
</Typography>
|
|
792
|
+
</Td>
|
|
793
|
+
|
|
794
|
+
{/* Target Account */}
|
|
795
|
+
<Td>
|
|
796
|
+
<Flex direction="column" alignItems="flex-start" gap={1}>
|
|
797
|
+
<Typography fontWeight="semiBold">
|
|
798
|
+
{rule.accountName}
|
|
799
|
+
</Typography>
|
|
800
|
+
{rule.fallbackAccountName && (
|
|
801
|
+
<Typography variant="pi" textColor="neutral600">
|
|
802
|
+
Fallback: {rule.fallbackAccountName}
|
|
803
|
+
</Typography>
|
|
804
|
+
)}
|
|
805
|
+
</Flex>
|
|
806
|
+
</Td>
|
|
807
|
+
|
|
808
|
+
{/* Priority */}
|
|
809
|
+
<Td>
|
|
810
|
+
<Badge
|
|
811
|
+
size="S"
|
|
812
|
+
variant="secondary"
|
|
813
|
+
backgroundColor={rule.priority >= 5 ? 'warning100' : 'neutral100'}
|
|
814
|
+
textColor={rule.priority >= 5 ? 'warning700' : 'neutral700'}
|
|
815
|
+
>
|
|
816
|
+
{rule.priority >= 5 && '⭐ '}
|
|
817
|
+
{rule.priority}
|
|
818
|
+
</Badge>
|
|
819
|
+
</Td>
|
|
820
|
+
|
|
821
|
+
{/* Actions */}
|
|
822
|
+
<Td>
|
|
823
|
+
<Flex gap={2}>
|
|
824
|
+
<Button
|
|
825
|
+
variant="secondary"
|
|
826
|
+
onClick={() => setEditingRule(rule)}
|
|
827
|
+
size="S"
|
|
828
|
+
aria-label="Edit Rule"
|
|
829
|
+
>
|
|
830
|
+
<PencilIcon style={{ width: 16, height: 16 }} />
|
|
831
|
+
</Button>
|
|
832
|
+
<Button
|
|
833
|
+
variant="danger-light"
|
|
834
|
+
onClick={() => deleteRule(rule.id, rule.name)}
|
|
835
|
+
size="S"
|
|
836
|
+
aria-label="Delete Rule"
|
|
837
|
+
>
|
|
838
|
+
<TrashIcon style={{ width: 16, height: 16 }} />
|
|
839
|
+
</Button>
|
|
840
|
+
</Flex>
|
|
841
|
+
</Td>
|
|
842
|
+
</Tr>
|
|
843
|
+
))}
|
|
844
|
+
</Tbody>
|
|
845
|
+
</StyledTable>
|
|
846
|
+
</Box>
|
|
847
|
+
) : (
|
|
848
|
+
<Box padding={8} style={{ textAlign: 'center' }}>
|
|
849
|
+
<Typography variant="beta" textColor="neutral600">
|
|
850
|
+
No rules found matching your filters
|
|
851
|
+
</Typography>
|
|
852
|
+
</Box>
|
|
853
|
+
)}
|
|
854
|
+
</RulesContainer>
|
|
855
|
+
)}
|
|
856
|
+
|
|
857
|
+
{/* Create/Edit Modal */}
|
|
858
|
+
{(showModal || editingRule) && (
|
|
859
|
+
<RuleModal
|
|
860
|
+
rule={editingRule}
|
|
861
|
+
accounts={accounts}
|
|
862
|
+
onClose={() => {
|
|
863
|
+
setShowModal(false);
|
|
864
|
+
setEditingRule(null);
|
|
865
|
+
}}
|
|
866
|
+
onSave={fetchData}
|
|
867
|
+
/>
|
|
868
|
+
)}
|
|
869
|
+
</Container>
|
|
870
|
+
);
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
// ================ RULE MODAL COMPONENT ================
|
|
874
|
+
const RuleModal = ({ rule, accounts, onClose, onSave }) => {
|
|
875
|
+
const { post, put } = useFetchClient();
|
|
876
|
+
const { toggleNotification } = useNotification();
|
|
877
|
+
const [loading, setLoading] = useState(false);
|
|
878
|
+
const isEditMode = !!rule;
|
|
879
|
+
|
|
880
|
+
const [formData, setFormData] = useState({
|
|
881
|
+
name: rule?.name || '',
|
|
882
|
+
description: rule?.description || '',
|
|
883
|
+
isActive: rule?.isActive !== undefined ? rule.isActive : true,
|
|
884
|
+
priority: rule?.priority || 1,
|
|
885
|
+
matchType: rule?.matchType || 'emailType',
|
|
886
|
+
matchValue: rule?.matchValue || '',
|
|
887
|
+
accountName: rule?.accountName || '',
|
|
888
|
+
fallbackAccountName: rule?.fallbackAccountName || '',
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const handleChange = (field, value) => {
|
|
892
|
+
setFormData(prev => ({ ...prev, [field]: value }));
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const handleSubmit = async () => {
|
|
896
|
+
setLoading(true);
|
|
897
|
+
try {
|
|
898
|
+
if (isEditMode) {
|
|
899
|
+
await put(`/magic-mail/routing-rules/${rule.id}`, formData);
|
|
900
|
+
toggleNotification({
|
|
901
|
+
type: 'success',
|
|
902
|
+
message: 'Routing rule updated successfully',
|
|
903
|
+
});
|
|
904
|
+
} else {
|
|
905
|
+
await post('/magic-mail/routing-rules', formData);
|
|
906
|
+
toggleNotification({
|
|
907
|
+
type: 'success',
|
|
908
|
+
message: 'Routing rule created successfully',
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
onSave();
|
|
912
|
+
onClose();
|
|
913
|
+
} catch (err) {
|
|
914
|
+
toggleNotification({
|
|
915
|
+
type: 'danger',
|
|
916
|
+
message: err.response?.data?.error?.message || `Failed to ${isEditMode ? 'update' : 'create'} routing rule`,
|
|
917
|
+
});
|
|
918
|
+
} finally {
|
|
919
|
+
setLoading(false);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const canSubmit = formData.name && formData.matchType && formData.matchValue && formData.accountName;
|
|
924
|
+
|
|
925
|
+
const getMatchTypeHelp = () => {
|
|
926
|
+
switch (formData.matchType) {
|
|
927
|
+
case 'emailType':
|
|
928
|
+
return 'Match based on email type (e.g., "transactional", "marketing", "notification")';
|
|
929
|
+
case 'recipient':
|
|
930
|
+
return 'Match if recipient email contains this value (e.g., "@vip-customers.com")';
|
|
931
|
+
case 'subject':
|
|
932
|
+
return 'Match if subject line contains this value (e.g., "Invoice", "Password Reset")';
|
|
933
|
+
case 'template':
|
|
934
|
+
return 'Match if email uses this template name (exact match)';
|
|
935
|
+
case 'custom':
|
|
936
|
+
return 'Match based on custom field value passed in emailData.customField';
|
|
937
|
+
default:
|
|
938
|
+
return '';
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
return (
|
|
943
|
+
<Modal.Root open={true} onOpenChange={onClose}>
|
|
944
|
+
<Modal.Content size="L">
|
|
945
|
+
<Modal.Header>
|
|
946
|
+
<Typography variant="beta">
|
|
947
|
+
<Cog6ToothIcon style={{ marginRight: 8, width: 24, height: 24 }} />
|
|
948
|
+
{isEditMode ? 'Edit Routing Rule' : 'Create Routing Rule'}
|
|
949
|
+
</Typography>
|
|
950
|
+
</Modal.Header>
|
|
951
|
+
|
|
952
|
+
<Modal.Body>
|
|
953
|
+
<Box style={{ width: '100%' }}>
|
|
954
|
+
<Flex direction="column" gap={6} style={{ width: '100%' }}>
|
|
955
|
+
|
|
956
|
+
{/* Rule Name */}
|
|
957
|
+
<Field.Root required style={{ width: '100%' }}>
|
|
958
|
+
<Field.Label>Rule Name</Field.Label>
|
|
959
|
+
<TextInput
|
|
960
|
+
placeholder="Marketing emails via SendGrid"
|
|
961
|
+
value={formData.name}
|
|
962
|
+
onChange={(e) => handleChange('name', e.target.value)}
|
|
963
|
+
style={{ width: '100%' }}
|
|
964
|
+
/>
|
|
965
|
+
<Field.Hint>
|
|
966
|
+
A descriptive name for this routing rule
|
|
967
|
+
</Field.Hint>
|
|
968
|
+
</Field.Root>
|
|
969
|
+
|
|
970
|
+
{/* Description */}
|
|
971
|
+
<Field.Root style={{ width: '100%' }}>
|
|
972
|
+
<Field.Label>Description (Optional)</Field.Label>
|
|
973
|
+
<Textarea
|
|
974
|
+
placeholder="Route all marketing emails through SendGrid for better deliverability..."
|
|
975
|
+
value={formData.description}
|
|
976
|
+
onChange={(e) => handleChange('description', e.target.value)}
|
|
977
|
+
rows={3}
|
|
978
|
+
style={{ width: '100%' }}
|
|
979
|
+
/>
|
|
980
|
+
</Field.Root>
|
|
981
|
+
|
|
982
|
+
{/* Match Type */}
|
|
983
|
+
<Field.Root required style={{ width: '100%' }}>
|
|
984
|
+
<Field.Label>Match Type</Field.Label>
|
|
985
|
+
<SingleSelect
|
|
986
|
+
value={formData.matchType}
|
|
987
|
+
onChange={(value) => handleChange('matchType', value)}
|
|
988
|
+
style={{ width: '100%' }}
|
|
989
|
+
>
|
|
990
|
+
<SingleSelectOption value="emailType">📧 Email Type</SingleSelectOption>
|
|
991
|
+
<SingleSelectOption value="recipient">👤 Recipient</SingleSelectOption>
|
|
992
|
+
<SingleSelectOption value="subject">📝 Subject</SingleSelectOption>
|
|
993
|
+
<SingleSelectOption value="template">🎨 Template</SingleSelectOption>
|
|
994
|
+
<SingleSelectOption value="custom">⚙️ Custom Field</SingleSelectOption>
|
|
995
|
+
</SingleSelect>
|
|
996
|
+
<Field.Hint>
|
|
997
|
+
{getMatchTypeHelp()}
|
|
998
|
+
</Field.Hint>
|
|
999
|
+
</Field.Root>
|
|
1000
|
+
|
|
1001
|
+
{/* Match Value */}
|
|
1002
|
+
<Field.Root required style={{ width: '100%' }}>
|
|
1003
|
+
<Field.Label>Match Value</Field.Label>
|
|
1004
|
+
<TextInput
|
|
1005
|
+
placeholder={
|
|
1006
|
+
formData.matchType === 'emailType' ? 'marketing' :
|
|
1007
|
+
formData.matchType === 'recipient' ? '@vip-customers.com' :
|
|
1008
|
+
formData.matchType === 'subject' ? 'Invoice' :
|
|
1009
|
+
formData.matchType === 'template' ? 'welcome-email' :
|
|
1010
|
+
'custom-value'
|
|
1011
|
+
}
|
|
1012
|
+
value={formData.matchValue}
|
|
1013
|
+
onChange={(e) => handleChange('matchValue', e.target.value)}
|
|
1014
|
+
style={{ width: '100%' }}
|
|
1015
|
+
/>
|
|
1016
|
+
<Field.Hint>
|
|
1017
|
+
The value to match against. Case-insensitive for recipient and subject.
|
|
1018
|
+
</Field.Hint>
|
|
1019
|
+
</Field.Root>
|
|
1020
|
+
|
|
1021
|
+
{/* Target Account */}
|
|
1022
|
+
<Field.Root required style={{ width: '100%' }}>
|
|
1023
|
+
<Field.Label>Target Account</Field.Label>
|
|
1024
|
+
<SingleSelect
|
|
1025
|
+
value={formData.accountName}
|
|
1026
|
+
onChange={(value) => handleChange('accountName', value)}
|
|
1027
|
+
style={{ width: '100%' }}
|
|
1028
|
+
>
|
|
1029
|
+
<SingleSelectOption value="">Select account...</SingleSelectOption>
|
|
1030
|
+
{accounts.filter(a => a.isActive).map(account => (
|
|
1031
|
+
<SingleSelectOption key={account.name} value={account.name}>
|
|
1032
|
+
{account.name} ({account.provider})
|
|
1033
|
+
</SingleSelectOption>
|
|
1034
|
+
))}
|
|
1035
|
+
</SingleSelect>
|
|
1036
|
+
<Field.Hint>
|
|
1037
|
+
The email account to use when this rule matches
|
|
1038
|
+
</Field.Hint>
|
|
1039
|
+
</Field.Root>
|
|
1040
|
+
|
|
1041
|
+
{/* Fallback Account */}
|
|
1042
|
+
<Field.Root style={{ width: '100%' }}>
|
|
1043
|
+
<Field.Label>Fallback Account (Optional)</Field.Label>
|
|
1044
|
+
<SingleSelect
|
|
1045
|
+
value={formData.fallbackAccountName}
|
|
1046
|
+
onChange={(value) => handleChange('fallbackAccountName', value)}
|
|
1047
|
+
style={{ width: '100%' }}
|
|
1048
|
+
>
|
|
1049
|
+
<SingleSelectOption value="">No fallback</SingleSelectOption>
|
|
1050
|
+
{accounts.filter(a => a.isActive && a.name !== formData.accountName).map(account => (
|
|
1051
|
+
<SingleSelectOption key={account.name} value={account.name}>
|
|
1052
|
+
{account.name} ({account.provider})
|
|
1053
|
+
</SingleSelectOption>
|
|
1054
|
+
))}
|
|
1055
|
+
</SingleSelect>
|
|
1056
|
+
<Field.Hint>
|
|
1057
|
+
Use this account if the target account is unavailable or rate-limited
|
|
1058
|
+
</Field.Hint>
|
|
1059
|
+
</Field.Root>
|
|
1060
|
+
|
|
1061
|
+
{/* Priority */}
|
|
1062
|
+
<Field.Root style={{ width: '100%' }}>
|
|
1063
|
+
<Field.Label>Rule Priority</Field.Label>
|
|
1064
|
+
<NumberInput
|
|
1065
|
+
value={formData.priority}
|
|
1066
|
+
onValueChange={(value) => handleChange('priority', value)}
|
|
1067
|
+
min={1}
|
|
1068
|
+
max={10}
|
|
1069
|
+
style={{ width: '100%' }}
|
|
1070
|
+
/>
|
|
1071
|
+
<Field.Hint>
|
|
1072
|
+
Higher priority rules are evaluated first (1-10). Use high priority for more specific rules.
|
|
1073
|
+
</Field.Hint>
|
|
1074
|
+
</Field.Root>
|
|
1075
|
+
|
|
1076
|
+
{/* Active Toggle */}
|
|
1077
|
+
<Box
|
|
1078
|
+
padding={4}
|
|
1079
|
+
background={formData.isActive ? theme.colors.success[100] : theme.colors.danger[100]}
|
|
1080
|
+
hasRadius
|
|
1081
|
+
style={{
|
|
1082
|
+
width: '100%',
|
|
1083
|
+
border: formData.isActive ? `2px solid ${theme.colors.success[600]}` : `2px solid ${theme.colors.danger[600]}`,
|
|
1084
|
+
borderRadius: theme.borderRadius.md,
|
|
1085
|
+
transition: 'all 0.2s ease'
|
|
1086
|
+
}}
|
|
1087
|
+
>
|
|
1088
|
+
<Flex gap={3} alignItems="center">
|
|
1089
|
+
<Toggle
|
|
1090
|
+
checked={formData.isActive}
|
|
1091
|
+
onChange={() => handleChange('isActive', !formData.isActive)}
|
|
1092
|
+
/>
|
|
1093
|
+
<Box style={{ flex: 1 }}>
|
|
1094
|
+
<Flex alignItems="center" gap={2}>
|
|
1095
|
+
<Typography fontWeight="semiBold">
|
|
1096
|
+
{formData.isActive ? '✅ Rule Active' : '❌ Rule Inactive'}
|
|
1097
|
+
</Typography>
|
|
1098
|
+
<Badge
|
|
1099
|
+
backgroundColor={formData.isActive ? 'success600' : 'danger600'}
|
|
1100
|
+
textColor="neutral0"
|
|
1101
|
+
size="S"
|
|
1102
|
+
>
|
|
1103
|
+
{formData.isActive ? 'ENABLED' : 'DISABLED'}
|
|
1104
|
+
</Badge>
|
|
1105
|
+
</Flex>
|
|
1106
|
+
<Typography variant="pi" textColor="neutral600" marginTop={1}>
|
|
1107
|
+
{formData.isActive
|
|
1108
|
+
? 'This rule is active and will be used for email routing'
|
|
1109
|
+
: 'This rule is disabled and will be ignored'
|
|
1110
|
+
}
|
|
1111
|
+
</Typography>
|
|
1112
|
+
</Box>
|
|
1113
|
+
</Flex>
|
|
1114
|
+
</Box>
|
|
1115
|
+
|
|
1116
|
+
</Flex>
|
|
1117
|
+
</Box>
|
|
1118
|
+
</Modal.Body>
|
|
1119
|
+
|
|
1120
|
+
<Modal.Footer>
|
|
1121
|
+
<Flex justifyContent="flex-end" gap={2} style={{ width: '100%' }}>
|
|
1122
|
+
<Button onClick={onClose} variant="tertiary">
|
|
1123
|
+
Cancel
|
|
1124
|
+
</Button>
|
|
1125
|
+
<Button
|
|
1126
|
+
onClick={handleSubmit}
|
|
1127
|
+
loading={loading}
|
|
1128
|
+
disabled={!canSubmit}
|
|
1129
|
+
startIcon={<CheckIcon style={{ width: 16, height: 16 }} />}
|
|
1130
|
+
>
|
|
1131
|
+
{isEditMode ? 'Update Rule' : 'Create Rule'}
|
|
1132
|
+
</Button>
|
|
1133
|
+
</Flex>
|
|
1134
|
+
</Modal.Footer>
|
|
1135
|
+
</Modal.Content>
|
|
1136
|
+
</Modal.Root>
|
|
1137
|
+
);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
export default RoutingRulesPage;
|
|
1141
|
+
|