@flusys/ng-email 3.0.0-rc → 3.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/README.md +278 -484
- package/fesm2022/{flusys-ng-email-email-config-form.component-CC4bsd_p.mjs → flusys-ng-email-email-config-form.component-tg4pKP7G.mjs} +4 -4
- package/fesm2022/{flusys-ng-email-email-config-form.component-CC4bsd_p.mjs.map → flusys-ng-email-email-config-form.component-tg4pKP7G.mjs.map} +1 -1
- package/fesm2022/{flusys-ng-email-email-config-list.component-eW0pbwjL.mjs → flusys-ng-email-email-config-list.component-Dt9PY9xD.mjs} +4 -4
- package/fesm2022/{flusys-ng-email-email-config-list.component-eW0pbwjL.mjs.map → flusys-ng-email-email-config-list.component-Dt9PY9xD.mjs.map} +1 -1
- package/fesm2022/{flusys-ng-email-template-form.component-BFtOKk5a.mjs → flusys-ng-email-template-form.component-vlfHz6VE.mjs} +4 -4
- package/fesm2022/{flusys-ng-email-template-form.component-BFtOKk5a.mjs.map → flusys-ng-email-template-form.component-vlfHz6VE.mjs.map} +1 -1
- package/fesm2022/{flusys-ng-email-template-list.component-C6QUg8_5.mjs → flusys-ng-email-template-list.component-krrpsjDv.mjs} +4 -4
- package/fesm2022/{flusys-ng-email-template-list.component-C6QUg8_5.mjs.map → flusys-ng-email-template-list.component-krrpsjDv.mjs.map} +1 -1
- package/fesm2022/flusys-ng-email.mjs +29 -29
- package/fesm2022/flusys-ng-email.mjs.map +1 -1
- package/package.json +12 -12
- package/types/flusys-ng-email.d.ts +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Email Package Guide
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flusys/ng-email`
|
|
4
|
+
> **Version:** 3.0.1
|
|
4
5
|
> **Type:** Email management with configurations, templates, and sending
|
|
5
6
|
|
|
6
7
|
---
|
|
@@ -15,15 +16,17 @@
|
|
|
15
16
|
- **Test Sending** - Test configurations and templates with dynamic variables
|
|
16
17
|
- **Company Scoping** - Automatic company-scoped queries (when enabled)
|
|
17
18
|
- **Lazy Loading** - All page components are lazy-loaded via routes
|
|
19
|
+
- **Permission Guards** - Route-level access control using `permissionGuard`
|
|
20
|
+
- **Zoneless Ready** - All components use signals for zoneless change detection
|
|
18
21
|
- **Responsive Design** - Mobile-first with horizontal scroll tables
|
|
19
22
|
- **Dark Mode** - Full dark/light mode support
|
|
20
23
|
|
|
21
24
|
### Package Hierarchy
|
|
22
25
|
|
|
23
26
|
```
|
|
24
|
-
@flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME)
|
|
27
|
+
@flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME, getServiceUrl)
|
|
25
28
|
|
|
|
26
|
-
@flusys/ng-shared <- Shared utilities (ApiResourceService,
|
|
29
|
+
@flusys/ng-shared <- Shared utilities (ApiResourceService, HasPermissionDirective, PermissionValidatorService)
|
|
27
30
|
|
|
|
28
31
|
@flusys/ng-layout <- Layout (LAYOUT_AUTH_STATE for company context)
|
|
29
32
|
|
|
|
@@ -58,31 +61,30 @@ readonly currentCompanyName = computed(
|
|
|
58
61
|
ng-email/
|
|
59
62
|
├── enums/
|
|
60
63
|
│ ├── email-provider.enum.ts # EmailProviderEnum (smtp, sendgrid, mailgun)
|
|
61
|
-
│ ├── email-block-type.enum.ts # EmailBlockType (text, image, button,
|
|
64
|
+
│ ├── email-block-type.enum.ts # EmailBlockType (text, image, button, divider, html)
|
|
62
65
|
│ └── public-api.ts
|
|
63
66
|
├── 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
|
|
67
|
+
│ ├── email-config.interface.ts # IEmailConfig, ICreateEmailConfigDto, ISmtpAuth, ISmtpConfig, ISendGridConfig, IMailgunConfig
|
|
68
|
+
│ ├── email-template.interface.ts # IEmailTemplate, ICreateEmailTemplateDto, IUpdateEmailTemplateDto
|
|
69
|
+
│ ├── email-schema.interface.ts # IEmailSchema, IEmailSection, IEmailBlock, block content types
|
|
70
|
+
│ ├── email-send.interface.ts # ISendEmailDto, ISendTemplateEmailDto, ITestSendResult
|
|
68
71
|
│ └── public-api.ts
|
|
69
72
|
├── services/
|
|
70
73
|
│ ├── email-config-api.service.ts # Config CRUD + test sending
|
|
71
74
|
│ ├── email-template-api.service.ts # Template CRUD + getBySlug
|
|
72
|
-
│ ├── email-send.service.ts # Email sending operations
|
|
73
|
-
│
|
|
74
|
-
│ └── public-api.ts
|
|
75
|
+
│ ├── email-send.service.ts # Email sending operations (direct + template)
|
|
76
|
+
│ └── email-builder-state.service.ts # Template builder state management
|
|
75
77
|
├── pages/
|
|
76
78
|
│ ├── email-container/
|
|
77
|
-
│ │ └── email-container.component.ts # Main container with tabs
|
|
79
|
+
│ │ └── email-container.component.ts # Main container with permission-filtered tabs
|
|
78
80
|
│ ├── config/
|
|
79
|
-
│ │ ├── email-config-list.component.ts
|
|
80
|
-
│ │ └── email-config-form.component.ts
|
|
81
|
+
│ │ ├── email-config-list.component.ts # Config list with test dialog
|
|
82
|
+
│ │ └── email-config-form.component.ts # Config form with dynamic provider fields
|
|
81
83
|
│ └── template/
|
|
82
|
-
│ ├── template-list.component.ts
|
|
83
|
-
│ └── template-form.component.ts
|
|
84
|
+
│ ├── template-list.component.ts # Template list with test send dialog
|
|
85
|
+
│ └── template-form.component.ts # Template form with Angular Signal Forms
|
|
84
86
|
├── routes/
|
|
85
|
-
│ └── email.routes.ts
|
|
87
|
+
│ └── email.routes.ts # Routes with permission guards
|
|
86
88
|
└── public-api.ts
|
|
87
89
|
```
|
|
88
90
|
|
|
@@ -104,17 +106,17 @@ export const routes: Routes = [
|
|
|
104
106
|
];
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
### Route Structure
|
|
109
|
+
### Route Structure with Permission Guards
|
|
108
110
|
|
|
109
|
-
| Path | Component |
|
|
110
|
-
|
|
111
|
-
| `/email` | EmailContainerComponent |
|
|
112
|
-
| `/email/templates` | TemplateListComponent |
|
|
113
|
-
| `/email/templates/new` | TemplateFormComponent |
|
|
114
|
-
| `/email/templates/:id` | TemplateFormComponent |
|
|
115
|
-
| `/email/configs` | EmailConfigListComponent |
|
|
116
|
-
| `/email/configs/new` | EmailConfigFormComponent |
|
|
117
|
-
| `/email/configs/:id` | EmailConfigFormComponent |
|
|
111
|
+
| Path | Component | Permission Guard |
|
|
112
|
+
|------|-----------|------------------|
|
|
113
|
+
| `/email` | EmailContainerComponent | - |
|
|
114
|
+
| `/email/templates` | TemplateListComponent | `EMAIL_TEMPLATE_PERMISSIONS.READ` |
|
|
115
|
+
| `/email/templates/new` | TemplateFormComponent | `EMAIL_TEMPLATE_PERMISSIONS.CREATE` |
|
|
116
|
+
| `/email/templates/:id` | TemplateFormComponent | `EMAIL_TEMPLATE_PERMISSIONS.UPDATE` |
|
|
117
|
+
| `/email/configs` | EmailConfigListComponent | `EMAIL_CONFIG_PERMISSIONS.READ` |
|
|
118
|
+
| `/email/configs/new` | EmailConfigFormComponent | `EMAIL_CONFIG_PERMISSIONS.CREATE` |
|
|
119
|
+
| `/email/configs/:id` | EmailConfigFormComponent | `EMAIL_CONFIG_PERMISSIONS.UPDATE` |
|
|
118
120
|
|
|
119
121
|
---
|
|
120
122
|
|
|
@@ -149,60 +151,68 @@ export enum EmailBlockType {
|
|
|
149
151
|
### Email Configuration
|
|
150
152
|
|
|
151
153
|
```typescript
|
|
154
|
+
/** SMTP auth credentials (nodemailer style) */
|
|
155
|
+
interface ISmtpAuth {
|
|
156
|
+
user: string;
|
|
157
|
+
pass: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** SMTP provider configuration (nodemailer style) */
|
|
161
|
+
interface ISmtpConfig {
|
|
162
|
+
host: string;
|
|
163
|
+
port: number;
|
|
164
|
+
secure?: boolean;
|
|
165
|
+
auth: ISmtpAuth;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** SendGrid provider configuration */
|
|
169
|
+
interface ISendGridConfig {
|
|
170
|
+
apiKey: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Mailgun provider configuration */
|
|
174
|
+
interface IMailgunConfig {
|
|
175
|
+
apiKey: string;
|
|
176
|
+
domain: string;
|
|
177
|
+
region?: 'us' | 'eu';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Union type for all provider configurations */
|
|
181
|
+
type EmailProviderConfig = ISmtpConfig | ISendGridConfig | IMailgunConfig;
|
|
182
|
+
|
|
183
|
+
/** Email configuration interface */
|
|
152
184
|
interface IEmailConfig extends IBaseEntity {
|
|
153
185
|
name: string;
|
|
154
186
|
provider: EmailProviderEnum;
|
|
155
|
-
config:
|
|
187
|
+
config: EmailProviderConfig;
|
|
156
188
|
fromEmail: string | null;
|
|
157
189
|
fromName: string | null;
|
|
158
190
|
isActive: boolean;
|
|
159
|
-
isDefault: boolean;
|
|
191
|
+
isDefault: boolean;
|
|
160
192
|
companyId?: string | null;
|
|
161
193
|
}
|
|
162
194
|
|
|
195
|
+
/** Create email config DTO */
|
|
163
196
|
interface ICreateEmailConfigDto {
|
|
164
197
|
name: string;
|
|
165
198
|
provider: EmailProviderEnum;
|
|
166
|
-
config:
|
|
199
|
+
config: EmailProviderConfig;
|
|
167
200
|
fromEmail?: string;
|
|
168
201
|
fromName?: string;
|
|
169
202
|
isActive?: boolean;
|
|
170
203
|
isDefault?: boolean;
|
|
171
204
|
}
|
|
172
205
|
|
|
206
|
+
/** Update email config DTO */
|
|
173
207
|
interface IUpdateEmailConfigDto extends Partial<ICreateEmailConfigDto> {
|
|
174
208
|
id: string;
|
|
175
209
|
}
|
|
176
210
|
```
|
|
177
211
|
|
|
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
212
|
### Email Template
|
|
204
213
|
|
|
205
214
|
```typescript
|
|
215
|
+
/** Email template interface */
|
|
206
216
|
interface IEmailTemplate extends IBaseEntity {
|
|
207
217
|
name: string;
|
|
208
218
|
slug: string;
|
|
@@ -213,11 +223,12 @@ interface IEmailTemplate extends IBaseEntity {
|
|
|
213
223
|
textContent: string | null;
|
|
214
224
|
schemaVersion: number;
|
|
215
225
|
isActive: boolean;
|
|
216
|
-
isHtml: boolean;
|
|
226
|
+
isHtml: boolean;
|
|
217
227
|
metadata: Record<string, unknown> | null;
|
|
218
228
|
companyId?: string | null;
|
|
219
229
|
}
|
|
220
230
|
|
|
231
|
+
/** Create email template DTO */
|
|
221
232
|
interface ICreateEmailTemplateDto {
|
|
222
233
|
name: string;
|
|
223
234
|
slug: string;
|
|
@@ -231,6 +242,7 @@ interface ICreateEmailTemplateDto {
|
|
|
231
242
|
metadata?: Record<string, unknown>;
|
|
232
243
|
}
|
|
233
244
|
|
|
245
|
+
/** Update email template DTO */
|
|
234
246
|
interface IUpdateEmailTemplateDto extends Partial<ICreateEmailTemplateDto> {
|
|
235
247
|
id: string;
|
|
236
248
|
}
|
|
@@ -238,55 +250,70 @@ interface IUpdateEmailTemplateDto extends Partial<ICreateEmailTemplateDto> {
|
|
|
238
250
|
|
|
239
251
|
### Email Schema
|
|
240
252
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
name: string;
|
|
246
|
-
subject: string;
|
|
247
|
-
preheader?: string;
|
|
248
|
-
sections: IEmailSection[];
|
|
249
|
-
variables?: IEmailTemplateVariable[];
|
|
250
|
-
settings?: IEmailSettings;
|
|
251
|
-
}
|
|
253
|
+
**Type Aliases:**
|
|
254
|
+
- `EmailVariableType` = `'string' | 'number' | 'boolean' | 'date'`
|
|
255
|
+
- `EmailSectionType` = `'header' | 'body' | 'footer'`
|
|
256
|
+
- `EmailBlockContent` = `ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent`
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
id: string;
|
|
255
|
-
type: 'header' | 'body' | 'footer';
|
|
256
|
-
name: string;
|
|
257
|
-
blocks: IEmailBlock[];
|
|
258
|
-
order: number;
|
|
259
|
-
style?: IEmailSectionStyle;
|
|
260
|
-
}
|
|
258
|
+
**Block Content Types:**
|
|
261
259
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
| Interface | Fields |
|
|
261
|
+
|-----------|--------|
|
|
262
|
+
| `ITextBlockContent` | `text`, `html?` |
|
|
263
|
+
| `IImageBlockContent` | `src`, `alt?`, `link?`, `width?`, `height?` |
|
|
264
|
+
| `IButtonBlockContent` | `text`, `link`, `backgroundColor?`, `textColor?`, `borderRadius?` |
|
|
265
|
+
| `IDividerBlockContent` | `height?`, `color?`, `style?` (solid\|dashed\|dotted) |
|
|
266
|
+
| `IHtmlBlockContent` | `html` |
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
**Schema Interfaces:**
|
|
269
|
+
|
|
270
|
+
| Interface | Fields |
|
|
271
|
+
|-----------|--------|
|
|
272
|
+
| `IEmailTemplateVariable` | `name`, `type`, `required?`, `defaultValue?`, `description?` |
|
|
273
|
+
| `IEmailSectionStyle` | `backgroundColor?`, `padding?`, `maxWidth?` |
|
|
274
|
+
| `IEmailBlockStyle` | `textAlign?`, `padding?`, `margin?`, `backgroundColor?` |
|
|
275
|
+
| `IEmailBlock` | `id`, `type`, `order`, `content`, `style?` |
|
|
276
|
+
| `IEmailSection` | `id`, `type`, `name`, `blocks[]`, `order`, `style?` |
|
|
277
|
+
| `IEmailSettings` | `maxWidth?`, `backgroundColor?`, `fontFamily?`, `baseTextColor?` |
|
|
278
|
+
| `IEmailSchema` | `id`, `version`, `name`, `subject`, `preheader?`, `sections[]`, `variables?`, `settings?` |
|
|
279
|
+
|
|
280
|
+
### Schema Helper Functions
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
/** Default email settings */
|
|
284
|
+
const DEFAULT_EMAIL_SETTINGS: IEmailSettings = {
|
|
285
|
+
maxWidth: '600px',
|
|
286
|
+
backgroundColor: '#f5f5f5',
|
|
287
|
+
fontFamily: 'Arial, sans-serif',
|
|
288
|
+
baseTextColor: '#333333',
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/** Create a new email schema with default sections */
|
|
292
|
+
function createEmailSchema(partial: Partial<IEmailSchema> = {}): IEmailSchema {
|
|
293
|
+
return {
|
|
294
|
+
id: partial.id ?? crypto.randomUUID(),
|
|
295
|
+
version: partial.version ?? '1.0.0',
|
|
296
|
+
name: partial.name ?? 'Untitled Template',
|
|
297
|
+
subject: partial.subject ?? '',
|
|
298
|
+
preheader: partial.preheader,
|
|
299
|
+
sections: partial.sections ?? createDefaultSections(),
|
|
300
|
+
variables: partial.variables ?? [],
|
|
301
|
+
settings: { ...DEFAULT_EMAIL_SETTINGS, ...partial.settings },
|
|
302
|
+
};
|
|
276
303
|
}
|
|
277
304
|
```
|
|
278
305
|
|
|
279
306
|
### Email Sending
|
|
280
307
|
|
|
281
308
|
```typescript
|
|
282
|
-
|
|
309
|
+
/** Test send result */
|
|
283
310
|
interface ITestSendResult {
|
|
284
311
|
success: boolean;
|
|
285
312
|
messageId?: string;
|
|
286
313
|
error?: string;
|
|
287
314
|
}
|
|
288
315
|
|
|
289
|
-
|
|
316
|
+
/** Send email request DTO (direct) */
|
|
290
317
|
interface ISendEmailDto {
|
|
291
318
|
to: string | string[];
|
|
292
319
|
cc?: string | string[];
|
|
@@ -300,7 +327,7 @@ interface ISendEmailDto {
|
|
|
300
327
|
emailConfigId?: string;
|
|
301
328
|
}
|
|
302
329
|
|
|
303
|
-
|
|
330
|
+
/** Send template email request DTO */
|
|
304
331
|
interface ISendTemplateEmailDto {
|
|
305
332
|
templateId?: string;
|
|
306
333
|
templateSlug?: string;
|
|
@@ -319,46 +346,53 @@ interface ISendTemplateEmailDto {
|
|
|
319
346
|
|
|
320
347
|
## Services
|
|
321
348
|
|
|
322
|
-
All services are `providedIn: 'root'`. Company scoping is automatic when the company feature is enabled.
|
|
323
|
-
|
|
324
349
|
### EmailConfigApiService
|
|
325
350
|
|
|
326
|
-
Email configuration CRUD. Extends `ApiResourceService<IUpdateEmailConfigDto, IEmailConfig>`.
|
|
351
|
+
Email configuration CRUD. Extends `ApiResourceService<ICreateEmailConfigDto | IUpdateEmailConfigDto, IEmailConfig>`.
|
|
327
352
|
|
|
328
353
|
```typescript
|
|
329
354
|
@Injectable({ providedIn: 'root' })
|
|
330
355
|
export class EmailConfigApiService extends ApiResourceService<
|
|
331
|
-
IUpdateEmailConfigDto,
|
|
356
|
+
ICreateEmailConfigDto | IUpdateEmailConfigDto,
|
|
332
357
|
IEmailConfig
|
|
333
358
|
> {
|
|
359
|
+
private readonly appConfig = inject<IAppConfig>(APP_CONFIG);
|
|
360
|
+
|
|
334
361
|
constructor() {
|
|
335
|
-
|
|
336
|
-
super('email
|
|
362
|
+
// Resource name, HttpClient, service prefix
|
|
363
|
+
super('email-config', inject(HttpClient), 'email');
|
|
337
364
|
}
|
|
338
365
|
|
|
339
366
|
/** Send test email to verify configuration */
|
|
340
367
|
sendTest(configId: string, recipient: string): Observable<ISingleResponse<ITestSendResult>> {
|
|
368
|
+
const emailBaseUrl = getServiceUrl(this.appConfig, 'email');
|
|
341
369
|
return this.http.post<ISingleResponse<ITestSendResult>>(
|
|
342
|
-
`${
|
|
370
|
+
`${emailBaseUrl}/send/test`,
|
|
343
371
|
{ emailConfigId: configId, recipient },
|
|
344
372
|
);
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
375
|
```
|
|
348
376
|
|
|
377
|
+
**Inherited Methods from ApiResourceService:**
|
|
378
|
+
- `getAll(search, queryParams)` - List with pagination
|
|
379
|
+
- `findByIdAsync(id)` - Get single by ID
|
|
380
|
+
- `insertAsync(dto)` - Create new
|
|
381
|
+
- `updateAsync(dto)` - Update existing
|
|
382
|
+
- `deleteAsync({ id, type })` - Delete
|
|
383
|
+
|
|
349
384
|
### EmailTemplateApiService
|
|
350
385
|
|
|
351
|
-
Email template CRUD. Extends `ApiResourceService<IUpdateEmailTemplateDto, IEmailTemplate>`.
|
|
386
|
+
Email template CRUD. Extends `ApiResourceService<ICreateEmailTemplateDto | IUpdateEmailTemplateDto, IEmailTemplate>`.
|
|
352
387
|
|
|
353
388
|
```typescript
|
|
354
389
|
@Injectable({ providedIn: 'root' })
|
|
355
390
|
export class EmailTemplateApiService extends ApiResourceService<
|
|
356
|
-
IUpdateEmailTemplateDto,
|
|
391
|
+
ICreateEmailTemplateDto | IUpdateEmailTemplateDto,
|
|
357
392
|
IEmailTemplate
|
|
358
393
|
> {
|
|
359
394
|
constructor() {
|
|
360
|
-
|
|
361
|
-
super('email/email-template', http);
|
|
395
|
+
super('email-template', inject(HttpClient), 'email');
|
|
362
396
|
}
|
|
363
397
|
|
|
364
398
|
/** Get template by slug (POST-only RPC pattern) */
|
|
@@ -373,268 +407,157 @@ export class EmailTemplateApiService extends ApiResourceService<
|
|
|
373
407
|
|
|
374
408
|
### EmailSendService
|
|
375
409
|
|
|
376
|
-
Handles email sending operations.
|
|
410
|
+
Handles email sending operations. Uses `getServiceUrl` from `@flusys/ng-core` for dynamic endpoint resolution.
|
|
377
411
|
|
|
378
412
|
```typescript
|
|
379
413
|
@Injectable({ providedIn: 'root' })
|
|
380
414
|
export class EmailSendService {
|
|
381
415
|
private readonly http = inject(HttpClient);
|
|
382
|
-
private readonly
|
|
416
|
+
private readonly appConfig = inject<IAppConfig>(APP_CONFIG);
|
|
417
|
+
private readonly baseUrl = `${getServiceUrl(this.appConfig, 'email')}/send`;
|
|
383
418
|
|
|
384
419
|
/** Send email directly (without template) */
|
|
385
|
-
sendDirect(dto: ISendEmailDto): Observable<ISingleResponse<ITestSendResult
|
|
420
|
+
sendDirect(dto: ISendEmailDto): Observable<ISingleResponse<ITestSendResult>> {
|
|
421
|
+
return this.http.post<ISingleResponse<ITestSendResult>>(
|
|
422
|
+
`${this.baseUrl}/direct`,
|
|
423
|
+
dto,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
386
426
|
|
|
387
427
|
/** Send email using template */
|
|
388
|
-
sendTemplate(dto: ISendTemplateEmailDto): Observable<ISingleResponse<ITestSendResult
|
|
428
|
+
sendTemplate(dto: ISendTemplateEmailDto): Observable<ISingleResponse<ITestSendResult>> {
|
|
429
|
+
return this.http.post<ISingleResponse<ITestSendResult>>(
|
|
430
|
+
`${this.baseUrl}/template`,
|
|
431
|
+
dto,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
389
434
|
}
|
|
390
435
|
```
|
|
391
436
|
|
|
392
437
|
### EmailBuilderStateService
|
|
393
438
|
|
|
394
|
-
Manages email builder UI state.
|
|
439
|
+
Manages email builder UI state. **Provided at component level**, not root.
|
|
395
440
|
|
|
396
441
|
```typescript
|
|
397
|
-
@Injectable()
|
|
442
|
+
@Injectable() // NOT providedIn: 'root'
|
|
398
443
|
export class EmailBuilderStateService {
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
readonly
|
|
405
|
-
readonly
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
readonly
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
// =========================================
|
|
445
|
+
// Private Writable Signals
|
|
446
|
+
// =========================================
|
|
447
|
+
private readonly _schema = signal<IEmailSchema>(createEmailSchema());
|
|
448
|
+
private readonly _isDirty = signal(false);
|
|
449
|
+
private readonly _selectedSectionId = signal<string | null>(null);
|
|
450
|
+
private readonly _selectedBlockId = signal<string | null>(null);
|
|
451
|
+
private readonly _viewMode = signal<'visual' | 'html'>('visual');
|
|
452
|
+
|
|
453
|
+
// =========================================
|
|
454
|
+
// Public Readonly Signals
|
|
455
|
+
// =========================================
|
|
456
|
+
readonly schema = this._schema.asReadonly();
|
|
457
|
+
readonly isDirty = this._isDirty.asReadonly();
|
|
458
|
+
readonly selectedSectionId = this._selectedSectionId.asReadonly();
|
|
459
|
+
readonly selectedBlockId = this._selectedBlockId.asReadonly();
|
|
460
|
+
readonly viewMode = this._viewMode.asReadonly();
|
|
461
|
+
|
|
462
|
+
// Computed Signals
|
|
463
|
+
readonly sections = computed(() => this._schema().sections);
|
|
464
|
+
readonly templateName = computed(() => this._schema().name);
|
|
465
|
+
readonly subject = computed(() => this._schema().subject);
|
|
466
|
+
readonly settings = computed(() => this._schema().settings ?? DEFAULT_EMAIL_SETTINGS);
|
|
467
|
+
readonly variables = computed(() => this._schema().variables ?? []);
|
|
468
|
+
readonly headerSection = computed(() => this.sections().find((s) => s.type === 'header') ?? null);
|
|
469
|
+
readonly bodySection = computed(() => this.sections().find((s) => s.type === 'body') ?? null);
|
|
470
|
+
readonly footerSection = computed(() => this.sections().find((s) => s.type === 'footer') ?? null);
|
|
471
|
+
readonly selectedSection = computed(() => /* finds section by _selectedSectionId */);
|
|
472
|
+
readonly selectedBlock = computed(() => /* finds block by _selectedBlockId across sections */);
|
|
473
|
+
readonly allBlocks = computed(() => this.sections().flatMap((s) => s.blocks));
|
|
474
|
+
|
|
475
|
+
// Methods documented in table below
|
|
419
476
|
}
|
|
420
477
|
```
|
|
421
478
|
|
|
479
|
+
**Methods:**
|
|
480
|
+
|
|
481
|
+
| Category | Method | Description |
|
|
482
|
+
|----------|--------|-------------|
|
|
483
|
+
| Schema | `loadSchema(schema)` | Load existing schema into builder |
|
|
484
|
+
| Schema | `createNewSchema(name?)` | Create new empty schema |
|
|
485
|
+
| Schema | `updateSchemaMeta(updates)` | Update name, subject, preheader, settings, variables |
|
|
486
|
+
| Schema | `markAsSaved()` | Clear dirty flag |
|
|
487
|
+
| Schema | `setViewMode(mode)` | Set 'visual' or 'html' mode |
|
|
488
|
+
| Section | `updateSection(sectionId, updates)` | Update section properties |
|
|
489
|
+
| Block | `addBlock(sectionId, blockType, block?)` | Add block, returns block ID |
|
|
490
|
+
| Block | `updateBlock(sectionId, blockId, updates)` | Update block properties |
|
|
491
|
+
| Block | `deleteBlock(sectionId, blockId)` | Delete block from section |
|
|
492
|
+
| Block | `reorderBlocks(sectionId, from, to)` | Reorder blocks within section |
|
|
493
|
+
| Block | `duplicateBlock(sectionId, blockId)` | Duplicate block, returns new ID |
|
|
494
|
+
| Selection | `selectSection(sectionId)` | Select section (clears block selection) |
|
|
495
|
+
| Selection | `selectBlock(blockId)` | Select block (auto-selects parent section) |
|
|
496
|
+
| Selection | `clearSelection()` | Clear all selection |
|
|
497
|
+
|
|
498
|
+
**Default Block Content:**
|
|
499
|
+
|
|
500
|
+
| Block Type | Default Content |
|
|
501
|
+
|------------|-----------------|
|
|
502
|
+
| TEXT | `{ text: 'Enter your text here...', html: '<p>Enter your text here...</p>' }` |
|
|
503
|
+
| IMAGE | `{ src: '', alt: 'Image' }` |
|
|
504
|
+
| BUTTON | `{ text: 'Click Here', link: 'https://', backgroundColor: '#007bff', textColor: '#ffffff', borderRadius: '4px' }` |
|
|
505
|
+
| DIVIDER | `{ height: '1px', color: '#cccccc', style: 'solid' }` |
|
|
506
|
+
| HTML | `{ html: '<!-- Custom HTML -->' }` |
|
|
507
|
+
|
|
422
508
|
---
|
|
423
509
|
|
|
424
510
|
## Components
|
|
425
511
|
|
|
426
512
|
All components are standalone, use Angular 21 signals, OnPush change detection, and lazy-load via routes.
|
|
427
513
|
|
|
428
|
-
###
|
|
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
|
|
514
|
+
### Component Overview
|
|
436
515
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
516
|
+
| Component | Purpose | Key Features |
|
|
517
|
+
|-----------|---------|--------------|
|
|
518
|
+
| `EmailContainerComponent` | Tab container | Permission-filtered tabs, horizontal scroll, router outlet |
|
|
519
|
+
| `EmailConfigListComponent` | Config list | Lazy pagination, provider metadata badges, test dialog |
|
|
520
|
+
| `EmailConfigFormComponent` | Config form | Dynamic provider fields, type guards, signal-based form |
|
|
521
|
+
| `TemplateListComponent` | Template list | Lazy pagination, variable extraction, test send dialog |
|
|
522
|
+
| `TemplateFormComponent` | Template form | Angular Signal Forms, HTML/Text toggle, live preview |
|
|
441
523
|
|
|
442
|
-
|
|
443
|
-
@for (tab of tabs; track tab.id) {
|
|
444
|
-
<a [routerLink]="tab.route" routerLinkActive="tab-active">...</a>
|
|
445
|
-
}
|
|
446
|
-
</div>
|
|
524
|
+
### EmailContainerComponent
|
|
447
525
|
|
|
448
|
-
|
|
449
|
-
</div>
|
|
450
|
-
```
|
|
526
|
+
Filters tabs using `PermissionValidatorService.hasPermission()`. Tabs: Templates (`EMAIL_TEMPLATE_PERMISSIONS.READ`), Configurations (`EMAIL_CONFIG_PERMISSIONS.READ`).
|
|
451
527
|
|
|
452
528
|
### EmailConfigListComponent
|
|
453
529
|
|
|
454
|
-
|
|
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
|
-
}
|
|
530
|
+
**Provider Metadata:** SMTP (`pi pi-server`, info), SendGrid (`pi pi-cloud`, success), Mailgun (`pi pi-cloud`, warn). Default icon: `pi pi-envelope`.
|
|
479
531
|
|
|
480
|
-
|
|
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
|
-
```
|
|
532
|
+
**Signals:** `isLoading`, `configs`, `totalRecords`, `pageSize`, `first`, `showTestDialog`, `selectedConfig`, `isSendingTest`, `testRecipient`.
|
|
498
533
|
|
|
499
534
|
### EmailConfigFormComponent
|
|
500
535
|
|
|
501
|
-
|
|
536
|
+
**Type Guards:** `isSmtpConfig` (`'host' in config && 'auth' in config`), `isSendGridConfig` (`'apiKey' in config && !('domain' in config)`), `isMailgunConfig` (`'apiKey' in config && 'domain' in config`).
|
|
502
537
|
|
|
503
|
-
**
|
|
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
|
|
538
|
+
**Form Pattern:** Private signal with flattened provider fields, public getter, typed `updateFormModel<K>()` method.
|
|
512
539
|
|
|
513
540
|
### TemplateListComponent
|
|
514
541
|
|
|
515
|
-
|
|
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
|
-
```
|
|
542
|
+
**Variable Extraction:** Computed signal extracts `{{variableName}}` patterns via `matchAll(/\{\{(\w+)\}\}/g)`, deduplicates with Set, returns sorted array.
|
|
551
543
|
|
|
552
544
|
### TemplateFormComponent
|
|
553
545
|
|
|
554
|
-
|
|
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
|
|
546
|
+
Uses Angular Signal Forms (`form`, `FormField`, `required` from `@angular/forms/signals`). Integrates with `EmailBuilderStateService` for schema management.
|
|
567
547
|
|
|
568
|
-
**Preview
|
|
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
|
-
```
|
|
548
|
+
**Content Conversion:** Auto-syncs when switching modes. `textToHtml()` wraps in `<p>`, `htmlToPlainText()` uses `DOMParser` for XSS-safe parsing. Preview renders in sandboxed iframe.
|
|
586
549
|
|
|
587
550
|
---
|
|
588
551
|
|
|
589
552
|
## Responsive Design Patterns
|
|
590
553
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
```
|
|
554
|
+
| Pattern | Implementation |
|
|
555
|
+
|---------|----------------|
|
|
556
|
+
| **Tables** | `overflow-x-auto -mx-4 sm:mx-0` wrapper, `[lazy]="true"`, `[paginator]="totalRecords() > 0"`, hidden columns: `hidden md:table-cell` |
|
|
557
|
+
| **Forms** | `grid grid-cols-1 md:grid-cols-2 gap-4`, full-width: `md:col-span-2` |
|
|
558
|
+
| **Headers** | `flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3` |
|
|
559
|
+
| **Buttons** | `styleClass="w-full sm:w-auto"` |
|
|
560
|
+
| **Dialogs** | `{ width: '95vw', maxWidth: '500px' }`, `[breakpoints]="{ '575px': '95vw' }"` |
|
|
638
561
|
|
|
639
562
|
---
|
|
640
563
|
|
|
@@ -649,90 +572,40 @@ All components use PrimeNG/Tailwind dark mode classes:
|
|
|
649
572
|
| Backgrounds | `bg-surface-0 dark:bg-surface-900`, `bg-surface-100 dark:bg-surface-700` |
|
|
650
573
|
| Code blocks | `bg-surface-100 dark:bg-surface-700` |
|
|
651
574
|
| Hover states | `hover:bg-surface-hover` |
|
|
575
|
+
| Surface ground | `bg-surface-ground` |
|
|
652
576
|
|
|
653
577
|
---
|
|
654
578
|
|
|
655
579
|
## Usage Examples
|
|
656
580
|
|
|
657
|
-
### Send Direct Email
|
|
658
|
-
|
|
659
581
|
```typescript
|
|
660
|
-
import { EmailSendService } from '@flusys/ng-email';
|
|
582
|
+
import { EmailSendService, EmailConfigApiService, EmailTemplateApiService, EmailBuilderStateService, EmailBlockType } from '@flusys/ng-email';
|
|
661
583
|
|
|
584
|
+
// Inject services
|
|
662
585
|
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
586
|
private readonly configService = inject(EmailConfigApiService);
|
|
587
|
+
private readonly templateService = inject(EmailTemplateApiService);
|
|
710
588
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return response.data?.success ?? false;
|
|
717
|
-
}
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
### Get Template by Slug
|
|
589
|
+
// Send direct email
|
|
590
|
+
const res1 = await firstValueFrom(this.emailSendService.sendDirect({
|
|
591
|
+
to: 'user@example.com', subject: 'Hello!', html: '<h1>Hello</h1>', emailConfigId: 'config-id'
|
|
592
|
+
}));
|
|
721
593
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
594
|
+
// Send template email (prefer templateSlug over templateId)
|
|
595
|
+
const res2 = await firstValueFrom(this.emailSendService.sendTemplate({
|
|
596
|
+
templateSlug: 'welcome-email', to: 'user@example.com', emailConfigId: 'config-id',
|
|
597
|
+
variables: { userName: 'John', appName: 'My App' }
|
|
598
|
+
}));
|
|
726
599
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
this.templateService.getBySlug(slug)
|
|
730
|
-
);
|
|
600
|
+
// Test configuration
|
|
601
|
+
const success = (await firstValueFrom(this.configService.sendTest(configId, recipient))).data?.success;
|
|
731
602
|
|
|
732
|
-
|
|
733
|
-
|
|
603
|
+
// Get template by slug
|
|
604
|
+
const template = (await firstValueFrom(this.templateService.getBySlug('welcome-email'))).data;
|
|
734
605
|
```
|
|
735
606
|
|
|
607
|
+
**Using EmailBuilderStateService:** Provide at component level, access signals (`headerSection`, `bodySection`), methods: `loadSchema()`, `addBlock(sectionId, EmailBlockType.TEXT)`, `updateBlock(sectionId, blockId, { content })`.
|
|
608
|
+
|
|
736
609
|
---
|
|
737
610
|
|
|
738
611
|
## Backend Integration
|
|
@@ -760,113 +633,34 @@ All endpoints use **POST-only RPC** style (not REST).
|
|
|
760
633
|
|
|
761
634
|
## Best Practices
|
|
762
635
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
```
|
|
636
|
+
| Practice | Description |
|
|
637
|
+
|----------|-------------|
|
|
638
|
+
| **Use Template Slugs** | Use `templateSlug: 'welcome-email'` instead of hardcoded UUIDs |
|
|
639
|
+
| **Provide Both Content Types** | Include both `htmlContent` and `textContent` for email client compatibility |
|
|
640
|
+
| **Test Before Production** | Always use test send feature to verify configurations |
|
|
641
|
+
| **Meaningful Variable Names** | Use `{{userName}}` not `{{u}}` - clear, descriptive names |
|
|
642
|
+
| **Handle Failures** | Check `response.data?.success`, show toast on error (HTTP errors handled by global interceptor) |
|
|
643
|
+
| **Signal-Based Forms** | See [EmailConfigFormComponent](#emailconfigformcomponent) for zoneless pattern |
|
|
644
|
+
| **Component-Level StateService** | `providers: [EmailBuilderStateService]` - never `providedIn: 'root'` |
|
|
835
645
|
|
|
836
646
|
---
|
|
837
647
|
|
|
838
648
|
## Public API Exports
|
|
839
649
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
```
|
|
650
|
+
| Category | Exports |
|
|
651
|
+
|----------|---------|
|
|
652
|
+
| **Enums** | `EmailProviderEnum`, `EmailBlockType` |
|
|
653
|
+
| **Config Interfaces** | `IEmailConfig`, `ICreateEmailConfigDto`, `IUpdateEmailConfigDto`, `ISmtpAuth`, `ISmtpConfig`, `ISendGridConfig`, `IMailgunConfig`, `EmailProviderConfig` |
|
|
654
|
+
| **Template Interfaces** | `IEmailTemplate`, `ICreateEmailTemplateDto`, `IUpdateEmailTemplateDto` |
|
|
655
|
+
| **Schema Interfaces** | `IEmailSchema`, `IEmailSection`, `IEmailBlock`, `IEmailTemplateVariable`, `IEmailSettings`, `IEmailSectionStyle`, `IEmailBlockStyle`, `ITextBlockContent`, `IImageBlockContent`, `IButtonBlockContent`, `IDividerBlockContent`, `IHtmlBlockContent`, `EmailBlockContent`, `EmailVariableType`, `EmailSectionType` |
|
|
656
|
+
| **Send Interfaces** | `ISendEmailDto`, `ISendTemplateEmailDto`, `ITestSendResult` |
|
|
657
|
+
| **Constants** | `DEFAULT_EMAIL_SETTINGS`, `createEmailSchema` |
|
|
658
|
+
| **Services** | `EmailConfigApiService`, `EmailTemplateApiService`, `EmailSendService`, `EmailBuilderStateService` |
|
|
659
|
+
| **Components** | `EmailContainerComponent` |
|
|
660
|
+
| **Routes** | `EMAIL_ROUTES` |
|
|
868
661
|
|
|
869
662
|
---
|
|
870
663
|
|
|
871
|
-
**Last Updated:** 2026-02-
|
|
664
|
+
**Last Updated:** 2026-02-25
|
|
665
|
+
**Version:** 3.0.1
|
|
872
666
|
**Angular Version:** 21
|