@flusys/ng-email 1.1.0-beta → 2.0.0-rc.0
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 +380 -168
- package/fesm2022/{flusys-ng-email-email-config-form.component-8dOBD-Q-.mjs → flusys-ng-email-email-config-form.component-CC4bsd_p.mjs} +234 -173
- package/fesm2022/flusys-ng-email-email-config-form.component-CC4bsd_p.mjs.map +1 -0
- package/fesm2022/{flusys-ng-email-email-config-list.component-CJhSPkCZ.mjs → flusys-ng-email-email-config-list.component-eW0pbwjL.mjs} +245 -234
- package/fesm2022/flusys-ng-email-email-config-list.component-eW0pbwjL.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-template-form.component-BFtOKk5a.mjs +563 -0
- package/fesm2022/flusys-ng-email-template-form.component-BFtOKk5a.mjs.map +1 -0
- package/fesm2022/{flusys-ng-email-template-list.component-CGJxfZMT.mjs → flusys-ng-email-template-list.component-C6QUg8_5.mjs} +268 -228
- package/fesm2022/flusys-ng-email-template-list.component-C6QUg8_5.mjs.map +1 -0
- package/fesm2022/flusys-ng-email.mjs +98 -77
- package/fesm2022/flusys-ng-email.mjs.map +1 -1
- package/package.json +4 -3
- package/types/flusys-ng-email.d.ts +70 -48
- package/fesm2022/flusys-ng-email-email-config-form.component-8dOBD-Q-.mjs.map +0 -1
- package/fesm2022/flusys-ng-email-email-config-list.component-CJhSPkCZ.mjs.map +0 -1
- package/fesm2022/flusys-ng-email-template-form.component-DjzG_lG1.mjs +0 -529
- package/fesm2022/flusys-ng-email-template-form.component-DjzG_lG1.mjs.map +0 -1
- package/fesm2022/flusys-ng-email-template-list.component-CGJxfZMT.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
- **Email Configurations** - CRUD for SMTP, SendGrid, Mailgun providers
|
|
13
13
|
- **Email Templates** - Visual template management with variable interpolation
|
|
14
|
-
- **HTML/Plain Text** - Toggle between content types
|
|
14
|
+
- **HTML/Plain Text** - Toggle between content types with live preview
|
|
15
15
|
- **Test Sending** - Test configurations and templates with dynamic variables
|
|
16
16
|
- **Company Scoping** - Automatic company-scoped queries (when enabled)
|
|
17
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
|
|
18
20
|
|
|
19
21
|
### Package Hierarchy
|
|
20
22
|
|
|
21
23
|
```
|
|
22
|
-
@flusys/ng-core <- Foundation (
|
|
24
|
+
@flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME)
|
|
23
25
|
|
|
|
24
26
|
@flusys/ng-shared <- Shared utilities (ApiResourceService, IBaseEntity)
|
|
25
27
|
|
|
|
@@ -34,11 +36,17 @@ Email components use `LAYOUT_AUTH_STATE` from `@flusys/ng-layout` for company co
|
|
|
34
36
|
|
|
35
37
|
```typescript
|
|
36
38
|
import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
|
|
39
|
+
import { APP_CONFIG, DEFAULT_APP_NAME } from '@flusys/ng-core';
|
|
37
40
|
|
|
38
|
-
|
|
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
|
+
);
|
|
39
47
|
|
|
40
48
|
readonly currentCompanyName = computed(
|
|
41
|
-
() => this.companyContext
|
|
49
|
+
() => this.companyContext?.currentCompanyInfo()?.name ?? DEFAULT_APP_NAME,
|
|
42
50
|
);
|
|
43
51
|
```
|
|
44
52
|
|
|
@@ -48,17 +56,25 @@ readonly currentCompanyName = computed(
|
|
|
48
56
|
|
|
49
57
|
```
|
|
50
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
|
|
51
63
|
├── interfaces/
|
|
52
|
-
│ ├── email-config.interface.ts
|
|
53
|
-
│ ├── email-template.interface.ts
|
|
54
|
-
│ ├── email-
|
|
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
|
|
55
68
|
│ └── public-api.ts
|
|
56
69
|
├── services/
|
|
57
|
-
│ ├── email-config-api.service.ts
|
|
58
|
-
│ ├── email-template-api.service.ts # Template CRUD
|
|
59
|
-
│ ├── email-send.service.ts
|
|
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
|
|
60
74
|
│ └── public-api.ts
|
|
61
75
|
├── pages/
|
|
76
|
+
│ ├── email-container/
|
|
77
|
+
│ │ └── email-container.component.ts # Main container with tabs
|
|
62
78
|
│ ├── config/
|
|
63
79
|
│ │ ├── email-config-list.component.ts
|
|
64
80
|
│ │ └── email-config-form.component.ts
|
|
@@ -92,12 +108,39 @@ export const routes: Routes = [
|
|
|
92
108
|
|
|
93
109
|
| Path | Component | Description |
|
|
94
110
|
|------|-----------|-------------|
|
|
95
|
-
| `/email
|
|
96
|
-
| `/email/configs/new` | EmailConfigFormComponent | Create new config |
|
|
97
|
-
| `/email/configs/:id` | EmailConfigFormComponent | Edit config |
|
|
111
|
+
| `/email` | EmailContainerComponent | Main container with tab navigation |
|
|
98
112
|
| `/email/templates` | TemplateListComponent | List email templates |
|
|
99
113
|
| `/email/templates/new` | TemplateFormComponent | Create new template |
|
|
100
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
|
+
```
|
|
101
144
|
|
|
102
145
|
---
|
|
103
146
|
|
|
@@ -108,11 +151,10 @@ export const routes: Routes = [
|
|
|
108
151
|
```typescript
|
|
109
152
|
interface IEmailConfig extends IBaseEntity {
|
|
110
153
|
name: string;
|
|
111
|
-
provider:
|
|
154
|
+
provider: EmailProviderEnum;
|
|
112
155
|
config: Record<string, any>; // Provider-specific config
|
|
113
|
-
fromEmail: string;
|
|
156
|
+
fromEmail: string | null;
|
|
114
157
|
fromName: string | null;
|
|
115
|
-
replyTo: string | null;
|
|
116
158
|
isActive: boolean;
|
|
117
159
|
isDefault: boolean; // Set as default configuration
|
|
118
160
|
companyId?: string | null;
|
|
@@ -120,25 +162,16 @@ interface IEmailConfig extends IBaseEntity {
|
|
|
120
162
|
|
|
121
163
|
interface ICreateEmailConfigDto {
|
|
122
164
|
name: string;
|
|
123
|
-
provider:
|
|
165
|
+
provider: EmailProviderEnum;
|
|
124
166
|
config: Record<string, any>;
|
|
125
|
-
fromEmail
|
|
167
|
+
fromEmail?: string;
|
|
126
168
|
fromName?: string;
|
|
127
|
-
replyTo?: string;
|
|
128
169
|
isActive?: boolean;
|
|
129
170
|
isDefault?: boolean;
|
|
130
171
|
}
|
|
131
172
|
|
|
132
|
-
interface IUpdateEmailConfigDto {
|
|
173
|
+
interface IUpdateEmailConfigDto extends Partial<ICreateEmailConfigDto> {
|
|
133
174
|
id: string;
|
|
134
|
-
name?: string;
|
|
135
|
-
provider?: 'smtp' | 'sendgrid' | 'mailgun';
|
|
136
|
-
config?: Record<string, any>;
|
|
137
|
-
fromEmail?: string;
|
|
138
|
-
fromName?: string;
|
|
139
|
-
replyTo?: string;
|
|
140
|
-
isActive?: boolean;
|
|
141
|
-
isDefault?: boolean;
|
|
142
175
|
}
|
|
143
176
|
```
|
|
144
177
|
|
|
@@ -149,11 +182,9 @@ interface IUpdateEmailConfigDto {
|
|
|
149
182
|
interface ISmtpConfig {
|
|
150
183
|
host: string;
|
|
151
184
|
port: number;
|
|
152
|
-
secure
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
pass: string;
|
|
156
|
-
};
|
|
185
|
+
secure?: boolean;
|
|
186
|
+
username: string;
|
|
187
|
+
password: string;
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
// SendGrid
|
|
@@ -177,7 +208,7 @@ interface IEmailTemplate extends IBaseEntity {
|
|
|
177
208
|
slug: string;
|
|
178
209
|
description: string | null;
|
|
179
210
|
subject: string;
|
|
180
|
-
schema:
|
|
211
|
+
schema: IEmailSchema;
|
|
181
212
|
htmlContent: string;
|
|
182
213
|
textContent: string | null;
|
|
183
214
|
schemaVersion: number;
|
|
@@ -192,30 +223,69 @@ interface ICreateEmailTemplateDto {
|
|
|
192
223
|
slug: string;
|
|
193
224
|
description?: string;
|
|
194
225
|
subject: string;
|
|
195
|
-
schema
|
|
226
|
+
schema: IEmailSchema;
|
|
196
227
|
htmlContent: string;
|
|
197
228
|
textContent?: string;
|
|
198
229
|
isActive?: boolean;
|
|
199
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;
|
|
200
251
|
}
|
|
201
252
|
|
|
202
|
-
interface
|
|
253
|
+
interface IEmailSection {
|
|
203
254
|
id: string;
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
206
275
|
description?: string;
|
|
207
|
-
subject?: string;
|
|
208
|
-
schema?: Record<string, unknown>;
|
|
209
|
-
htmlContent?: string;
|
|
210
|
-
textContent?: string;
|
|
211
|
-
isActive?: boolean;
|
|
212
|
-
isHtml?: boolean;
|
|
213
276
|
}
|
|
214
277
|
```
|
|
215
278
|
|
|
216
279
|
### Email Sending
|
|
217
280
|
|
|
218
281
|
```typescript
|
|
282
|
+
// Test send result
|
|
283
|
+
interface ITestSendResult {
|
|
284
|
+
success: boolean;
|
|
285
|
+
messageId?: string;
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
219
289
|
// Direct email
|
|
220
290
|
interface ISendEmailDto {
|
|
221
291
|
to: string | string[];
|
|
@@ -228,7 +298,6 @@ interface ISendEmailDto {
|
|
|
228
298
|
fromName?: string;
|
|
229
299
|
replyTo?: string;
|
|
230
300
|
emailConfigId?: string;
|
|
231
|
-
attachments?: IEmailAttachmentDto[];
|
|
232
301
|
}
|
|
233
302
|
|
|
234
303
|
// Template email
|
|
@@ -243,27 +312,6 @@ interface ISendTemplateEmailDto {
|
|
|
243
312
|
fromName?: string;
|
|
244
313
|
replyTo?: string;
|
|
245
314
|
emailConfigId?: string;
|
|
246
|
-
attachments?: IEmailAttachmentDto[];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Test email
|
|
250
|
-
interface ITestEmailDto {
|
|
251
|
-
emailConfigId: string;
|
|
252
|
-
recipient: string;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Attachment
|
|
256
|
-
interface IEmailAttachmentDto {
|
|
257
|
-
filename: string;
|
|
258
|
-
content: string; // Base64 encoded
|
|
259
|
-
contentType?: string;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Result
|
|
263
|
-
interface IEmailSendResult {
|
|
264
|
-
success: boolean;
|
|
265
|
-
messageId?: string;
|
|
266
|
-
error?: string;
|
|
267
315
|
}
|
|
268
316
|
```
|
|
269
317
|
|
|
@@ -277,54 +325,156 @@ All services are `providedIn: 'root'`. Company scoping is automatic when the com
|
|
|
277
325
|
|
|
278
326
|
Email configuration CRUD. Extends `ApiResourceService<IUpdateEmailConfigDto, IEmailConfig>`.
|
|
279
327
|
|
|
280
|
-
|
|
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
|
+
```
|
|
281
348
|
|
|
282
349
|
### EmailTemplateApiService
|
|
283
350
|
|
|
284
351
|
Email template CRUD. Extends `ApiResourceService<IUpdateEmailTemplateDto, IEmailTemplate>`.
|
|
285
352
|
|
|
286
|
-
|
|
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
|
+
```
|
|
287
373
|
|
|
288
374
|
### EmailSendService
|
|
289
375
|
|
|
290
|
-
Handles email sending operations.
|
|
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
|
|
291
393
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
```
|
|
297
421
|
|
|
298
422
|
---
|
|
299
423
|
|
|
300
424
|
## Components
|
|
301
425
|
|
|
302
|
-
All components are standalone, use Angular 21 signals, and lazy-load via routes.
|
|
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
|
+
```
|
|
303
451
|
|
|
304
452
|
### EmailConfigListComponent
|
|
305
453
|
|
|
306
454
|
Lists email configurations with actions.
|
|
307
455
|
|
|
308
456
|
**Features:**
|
|
309
|
-
- Lazy pagination via `p-
|
|
457
|
+
- Lazy pagination via `p-table` with `[paginator]="totalRecords() > 0"`
|
|
458
|
+
- Horizontal scroll on mobile (`overflow-x-auto -mx-4 sm:mx-0`)
|
|
310
459
|
- Provider type tags (SMTP, SendGrid, Mailgun)
|
|
311
460
|
- Active/Inactive status badges
|
|
461
|
+
- Default configuration badge
|
|
312
462
|
- Test button with dialog for recipient input
|
|
313
463
|
- Edit and delete actions
|
|
314
464
|
- Company info display when enabled
|
|
315
465
|
|
|
316
466
|
**Test Dialog:**
|
|
317
|
-
Opens a PrimeNG dialog to enter test recipient email instead of browser `prompt()`.
|
|
318
467
|
|
|
319
468
|
```typescript
|
|
320
|
-
|
|
469
|
+
showTestDialog = false; // Plain property for ngModel
|
|
321
470
|
readonly selectedConfig = signal<IEmailConfig | null>(null);
|
|
471
|
+
readonly isSendingTest = signal(false);
|
|
322
472
|
testRecipient = '';
|
|
323
473
|
|
|
324
474
|
onTest(config: IEmailConfig): void {
|
|
325
475
|
this.selectedConfig.set(config);
|
|
326
476
|
this.testRecipient = '';
|
|
327
|
-
this.showTestDialog
|
|
477
|
+
this.showTestDialog = true;
|
|
328
478
|
}
|
|
329
479
|
|
|
330
480
|
async sendTestEmail(): Promise<void> {
|
|
@@ -332,10 +482,7 @@ async sendTestEmail(): Promise<void> {
|
|
|
332
482
|
if (!config || !this.testRecipient) return;
|
|
333
483
|
|
|
334
484
|
const response = await firstValueFrom(
|
|
335
|
-
this.
|
|
336
|
-
emailConfigId: config.id,
|
|
337
|
-
recipient: this.testRecipient,
|
|
338
|
-
})
|
|
485
|
+
this.configService.sendTest(config.id, this.testRecipient)
|
|
339
486
|
);
|
|
340
487
|
|
|
341
488
|
if (response.data?.success) {
|
|
@@ -344,7 +491,7 @@ async sendTestEmail(): Promise<void> {
|
|
|
344
491
|
summary: 'Success',
|
|
345
492
|
detail: `Test email sent! Message ID: ${response.data.messageId}`,
|
|
346
493
|
});
|
|
347
|
-
this.showTestDialog
|
|
494
|
+
this.showTestDialog = false;
|
|
348
495
|
}
|
|
349
496
|
}
|
|
350
497
|
```
|
|
@@ -354,32 +501,28 @@ async sendTestEmail(): Promise<void> {
|
|
|
354
501
|
Create/edit email configuration with dynamic provider fields.
|
|
355
502
|
|
|
356
503
|
**Features:**
|
|
504
|
+
- Responsive grid layout (`grid-cols-1 md:grid-cols-2`)
|
|
357
505
|
- Dynamic form fields based on selected provider
|
|
358
|
-
- SMTP: host, port, secure, username, password
|
|
359
|
-
- SendGrid: apiKey
|
|
506
|
+
- SMTP: host, port, secure toggle, username, password
|
|
507
|
+
- SendGrid: apiKey (masked input)
|
|
360
508
|
- Mailgun: apiKey, domain, region
|
|
361
|
-
- Active toggle
|
|
362
|
-
-
|
|
509
|
+
- Active toggle and Set as Default toggle
|
|
510
|
+
- Form actions right-aligned at bottom
|
|
363
511
|
- Create/Edit mode auto-detection
|
|
364
512
|
|
|
365
|
-
**Default Configuration:**
|
|
366
|
-
When `isDefault: true`, this configuration will be used automatically when no `emailConfigId` is provided in send requests.
|
|
367
|
-
|
|
368
513
|
### TemplateListComponent
|
|
369
514
|
|
|
370
515
|
Lists email templates with test send functionality.
|
|
371
516
|
|
|
372
517
|
**Features:**
|
|
373
|
-
- Lazy pagination
|
|
518
|
+
- Lazy pagination with conditional display
|
|
519
|
+
- Responsive table with hidden columns on mobile
|
|
374
520
|
- HTML/Text type badges (based on `isHtml`)
|
|
375
521
|
- Active/Inactive status badges
|
|
376
|
-
- Test send
|
|
522
|
+
- Test send with dynamic variables
|
|
377
523
|
- Edit and delete actions
|
|
378
524
|
|
|
379
|
-
**Test Send Dialog:**
|
|
380
|
-
- Select email configuration
|
|
381
|
-
- Enter recipient email
|
|
382
|
-
- **Dynamic variable inputs** extracted from template content
|
|
525
|
+
**Test Send Dialog with Variables:**
|
|
383
526
|
|
|
384
527
|
```typescript
|
|
385
528
|
/** Extract variables from template content */
|
|
@@ -387,14 +530,12 @@ readonly templateVariables = computed(() => {
|
|
|
387
530
|
const template = this.selectedTemplate();
|
|
388
531
|
if (!template) return [];
|
|
389
532
|
|
|
390
|
-
// Combine all content sources to extract variables
|
|
391
533
|
const content = [
|
|
392
534
|
template.subject,
|
|
393
535
|
template.htmlContent,
|
|
394
536
|
template.textContent || '',
|
|
395
537
|
].join(' ');
|
|
396
538
|
|
|
397
|
-
// Find all {{variableName}} patterns
|
|
398
539
|
const matches = content.matchAll(/\{\{(\w+)\}\}/g);
|
|
399
540
|
const variables = new Set<string>();
|
|
400
541
|
|
|
@@ -405,48 +546,112 @@ readonly templateVariables = computed(() => {
|
|
|
405
546
|
return Array.from(variables).sort();
|
|
406
547
|
});
|
|
407
548
|
|
|
408
|
-
// Variable values bound to form inputs
|
|
409
549
|
variableValues: Record<string, string> = {};
|
|
410
550
|
```
|
|
411
551
|
|
|
412
552
|
### TemplateFormComponent
|
|
413
553
|
|
|
414
|
-
Create/edit email template with HTML/Plain Text toggle.
|
|
554
|
+
Create/edit email template with HTML/Plain Text toggle and live preview.
|
|
415
555
|
|
|
416
556
|
**Features:**
|
|
417
|
-
-
|
|
418
|
-
-
|
|
419
|
-
-
|
|
420
|
-
-
|
|
421
|
-
-
|
|
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
|
|
422
564
|
- Content sync between modes when switching
|
|
423
|
-
- Preview with variable highlighting
|
|
424
565
|
- Active toggle
|
|
566
|
+
- Form actions right-aligned at bottom
|
|
425
567
|
|
|
426
|
-
**
|
|
568
|
+
**Preview with Light Background:**
|
|
427
569
|
|
|
428
570
|
```typescript
|
|
429
|
-
|
|
430
|
-
const
|
|
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
|
+
```
|
|
431
586
|
|
|
432
|
-
|
|
433
|
-
if (currentMode === 'text' && mode === 'html') {
|
|
434
|
-
if (!this.formModel.htmlContent && this.formModel.textContent) {
|
|
435
|
-
this.formModel.htmlContent = this.textToHtml(this.formModel.textContent);
|
|
436
|
-
}
|
|
437
|
-
} else if (currentMode === 'html' && mode === 'text') {
|
|
438
|
-
if (!this.formModel.textContent && this.formModel.htmlContent) {
|
|
439
|
-
this.formModel.textContent = this.htmlToPlainText(this.formModel.htmlContent);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
587
|
+
---
|
|
442
588
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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>
|
|
446
637
|
```
|
|
447
638
|
|
|
448
639
|
---
|
|
449
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
|
+
|
|
450
655
|
## Usage Examples
|
|
451
656
|
|
|
452
657
|
### Send Direct Email
|
|
@@ -499,42 +704,32 @@ async sendTemplateEmail(): Promise<void> {
|
|
|
499
704
|
### Test Configuration
|
|
500
705
|
|
|
501
706
|
```typescript
|
|
707
|
+
import { EmailConfigApiService } from '@flusys/ng-email';
|
|
708
|
+
|
|
709
|
+
private readonly configService = inject(EmailConfigApiService);
|
|
710
|
+
|
|
502
711
|
async testConfig(configId: string, recipient: string): Promise<boolean> {
|
|
503
712
|
const response = await firstValueFrom(
|
|
504
|
-
this.
|
|
505
|
-
emailConfigId: configId,
|
|
506
|
-
recipient: recipient,
|
|
507
|
-
})
|
|
713
|
+
this.configService.sendTest(configId, recipient)
|
|
508
714
|
);
|
|
509
715
|
|
|
510
716
|
return response.data?.success ?? false;
|
|
511
717
|
}
|
|
512
718
|
```
|
|
513
719
|
|
|
514
|
-
###
|
|
720
|
+
### Get Template by Slug
|
|
515
721
|
|
|
516
722
|
```typescript
|
|
517
723
|
import { EmailTemplateApiService } from '@flusys/ng-email';
|
|
518
724
|
|
|
519
725
|
private readonly templateService = inject(EmailTemplateApiService);
|
|
520
726
|
|
|
521
|
-
async
|
|
727
|
+
async getTemplate(slug: string): Promise<IEmailTemplate | null> {
|
|
522
728
|
const response = await firstValueFrom(
|
|
523
|
-
this.templateService.
|
|
524
|
-
name: 'Welcome Email',
|
|
525
|
-
slug: 'welcome-email',
|
|
526
|
-
description: 'Sent to new users',
|
|
527
|
-
subject: 'Welcome {{userName}}!',
|
|
528
|
-
isHtml: true,
|
|
529
|
-
htmlContent: '<h1>Welcome, {{userName}}!</h1><p>Thank you for joining.</p>',
|
|
530
|
-
textContent: 'Welcome, {{userName}}! Thank you for joining.',
|
|
531
|
-
isActive: true,
|
|
532
|
-
})
|
|
729
|
+
this.templateService.getBySlug(slug)
|
|
533
730
|
);
|
|
534
731
|
|
|
535
|
-
|
|
536
|
-
console.log('Template created:', response.data?.id);
|
|
537
|
-
}
|
|
732
|
+
return response.data ?? null;
|
|
538
733
|
}
|
|
539
734
|
```
|
|
540
735
|
|
|
@@ -546,16 +741,17 @@ All endpoints use **POST-only RPC** style (not REST).
|
|
|
546
741
|
|
|
547
742
|
| Service | Endpoint | Description |
|
|
548
743
|
|---------|----------|-------------|
|
|
549
|
-
| Config | `POST /email/config/insert` | Create config |
|
|
550
|
-
| Config | `POST /email/config/get/:id` | Get config by ID |
|
|
551
|
-
| Config | `POST /email/config/get-all` | List configs (paginated) |
|
|
552
|
-
| Config | `POST /email/config/update` | Update config |
|
|
553
|
-
| Config | `POST /email/config/delete` | Delete config |
|
|
554
|
-
| Template | `POST /email/template/insert` | Create template |
|
|
555
|
-
| Template | `POST /email/template/get/:id` | Get template by ID |
|
|
556
|
-
| Template | `POST /email/template/get-all` | List templates (paginated) |
|
|
557
|
-
| Template | `POST /email/template/
|
|
558
|
-
| Template | `POST /email/template/
|
|
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 |
|
|
559
755
|
| Send | `POST /email/send/direct` | Send email directly |
|
|
560
756
|
| Send | `POST /email/send/template` | Send using template |
|
|
561
757
|
| Send | `POST /email/send/test` | Test configuration |
|
|
@@ -609,21 +805,29 @@ Always use the test send feature to verify configurations work before using them
|
|
|
609
805
|
|
|
610
806
|
```typescript
|
|
611
807
|
async sendEmail(): Promise<void> {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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) {
|
|
623
827
|
this.messageService.add({
|
|
624
828
|
severity: 'error',
|
|
625
|
-
summary: '
|
|
626
|
-
detail:
|
|
829
|
+
summary: 'Error',
|
|
830
|
+
detail: error?.message || 'Failed to send email.',
|
|
627
831
|
});
|
|
628
832
|
}
|
|
629
833
|
}
|
|
@@ -634,13 +838,17 @@ async sendEmail(): Promise<void> {
|
|
|
634
838
|
## Public API Exports
|
|
635
839
|
|
|
636
840
|
```typescript
|
|
841
|
+
// Enums
|
|
842
|
+
export { EmailProviderEnum } from '@flusys/ng-email';
|
|
843
|
+
export { EmailBlockType } from '@flusys/ng-email';
|
|
844
|
+
|
|
637
845
|
// Interfaces
|
|
638
846
|
export {
|
|
639
847
|
IEmailConfig, ICreateEmailConfigDto, IUpdateEmailConfigDto,
|
|
640
848
|
ISmtpConfig, ISendGridConfig, IMailgunConfig,
|
|
641
849
|
IEmailTemplate, ICreateEmailTemplateDto, IUpdateEmailTemplateDto,
|
|
642
|
-
|
|
643
|
-
|
|
850
|
+
IEmailSchema, IEmailSection, IEmailBlock, IEmailTemplateVariable,
|
|
851
|
+
ISendEmailDto, ISendTemplateEmailDto, ITestSendResult,
|
|
644
852
|
} from '@flusys/ng-email';
|
|
645
853
|
|
|
646
854
|
// Services
|
|
@@ -648,13 +856,17 @@ export {
|
|
|
648
856
|
EmailConfigApiService,
|
|
649
857
|
EmailTemplateApiService,
|
|
650
858
|
EmailSendService,
|
|
859
|
+
EmailBuilderStateService,
|
|
651
860
|
} from '@flusys/ng-email';
|
|
652
861
|
|
|
862
|
+
// Components
|
|
863
|
+
export { EmailContainerComponent } from '@flusys/ng-email';
|
|
864
|
+
|
|
653
865
|
// Routes
|
|
654
866
|
export { EMAIL_ROUTES } from '@flusys/ng-email';
|
|
655
867
|
```
|
|
656
868
|
|
|
657
869
|
---
|
|
658
870
|
|
|
659
|
-
**Last Updated:** 2026-02-
|
|
871
|
+
**Last Updated:** 2026-02-21
|
|
660
872
|
**Angular Version:** 21
|