@flusys/ng-email 1.0.0-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +872 -0
- package/fesm2022/flusys-ng-email-email-config-form.component-BK195yNS.mjs +713 -0
- package/fesm2022/flusys-ng-email-email-config-form.component-BK195yNS.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-email-config-list.component-DnIJdHdf.mjs +541 -0
- package/fesm2022/flusys-ng-email-email-config-list.component-DnIJdHdf.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-template-form.component-u40UX6yY.mjs +555 -0
- package/fesm2022/flusys-ng-email-template-form.component-u40UX6yY.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-template-list.component-91djzx28.mjs +636 -0
- package/fesm2022/flusys-ng-email-template-list.component-91djzx28.mjs.map +1 -0
- package/fesm2022/flusys-ng-email.mjs +579 -0
- package/fesm2022/flusys-ng-email.mjs.map +1 -0
- package/package.json +33 -0
- package/types/flusys-ng-email.d.ts +455 -0
package/README.md
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
# Email Package Guide
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flusys/ng-email`
|
|
4
|
+
> **Type:** Email management with configurations, templates, and sending
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`@flusys/ng-email` provides complete email management:
|
|
11
|
+
|
|
12
|
+
- **Email Configurations** - CRUD for SMTP, SendGrid, Mailgun providers
|
|
13
|
+
- **Email Templates** - Visual template management with variable interpolation
|
|
14
|
+
- **HTML/Plain Text** - Toggle between content types with live preview
|
|
15
|
+
- **Test Sending** - Test configurations and templates with dynamic variables
|
|
16
|
+
- **Company Scoping** - Automatic company-scoped queries (when enabled)
|
|
17
|
+
- **Lazy Loading** - All page components are lazy-loaded via routes
|
|
18
|
+
- **Responsive Design** - Mobile-first with horizontal scroll tables
|
|
19
|
+
- **Dark Mode** - Full dark/light mode support
|
|
20
|
+
|
|
21
|
+
### Package Hierarchy
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
@flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME)
|
|
25
|
+
|
|
|
26
|
+
@flusys/ng-shared <- Shared utilities (ApiResourceService, IBaseEntity)
|
|
27
|
+
|
|
|
28
|
+
@flusys/ng-layout <- Layout (LAYOUT_AUTH_STATE for company context)
|
|
29
|
+
|
|
|
30
|
+
@flusys/ng-email <- Email management (THIS PACKAGE)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Company Context
|
|
34
|
+
|
|
35
|
+
Email components use `LAYOUT_AUTH_STATE` from `@flusys/ng-layout` for company context. This follows the Provider Interface Pattern - ng-email depends on ng-layout's abstraction, not ng-auth directly.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
|
|
39
|
+
import { APP_CONFIG, DEFAULT_APP_NAME } from '@flusys/ng-core';
|
|
40
|
+
|
|
41
|
+
// Optional injection - works with or without company feature
|
|
42
|
+
private readonly companyContext = inject(LAYOUT_AUTH_STATE, { optional: true });
|
|
43
|
+
|
|
44
|
+
readonly showCompanyInfo = computed(
|
|
45
|
+
() => this.appConfig.enableCompanyFeature && !!this.companyContext,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
readonly currentCompanyName = computed(
|
|
49
|
+
() => this.companyContext?.currentCompanyInfo()?.name ?? DEFAULT_APP_NAME,
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Package Architecture
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
ng-email/
|
|
59
|
+
├── enums/
|
|
60
|
+
│ ├── email-provider.enum.ts # EmailProviderEnum (smtp, sendgrid, mailgun)
|
|
61
|
+
│ ├── email-block-type.enum.ts # EmailBlockType (text, image, button, etc.)
|
|
62
|
+
│ └── public-api.ts
|
|
63
|
+
├── interfaces/
|
|
64
|
+
│ ├── email-config.interface.ts # IEmailConfig, ICreateEmailConfigDto
|
|
65
|
+
│ ├── email-template.interface.ts # IEmailTemplate, ICreateEmailTemplateDto
|
|
66
|
+
│ ├── email-schema.interface.ts # IEmailSchema, IEmailSection, IEmailBlock
|
|
67
|
+
│ ├── email-send.interface.ts # ISendEmailDto, ISendTemplateEmailDto
|
|
68
|
+
│ └── public-api.ts
|
|
69
|
+
├── services/
|
|
70
|
+
│ ├── email-config-api.service.ts # Config CRUD + test sending
|
|
71
|
+
│ ├── email-template-api.service.ts # Template CRUD + getBySlug
|
|
72
|
+
│ ├── email-send.service.ts # Email sending operations
|
|
73
|
+
│ ├── email-builder-state.service.ts # Template builder state management
|
|
74
|
+
│ └── public-api.ts
|
|
75
|
+
├── pages/
|
|
76
|
+
│ ├── email-container/
|
|
77
|
+
│ │ └── email-container.component.ts # Main container with tabs
|
|
78
|
+
│ ├── config/
|
|
79
|
+
│ │ ├── email-config-list.component.ts
|
|
80
|
+
│ │ └── email-config-form.component.ts
|
|
81
|
+
│ └── template/
|
|
82
|
+
│ ├── template-list.component.ts
|
|
83
|
+
│ └── template-form.component.ts
|
|
84
|
+
├── routes/
|
|
85
|
+
│ └── email.routes.ts
|
|
86
|
+
└── public-api.ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Route Integration
|
|
92
|
+
|
|
93
|
+
### Adding Email Routes
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// app.routes.ts
|
|
97
|
+
import { EMAIL_ROUTES } from '@flusys/ng-email';
|
|
98
|
+
|
|
99
|
+
export const routes: Routes = [
|
|
100
|
+
{
|
|
101
|
+
path: 'email',
|
|
102
|
+
children: EMAIL_ROUTES,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Route Structure
|
|
108
|
+
|
|
109
|
+
| Path | Component | Description |
|
|
110
|
+
|------|-----------|-------------|
|
|
111
|
+
| `/email` | EmailContainerComponent | Main container with tab navigation |
|
|
112
|
+
| `/email/templates` | TemplateListComponent | List email templates |
|
|
113
|
+
| `/email/templates/new` | TemplateFormComponent | Create new template |
|
|
114
|
+
| `/email/templates/:id` | TemplateFormComponent | Edit template |
|
|
115
|
+
| `/email/configs` | EmailConfigListComponent | List email configurations |
|
|
116
|
+
| `/email/configs/new` | EmailConfigFormComponent | Create new config |
|
|
117
|
+
| `/email/configs/:id` | EmailConfigFormComponent | Edit config |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Enums
|
|
122
|
+
|
|
123
|
+
### EmailProviderEnum
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export enum EmailProviderEnum {
|
|
127
|
+
SMTP = 'smtp',
|
|
128
|
+
SENDGRID = 'sendgrid',
|
|
129
|
+
MAILGUN = 'mailgun',
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### EmailBlockType
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
export enum EmailBlockType {
|
|
137
|
+
TEXT = 'text',
|
|
138
|
+
IMAGE = 'image',
|
|
139
|
+
BUTTON = 'button',
|
|
140
|
+
DIVIDER = 'divider',
|
|
141
|
+
HTML = 'html',
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Interfaces
|
|
148
|
+
|
|
149
|
+
### Email Configuration
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
interface IEmailConfig extends IBaseEntity {
|
|
153
|
+
name: string;
|
|
154
|
+
provider: EmailProviderEnum;
|
|
155
|
+
config: Record<string, any>; // Provider-specific config
|
|
156
|
+
fromEmail: string | null;
|
|
157
|
+
fromName: string | null;
|
|
158
|
+
isActive: boolean;
|
|
159
|
+
isDefault: boolean; // Set as default configuration
|
|
160
|
+
companyId?: string | null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ICreateEmailConfigDto {
|
|
164
|
+
name: string;
|
|
165
|
+
provider: EmailProviderEnum;
|
|
166
|
+
config: Record<string, any>;
|
|
167
|
+
fromEmail?: string;
|
|
168
|
+
fromName?: string;
|
|
169
|
+
isActive?: boolean;
|
|
170
|
+
isDefault?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface IUpdateEmailConfigDto extends Partial<ICreateEmailConfigDto> {
|
|
174
|
+
id: string;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Provider-Specific Configs
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// SMTP
|
|
182
|
+
interface ISmtpConfig {
|
|
183
|
+
host: string;
|
|
184
|
+
port: number;
|
|
185
|
+
secure?: boolean;
|
|
186
|
+
username: string;
|
|
187
|
+
password: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// SendGrid
|
|
191
|
+
interface ISendGridConfig {
|
|
192
|
+
apiKey: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Mailgun
|
|
196
|
+
interface IMailgunConfig {
|
|
197
|
+
apiKey: string;
|
|
198
|
+
domain: string;
|
|
199
|
+
region?: 'us' | 'eu';
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Email Template
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
interface IEmailTemplate extends IBaseEntity {
|
|
207
|
+
name: string;
|
|
208
|
+
slug: string;
|
|
209
|
+
description: string | null;
|
|
210
|
+
subject: string;
|
|
211
|
+
schema: IEmailSchema;
|
|
212
|
+
htmlContent: string;
|
|
213
|
+
textContent: string | null;
|
|
214
|
+
schemaVersion: number;
|
|
215
|
+
isActive: boolean;
|
|
216
|
+
isHtml: boolean; // true=HTML mode, false=plain text
|
|
217
|
+
metadata: Record<string, unknown> | null;
|
|
218
|
+
companyId?: string | null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface ICreateEmailTemplateDto {
|
|
222
|
+
name: string;
|
|
223
|
+
slug: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
subject: string;
|
|
226
|
+
schema: IEmailSchema;
|
|
227
|
+
htmlContent: string;
|
|
228
|
+
textContent?: string;
|
|
229
|
+
isActive?: boolean;
|
|
230
|
+
isHtml?: boolean;
|
|
231
|
+
metadata?: Record<string, unknown>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface IUpdateEmailTemplateDto extends Partial<ICreateEmailTemplateDto> {
|
|
235
|
+
id: string;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Email Schema
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
interface IEmailSchema {
|
|
243
|
+
id: string;
|
|
244
|
+
version: string;
|
|
245
|
+
name: string;
|
|
246
|
+
subject: string;
|
|
247
|
+
preheader?: string;
|
|
248
|
+
sections: IEmailSection[];
|
|
249
|
+
variables?: IEmailTemplateVariable[];
|
|
250
|
+
settings?: IEmailSettings;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface IEmailSection {
|
|
254
|
+
id: string;
|
|
255
|
+
type: 'header' | 'body' | 'footer';
|
|
256
|
+
name: string;
|
|
257
|
+
blocks: IEmailBlock[];
|
|
258
|
+
order: number;
|
|
259
|
+
style?: IEmailSectionStyle;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface IEmailBlock {
|
|
263
|
+
id: string;
|
|
264
|
+
type: EmailBlockType;
|
|
265
|
+
order: number;
|
|
266
|
+
content: ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent;
|
|
267
|
+
style?: IEmailBlockStyle;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface IEmailTemplateVariable {
|
|
271
|
+
name: string;
|
|
272
|
+
type: 'string' | 'number' | 'boolean' | 'date';
|
|
273
|
+
required?: boolean;
|
|
274
|
+
defaultValue?: string;
|
|
275
|
+
description?: string;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Email Sending
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Test send result
|
|
283
|
+
interface ITestSendResult {
|
|
284
|
+
success: boolean;
|
|
285
|
+
messageId?: string;
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Direct email
|
|
290
|
+
interface ISendEmailDto {
|
|
291
|
+
to: string | string[];
|
|
292
|
+
cc?: string | string[];
|
|
293
|
+
bcc?: string | string[];
|
|
294
|
+
subject: string;
|
|
295
|
+
html: string;
|
|
296
|
+
text?: string;
|
|
297
|
+
from?: string;
|
|
298
|
+
fromName?: string;
|
|
299
|
+
replyTo?: string;
|
|
300
|
+
emailConfigId?: string;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Template email
|
|
304
|
+
interface ISendTemplateEmailDto {
|
|
305
|
+
templateId?: string;
|
|
306
|
+
templateSlug?: string;
|
|
307
|
+
to: string | string[];
|
|
308
|
+
cc?: string | string[];
|
|
309
|
+
bcc?: string | string[];
|
|
310
|
+
variables?: Record<string, any>;
|
|
311
|
+
from?: string;
|
|
312
|
+
fromName?: string;
|
|
313
|
+
replyTo?: string;
|
|
314
|
+
emailConfigId?: string;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Services
|
|
321
|
+
|
|
322
|
+
All services are `providedIn: 'root'`. Company scoping is automatic when the company feature is enabled.
|
|
323
|
+
|
|
324
|
+
### EmailConfigApiService
|
|
325
|
+
|
|
326
|
+
Email configuration CRUD. Extends `ApiResourceService<IUpdateEmailConfigDto, IEmailConfig>`.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
@Injectable({ providedIn: 'root' })
|
|
330
|
+
export class EmailConfigApiService extends ApiResourceService<
|
|
331
|
+
IUpdateEmailConfigDto,
|
|
332
|
+
IEmailConfig
|
|
333
|
+
> {
|
|
334
|
+
constructor() {
|
|
335
|
+
const http = inject(HttpClient);
|
|
336
|
+
super('email/email-config', http);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Send test email to verify configuration */
|
|
340
|
+
sendTest(configId: string, recipient: string): Observable<ISingleResponse<ITestSendResult>> {
|
|
341
|
+
return this.http.post<ISingleResponse<ITestSendResult>>(
|
|
342
|
+
`${this.baseUrl.replace('/email-config', '')}/send/test`,
|
|
343
|
+
{ emailConfigId: configId, recipient },
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### EmailTemplateApiService
|
|
350
|
+
|
|
351
|
+
Email template CRUD. Extends `ApiResourceService<IUpdateEmailTemplateDto, IEmailTemplate>`.
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
@Injectable({ providedIn: 'root' })
|
|
355
|
+
export class EmailTemplateApiService extends ApiResourceService<
|
|
356
|
+
IUpdateEmailTemplateDto,
|
|
357
|
+
IEmailTemplate
|
|
358
|
+
> {
|
|
359
|
+
constructor() {
|
|
360
|
+
const http = inject(HttpClient);
|
|
361
|
+
super('email/email-template', http);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Get template by slug (POST-only RPC pattern) */
|
|
365
|
+
getBySlug(slug: string): Observable<ISingleResponse<IEmailTemplate | null>> {
|
|
366
|
+
return this.http.post<ISingleResponse<IEmailTemplate | null>>(
|
|
367
|
+
`${this.baseUrl}/get-by-slug`,
|
|
368
|
+
{ slug },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### EmailSendService
|
|
375
|
+
|
|
376
|
+
Handles email sending operations.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
@Injectable({ providedIn: 'root' })
|
|
380
|
+
export class EmailSendService {
|
|
381
|
+
private readonly http = inject(HttpClient);
|
|
382
|
+
private readonly baseUrl = `${inject(APP_CONFIG).apiBaseUrl}/email/send`;
|
|
383
|
+
|
|
384
|
+
/** Send email directly (without template) */
|
|
385
|
+
sendDirect(dto: ISendEmailDto): Observable<ISingleResponse<ITestSendResult>>;
|
|
386
|
+
|
|
387
|
+
/** Send email using template */
|
|
388
|
+
sendTemplate(dto: ISendTemplateEmailDto): Observable<ISingleResponse<ITestSendResult>>;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### EmailBuilderStateService
|
|
393
|
+
|
|
394
|
+
Manages email builder UI state. Should be provided at component level, not root.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
@Injectable()
|
|
398
|
+
export class EmailBuilderStateService {
|
|
399
|
+
// Schema state
|
|
400
|
+
readonly schema: Signal<IEmailSchema>;
|
|
401
|
+
readonly isDirty: Signal<boolean>;
|
|
402
|
+
|
|
403
|
+
// Selection state
|
|
404
|
+
readonly selectedSectionId: Signal<string | null>;
|
|
405
|
+
readonly selectedBlockId: Signal<string | null>;
|
|
406
|
+
|
|
407
|
+
// Computed signals
|
|
408
|
+
readonly sections: Signal<IEmailSection[]>;
|
|
409
|
+
readonly headerSection: Signal<IEmailSection | null>;
|
|
410
|
+
readonly bodySection: Signal<IEmailSection | null>;
|
|
411
|
+
readonly footerSection: Signal<IEmailSection | null>;
|
|
412
|
+
|
|
413
|
+
// Methods
|
|
414
|
+
loadSchema(schema: IEmailSchema): void;
|
|
415
|
+
createNewSchema(name?: string): void;
|
|
416
|
+
addBlock(sectionId: string, blockType: EmailBlockType): string | null;
|
|
417
|
+
updateBlock(sectionId: string, blockId: string, updates: Partial<IEmailBlock>): void;
|
|
418
|
+
deleteBlock(sectionId: string, blockId: string): void;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Components
|
|
425
|
+
|
|
426
|
+
All components are standalone, use Angular 21 signals, OnPush change detection, and lazy-load via routes.
|
|
427
|
+
|
|
428
|
+
### EmailContainerComponent
|
|
429
|
+
|
|
430
|
+
Main container with tab navigation between Templates and Configurations.
|
|
431
|
+
|
|
432
|
+
**Features:**
|
|
433
|
+
- Responsive page title (`text-xl sm:text-2xl`)
|
|
434
|
+
- Tab navigation with horizontal scroll on mobile
|
|
435
|
+
- Router outlet for child routes
|
|
436
|
+
|
|
437
|
+
```html
|
|
438
|
+
<div class="p-2 sm:p-4">
|
|
439
|
+
<h1 class="text-xl sm:text-2xl font-bold m-0">Email</h1>
|
|
440
|
+
<p class="text-sm text-muted-color mt-1">Manage email templates...</p>
|
|
441
|
+
|
|
442
|
+
<div class="scrollbar-hide flex gap-1 mb-4 border-b border-surface overflow-x-auto">
|
|
443
|
+
@for (tab of tabs; track tab.id) {
|
|
444
|
+
<a [routerLink]="tab.route" routerLinkActive="tab-active">...</a>
|
|
445
|
+
}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<router-outlet />
|
|
449
|
+
</div>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### EmailConfigListComponent
|
|
453
|
+
|
|
454
|
+
Lists email configurations with actions.
|
|
455
|
+
|
|
456
|
+
**Features:**
|
|
457
|
+
- Lazy pagination via `p-table` with `[paginator]="totalRecords() > 0"`
|
|
458
|
+
- Horizontal scroll on mobile (`overflow-x-auto -mx-4 sm:mx-0`)
|
|
459
|
+
- Provider type tags (SMTP, SendGrid, Mailgun)
|
|
460
|
+
- Active/Inactive status badges
|
|
461
|
+
- Default configuration badge
|
|
462
|
+
- Test button with dialog for recipient input
|
|
463
|
+
- Edit and delete actions
|
|
464
|
+
- Company info display when enabled
|
|
465
|
+
|
|
466
|
+
**Test Dialog:**
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
showTestDialog = false; // Plain property for ngModel
|
|
470
|
+
readonly selectedConfig = signal<IEmailConfig | null>(null);
|
|
471
|
+
readonly isSendingTest = signal(false);
|
|
472
|
+
testRecipient = '';
|
|
473
|
+
|
|
474
|
+
onTest(config: IEmailConfig): void {
|
|
475
|
+
this.selectedConfig.set(config);
|
|
476
|
+
this.testRecipient = '';
|
|
477
|
+
this.showTestDialog = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async sendTestEmail(): Promise<void> {
|
|
481
|
+
const config = this.selectedConfig();
|
|
482
|
+
if (!config || !this.testRecipient) return;
|
|
483
|
+
|
|
484
|
+
const response = await firstValueFrom(
|
|
485
|
+
this.configService.sendTest(config.id, this.testRecipient)
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (response.data?.success) {
|
|
489
|
+
this.messageService.add({
|
|
490
|
+
severity: 'success',
|
|
491
|
+
summary: 'Success',
|
|
492
|
+
detail: `Test email sent! Message ID: ${response.data.messageId}`,
|
|
493
|
+
});
|
|
494
|
+
this.showTestDialog = false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### EmailConfigFormComponent
|
|
500
|
+
|
|
501
|
+
Create/edit email configuration with dynamic provider fields.
|
|
502
|
+
|
|
503
|
+
**Features:**
|
|
504
|
+
- Responsive grid layout (`grid-cols-1 md:grid-cols-2`)
|
|
505
|
+
- Dynamic form fields based on selected provider
|
|
506
|
+
- SMTP: host, port, secure toggle, username, password
|
|
507
|
+
- SendGrid: apiKey (masked input)
|
|
508
|
+
- Mailgun: apiKey, domain, region
|
|
509
|
+
- Active toggle and Set as Default toggle
|
|
510
|
+
- Form actions right-aligned at bottom
|
|
511
|
+
- Create/Edit mode auto-detection
|
|
512
|
+
|
|
513
|
+
### TemplateListComponent
|
|
514
|
+
|
|
515
|
+
Lists email templates with test send functionality.
|
|
516
|
+
|
|
517
|
+
**Features:**
|
|
518
|
+
- Lazy pagination with conditional display
|
|
519
|
+
- Responsive table with hidden columns on mobile
|
|
520
|
+
- HTML/Text type badges (based on `isHtml`)
|
|
521
|
+
- Active/Inactive status badges
|
|
522
|
+
- Test send with dynamic variables
|
|
523
|
+
- Edit and delete actions
|
|
524
|
+
|
|
525
|
+
**Test Send Dialog with Variables:**
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
/** Extract variables from template content */
|
|
529
|
+
readonly templateVariables = computed(() => {
|
|
530
|
+
const template = this.selectedTemplate();
|
|
531
|
+
if (!template) return [];
|
|
532
|
+
|
|
533
|
+
const content = [
|
|
534
|
+
template.subject,
|
|
535
|
+
template.htmlContent,
|
|
536
|
+
template.textContent || '',
|
|
537
|
+
].join(' ');
|
|
538
|
+
|
|
539
|
+
const matches = content.matchAll(/\{\{(\w+)\}\}/g);
|
|
540
|
+
const variables = new Set<string>();
|
|
541
|
+
|
|
542
|
+
for (const match of matches) {
|
|
543
|
+
variables.add(match[1]);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return Array.from(variables).sort();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
variableValues: Record<string, string> = {};
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### TemplateFormComponent
|
|
553
|
+
|
|
554
|
+
Create/edit email template with HTML/Plain Text toggle and live preview.
|
|
555
|
+
|
|
556
|
+
**Features:**
|
|
557
|
+
- Responsive form grid (`grid-cols-1 md:grid-cols-2`)
|
|
558
|
+
- Name, slug, subject, description fields
|
|
559
|
+
- Subject line with variable support hint
|
|
560
|
+
- Editor Mode Toggle (HTML/Plain Text buttons)
|
|
561
|
+
- Same height content editor and preview (`h-[400px]`)
|
|
562
|
+
- Horizontal scroll for code (`wrap="off"`)
|
|
563
|
+
- Live HTML preview in iframe with white background
|
|
564
|
+
- Content sync between modes when switching
|
|
565
|
+
- Active toggle
|
|
566
|
+
- Form actions right-aligned at bottom
|
|
567
|
+
|
|
568
|
+
**Preview with Light Background:**
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
getPreviewHtml(): string {
|
|
572
|
+
const baseStyles = `
|
|
573
|
+
<style>
|
|
574
|
+
body {
|
|
575
|
+
font-family: Arial, sans-serif;
|
|
576
|
+
margin: 16px;
|
|
577
|
+
background-color: #ffffff;
|
|
578
|
+
color: #333333;
|
|
579
|
+
}
|
|
580
|
+
img { max-width: 100%; height: auto; }
|
|
581
|
+
</style>
|
|
582
|
+
`;
|
|
583
|
+
return baseStyles + (this.formModel.htmlContent || '');
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Responsive Design Patterns
|
|
590
|
+
|
|
591
|
+
### Tables
|
|
592
|
+
|
|
593
|
+
```html
|
|
594
|
+
<!-- Horizontal scroll wrapper -->
|
|
595
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
596
|
+
<p-table
|
|
597
|
+
[paginator]="totalRecords() > 0"
|
|
598
|
+
styleClass="p-datatable-sm"
|
|
599
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
600
|
+
>
|
|
601
|
+
<!-- Hidden columns on smaller screens -->
|
|
602
|
+
<th class="hidden md:table-cell">Slug</th>
|
|
603
|
+
<th class="hidden lg:table-cell">Subject</th>
|
|
604
|
+
<th class="hidden xl:table-cell">Created</th>
|
|
605
|
+
</p-table>
|
|
606
|
+
</div>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Forms
|
|
610
|
+
|
|
611
|
+
```html
|
|
612
|
+
<!-- Responsive grid -->
|
|
613
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
614
|
+
<div class="field">...</div>
|
|
615
|
+
<div class="field md:col-span-2">...</div> <!-- Full width on all screens -->
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<!-- Form actions -->
|
|
619
|
+
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-surface">
|
|
620
|
+
<p-button label="Cancel" severity="secondary" [outlined]="true" routerLink="../" />
|
|
621
|
+
<p-button [label]="isEditMode() ? 'Update' : 'Create'" icon="pi pi-save" />
|
|
622
|
+
</div>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Headers
|
|
626
|
+
|
|
627
|
+
```html
|
|
628
|
+
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4">
|
|
629
|
+
<div>
|
|
630
|
+
<h3 class="text-lg sm:text-xl font-semibold m-0">Title</h3>
|
|
631
|
+
@if (showCompanyInfo()) {
|
|
632
|
+
<p class="text-sm text-muted-color mt-1">Company: {{ currentCompanyName() }}</p>
|
|
633
|
+
}
|
|
634
|
+
</div>
|
|
635
|
+
<p-button label="Action" styleClass="w-full sm:w-auto" />
|
|
636
|
+
</div>
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Dark Mode Support
|
|
642
|
+
|
|
643
|
+
All components use PrimeNG/Tailwind dark mode classes:
|
|
644
|
+
|
|
645
|
+
| Element | Classes |
|
|
646
|
+
|---------|---------|
|
|
647
|
+
| Borders | `border-surface` |
|
|
648
|
+
| Text | `text-muted-color` |
|
|
649
|
+
| Backgrounds | `bg-surface-0 dark:bg-surface-900`, `bg-surface-100 dark:bg-surface-700` |
|
|
650
|
+
| Code blocks | `bg-surface-100 dark:bg-surface-700` |
|
|
651
|
+
| Hover states | `hover:bg-surface-hover` |
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Usage Examples
|
|
656
|
+
|
|
657
|
+
### Send Direct Email
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { EmailSendService } from '@flusys/ng-email';
|
|
661
|
+
|
|
662
|
+
private readonly emailSendService = inject(EmailSendService);
|
|
663
|
+
|
|
664
|
+
async sendEmail(): Promise<void> {
|
|
665
|
+
const response = await firstValueFrom(
|
|
666
|
+
this.emailSendService.sendDirect({
|
|
667
|
+
to: 'user@example.com',
|
|
668
|
+
subject: 'Hello!',
|
|
669
|
+
html: '<h1>Hello World</h1>',
|
|
670
|
+
text: 'Hello World',
|
|
671
|
+
emailConfigId: 'config-id',
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
if (response.data?.success) {
|
|
676
|
+
console.log('Sent! Message ID:', response.data.messageId);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Send Template Email
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
async sendTemplateEmail(): Promise<void> {
|
|
685
|
+
const response = await firstValueFrom(
|
|
686
|
+
this.emailSendService.sendTemplate({
|
|
687
|
+
templateId: 'template-id',
|
|
688
|
+
to: 'user@example.com',
|
|
689
|
+
emailConfigId: 'config-id',
|
|
690
|
+
variables: {
|
|
691
|
+
userName: 'John',
|
|
692
|
+
appName: 'My App',
|
|
693
|
+
verificationLink: 'https://example.com/verify/abc123',
|
|
694
|
+
},
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
if (response.data?.success) {
|
|
699
|
+
console.log('Sent! Message ID:', response.data.messageId);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Test Configuration
|
|
705
|
+
|
|
706
|
+
```typescript
|
|
707
|
+
import { EmailConfigApiService } from '@flusys/ng-email';
|
|
708
|
+
|
|
709
|
+
private readonly configService = inject(EmailConfigApiService);
|
|
710
|
+
|
|
711
|
+
async testConfig(configId: string, recipient: string): Promise<boolean> {
|
|
712
|
+
const response = await firstValueFrom(
|
|
713
|
+
this.configService.sendTest(configId, recipient)
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
return response.data?.success ?? false;
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Get Template by Slug
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
import { EmailTemplateApiService } from '@flusys/ng-email';
|
|
724
|
+
|
|
725
|
+
private readonly templateService = inject(EmailTemplateApiService);
|
|
726
|
+
|
|
727
|
+
async getTemplate(slug: string): Promise<IEmailTemplate | null> {
|
|
728
|
+
const response = await firstValueFrom(
|
|
729
|
+
this.templateService.getBySlug(slug)
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
return response.data ?? null;
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Backend Integration
|
|
739
|
+
|
|
740
|
+
All endpoints use **POST-only RPC** style (not REST).
|
|
741
|
+
|
|
742
|
+
| Service | Endpoint | Description |
|
|
743
|
+
|---------|----------|-------------|
|
|
744
|
+
| Config | `POST /email/email-config/insert` | Create config |
|
|
745
|
+
| Config | `POST /email/email-config/get/:id` | Get config by ID |
|
|
746
|
+
| Config | `POST /email/email-config/get-all` | List configs (paginated) |
|
|
747
|
+
| Config | `POST /email/email-config/update` | Update config |
|
|
748
|
+
| Config | `POST /email/email-config/delete` | Delete config |
|
|
749
|
+
| Template | `POST /email/email-template/insert` | Create template |
|
|
750
|
+
| Template | `POST /email/email-template/get/:id` | Get template by ID |
|
|
751
|
+
| Template | `POST /email/email-template/get-all` | List templates (paginated) |
|
|
752
|
+
| Template | `POST /email/email-template/get-by-slug` | Get template by slug |
|
|
753
|
+
| Template | `POST /email/email-template/update` | Update template |
|
|
754
|
+
| Template | `POST /email/email-template/delete` | Delete template |
|
|
755
|
+
| Send | `POST /email/send/direct` | Send email directly |
|
|
756
|
+
| Send | `POST /email/send/template` | Send using template |
|
|
757
|
+
| Send | `POST /email/send/test` | Test configuration |
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Best Practices
|
|
762
|
+
|
|
763
|
+
### 1. Use Template Slugs for Code References
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
// ✅ Use slug for programmatic access
|
|
767
|
+
await this.emailSendService.sendTemplate({
|
|
768
|
+
templateSlug: 'welcome-email', // Stable identifier
|
|
769
|
+
...
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// ❌ Don't hardcode template IDs
|
|
773
|
+
await this.emailSendService.sendTemplate({
|
|
774
|
+
templateId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', // May change
|
|
775
|
+
...
|
|
776
|
+
});
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### 2. Provide Both HTML and Text Content
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
// ✅ Provide both for maximum compatibility
|
|
783
|
+
{
|
|
784
|
+
isHtml: true,
|
|
785
|
+
htmlContent: '<h1>Hello!</h1>',
|
|
786
|
+
textContent: 'Hello!', // Fallback for text-only clients
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 3. Test Configurations Before Production
|
|
791
|
+
|
|
792
|
+
Always use the test send feature to verify configurations work before using them in production.
|
|
793
|
+
|
|
794
|
+
### 4. Use Meaningful Variable Names
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
// ✅ Clear, descriptive variable names
|
|
798
|
+
"Hello {{userName}}, your order #{{orderNumber}} shipped on {{shipDate}}."
|
|
799
|
+
|
|
800
|
+
// ❌ Vague or abbreviated names
|
|
801
|
+
"Hello {{u}}, order {{o}} shipped {{d}}."
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### 5. Handle Send Failures Gracefully
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
async sendEmail(): Promise<void> {
|
|
808
|
+
try {
|
|
809
|
+
const response = await firstValueFrom(
|
|
810
|
+
this.emailSendService.sendTemplate({ ... })
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
if (response.data?.success) {
|
|
814
|
+
this.messageService.add({
|
|
815
|
+
severity: 'success',
|
|
816
|
+
summary: 'Email Sent',
|
|
817
|
+
detail: `Message ID: ${response.data.messageId}`,
|
|
818
|
+
});
|
|
819
|
+
} else {
|
|
820
|
+
this.messageService.add({
|
|
821
|
+
severity: 'error',
|
|
822
|
+
summary: 'Send Failed',
|
|
823
|
+
detail: response.data?.error || 'Unknown error occurred',
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
} catch (error: any) {
|
|
827
|
+
this.messageService.add({
|
|
828
|
+
severity: 'error',
|
|
829
|
+
summary: 'Error',
|
|
830
|
+
detail: error?.message || 'Failed to send email.',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Public API Exports
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
// Enums
|
|
842
|
+
export { EmailProviderEnum } from '@flusys/ng-email';
|
|
843
|
+
export { EmailBlockType } from '@flusys/ng-email';
|
|
844
|
+
|
|
845
|
+
// Interfaces
|
|
846
|
+
export {
|
|
847
|
+
IEmailConfig, ICreateEmailConfigDto, IUpdateEmailConfigDto,
|
|
848
|
+
ISmtpConfig, ISendGridConfig, IMailgunConfig,
|
|
849
|
+
IEmailTemplate, ICreateEmailTemplateDto, IUpdateEmailTemplateDto,
|
|
850
|
+
IEmailSchema, IEmailSection, IEmailBlock, IEmailTemplateVariable,
|
|
851
|
+
ISendEmailDto, ISendTemplateEmailDto, ITestSendResult,
|
|
852
|
+
} from '@flusys/ng-email';
|
|
853
|
+
|
|
854
|
+
// Services
|
|
855
|
+
export {
|
|
856
|
+
EmailConfigApiService,
|
|
857
|
+
EmailTemplateApiService,
|
|
858
|
+
EmailSendService,
|
|
859
|
+
EmailBuilderStateService,
|
|
860
|
+
} from '@flusys/ng-email';
|
|
861
|
+
|
|
862
|
+
// Components
|
|
863
|
+
export { EmailContainerComponent } from '@flusys/ng-email';
|
|
864
|
+
|
|
865
|
+
// Routes
|
|
866
|
+
export { EMAIL_ROUTES } from '@flusys/ng-email';
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
**Last Updated:** 2026-02-18
|
|
872
|
+
**Angular Version:** 21
|