@flusys/ng-email 4.0.1 → 4.1.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.
Files changed (2) hide show
  1. package/README.md +277 -521
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,98 +1,92 @@
1
- # Email Package Guide
1
+ # @flusys/ng-email
2
2
 
3
- > **Package:** `@flusys/ng-email`
4
- > **Version:** 4.0.1
5
- > **Type:** Email management with configurations, templates, and sending
3
+ > Email management UI library for the FLUSYS Angular platform — provider configuration, visual template builder, and programmatic email sending.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/ng-email.svg)](https://www.npmjs.com/package/@flusys/ng-email)
6
+ [![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
9
 
7
10
  ---
8
11
 
9
- ## Overview
12
+ ## Table of Contents
13
+
14
+ - [Overview](#overview)
15
+ - [Features](#features)
16
+ - [Compatibility](#compatibility)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [Email Schema](#email-schema)
20
+ - [IEmailSchema](#iemailschema)
21
+ - [Block Types](#block-types)
22
+ - [Services](#services)
23
+ - [EmailProviderApiService](#emailproviderapiservice)
24
+ - [EmailTemplateApiService](#emailtemplateapiservice)
25
+ - [EmailSendService](#emailsendservice)
26
+ - [EmailBuilderStateService](#emailbuilderstateservice)
27
+ - [Components](#components)
28
+ - [EmailBuilderComponent](#emailbuildercomponent)
29
+ - [EmailPreviewComponent](#emailpreviewcomponent)
30
+ - [Email Provider Enum](#email-provider-enum)
31
+ - [API Endpoints](#api-endpoints)
32
+ - [Configuration Reference](#configuration-reference)
33
+ - [Troubleshooting](#troubleshooting)
34
+ - [License](#license)
10
35
 
11
- `@flusys/ng-email` provides complete email management:
36
+ ---
12
37
 
13
- - **Email Configurations** - CRUD for SMTP, SendGrid, Mailgun providers
14
- - **Email Templates** - Visual template management with variable interpolation
15
- - **HTML/Plain Text** - Toggle between content types with live preview
16
- - **Test Sending** - Test configurations and templates with dynamic variables
17
- - **Company Scoping** - Automatic company-scoped queries (when enabled)
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
21
- - **Responsive Design** - Mobile-first with horizontal scroll tables
22
- - **Dark Mode** - Full dark/light mode support
38
+ ## Overview
23
39
 
24
- ### Package Hierarchy
40
+ `@flusys/ng-email` provides a complete email management UI for FLUSYS applications. Administrators can configure SMTP/SendGrid/Mailgun providers, design reusable email templates via a block-based visual builder, and developers can send emails programmatically using `EmailSendService`.
25
41
 
26
- ```
27
- @flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME, getServiceUrl)
28
- |
29
- @flusys/ng-shared <- Shared utilities (ApiResourceService, HasPermissionDirective, PermissionValidatorService)
30
- |
31
- @flusys/ng-layout <- Layout (LAYOUT_AUTH_STATE for company context)
32
- |
33
- @flusys/ng-email <- Email management (THIS PACKAGE)
34
- ```
42
+ Templates are stored as `IEmailSchema` JSON in the backend (`nestjs-email`) and rendered server-side with `{{variable}}` interpolation.
35
43
 
36
- ### Company Context
44
+ ---
37
45
 
38
- 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.
46
+ ## Features
39
47
 
40
- ```typescript
41
- import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
42
- import { APP_CONFIG, DEFAULT_APP_NAME } from '@flusys/ng-core';
48
+ - ✅ Email provider configuration UI (SMTP, SendGrid, Mailgun)
49
+ - Visual block-based email template builder
50
+ - ✅ `{{variable}}` interpolation with XSS protection
51
+ - ✅ Email template preview (desktop + mobile viewport)
52
+ - ✅ `EmailSendService` for programmatic sending from any feature
53
+ - ✅ Template versioning (draft / published)
54
+ - ✅ File/image insertion via `ng-storage` integration
43
55
 
44
- // Optional injection - works with or without company feature
45
- private readonly companyContext = inject(LAYOUT_AUTH_STATE, { optional: true });
56
+ ---
46
57
 
47
- readonly showCompanyInfo = computed(
48
- () => this.appConfig.enableCompanyFeature && !!this.companyContext,
49
- );
58
+ ## Compatibility
50
59
 
51
- readonly currentCompanyName = computed(
52
- () => this.companyContext?.currentCompanyInfo()?.name ?? DEFAULT_APP_NAME,
53
- );
54
- ```
60
+ | Package | Version |
61
+ |---------|---------|
62
+ | Angular | 21+ |
63
+ | @flusys/ng-core | 4.x |
64
+ | @flusys/ng-shared | 4.x |
55
65
 
56
66
  ---
57
67
 
58
- ## Package Architecture
68
+ ## Installation
59
69
 
60
- ```
61
- ng-email/
62
- ├── enums/
63
- │ ├── email-provider.enum.ts # EmailProviderEnum (smtp, sendgrid, mailgun)
64
- │ ├── email-block-type.enum.ts # EmailBlockType (text, image, button, divider, html)
65
- │ └── public-api.ts
66
- ├── interfaces/
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
71
- │ └── public-api.ts
72
- ├── services/
73
- │ ├── email-config-api.service.ts # Config CRUD + test sending
74
- │ ├── email-template-api.service.ts # Template CRUD + getBySlug
75
- │ ├── email-send.service.ts # Email sending operations (direct + template)
76
- │ └── email-builder-state.service.ts # Template builder state management
77
- ├── pages/
78
- │ ├── email-container/
79
- │ │ └── email-container.component.ts # Main container with permission-filtered tabs
80
- │ ├── config/
81
- │ │ ├── email-config-list.component.ts # Config list with test dialog
82
- │ │ └── email-config-form.component.ts # Config form with dynamic provider fields
83
- │ └── template/
84
- │ ├── template-list.component.ts # Template list with test send dialog
85
- │ └── template-form.component.ts # Template form with Angular Signal Forms
86
- ├── routes/
87
- │ └── email.routes.ts # Routes with permission guards
88
- └── public-api.ts
70
+ ```bash
71
+ npm install @flusys/ng-email @flusys/ng-core @flusys/ng-shared
89
72
  ```
90
73
 
91
74
  ---
92
75
 
93
- ## Route Integration
76
+ ## Quick Start
77
+
78
+ ### 1. Enable Email in Config
94
79
 
95
- ### Adding Email Routes
80
+ ```typescript
81
+ // environments/environment.ts
82
+ export const environment = {
83
+ services: {
84
+ email: { enabled: true },
85
+ },
86
+ };
87
+ ```
88
+
89
+ ### 2. Add Email Routes
96
90
 
97
91
  ```typescript
98
92
  // app.routes.ts
@@ -101,566 +95,328 @@ import { EMAIL_ROUTES } from '@flusys/ng-email';
101
95
  export const routes: Routes = [
102
96
  {
103
97
  path: 'email',
104
- children: EMAIL_ROUTES,
98
+ loadChildren: () => EMAIL_ROUTES,
105
99
  },
106
100
  ];
107
101
  ```
108
102
 
109
- ### Route Structure with Permission Guards
110
-
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` |
120
-
121
- ---
122
-
123
- ## Enums
124
-
125
- ### EmailProviderEnum
103
+ ### 3. Send Email Programmatically
126
104
 
127
105
  ```typescript
128
- export enum EmailProviderEnum {
129
- SMTP = 'smtp',
130
- SENDGRID = 'sendgrid',
131
- MAILGUN = 'mailgun',
132
- }
133
- ```
134
-
135
- ### EmailBlockType
106
+ import { EmailSendService } from '@flusys/ng-email';
136
107
 
137
- ```typescript
138
- export enum EmailBlockType {
139
- TEXT = 'text',
140
- IMAGE = 'image',
141
- BUTTON = 'button',
142
- DIVIDER = 'divider',
143
- HTML = 'html',
108
+ @Injectable({ providedIn: 'root' })
109
+ export class OrderService {
110
+ private emailSend = inject(EmailSendService);
111
+
112
+ sendOrderConfirmation(order: Order): Observable<IMessageResponse> {
113
+ return this.emailSend.send({
114
+ templateKey: 'order-confirmation',
115
+ to: order.customerEmail,
116
+ variables: {
117
+ customerName: order.customerName,
118
+ orderNumber: order.id,
119
+ totalAmount: order.total.toFixed(2),
120
+ },
121
+ });
122
+ }
144
123
  }
145
124
  ```
146
125
 
147
126
  ---
148
127
 
149
- ## Interfaces
150
-
151
- ### Email Configuration
152
-
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;
128
+ ## Email Schema
182
129
 
183
- /** Email configuration interface */
184
- interface IEmailConfig extends IBaseEntity {
185
- name: string;
186
- provider: EmailProviderEnum;
187
- config: EmailProviderConfig;
188
- fromEmail: string | null;
189
- fromName: string | null;
190
- isActive: boolean;
191
- isDefault: boolean;
192
- companyId?: string | null;
193
- }
130
+ ### IEmailSchema
194
131
 
195
- /** Create email config DTO */
196
- interface ICreateEmailConfigDto {
197
- name: string;
198
- provider: EmailProviderEnum;
199
- config: EmailProviderConfig;
200
- fromEmail?: string;
201
- fromName?: string;
202
- isActive?: boolean;
203
- isDefault?: boolean;
204
- }
205
-
206
- /** Update email config DTO */
207
- interface IUpdateEmailConfigDto extends Partial<ICreateEmailConfigDto> {
208
- id: string;
209
- }
210
- ```
211
-
212
- ### Email Template
132
+ Complete email template definition stored as JSON:
213
133
 
214
134
  ```typescript
215
- /** Email template interface */
216
- interface IEmailTemplate extends IBaseEntity {
217
- name: string;
218
- slug: string;
219
- description: string | null;
220
- subject: string;
221
- schema: IEmailSchema;
222
- htmlContent: string;
223
- textContent: string | null;
224
- schemaVersion: number;
225
- isActive: boolean;
226
- isHtml: boolean;
227
- metadata: Record<string, unknown> | null;
228
- companyId?: string | null;
229
- }
230
-
231
- /** Create email template DTO */
232
- interface ICreateEmailTemplateDto {
135
+ interface IEmailSchema {
136
+ id: string;
233
137
  name: string;
234
- slug: string;
235
- description?: string;
236
- subject: string;
237
- schema: IEmailSchema;
238
- htmlContent: string;
239
- textContent?: string;
240
- isActive?: boolean;
241
- isHtml?: boolean;
242
- metadata?: Record<string, unknown>;
138
+ key: string; // Unique identifier for programmatic sending
139
+ subject: string; // Supports {{variable}} interpolation
140
+ previewText?: string;
141
+ blocks: IEmailBlock[];
142
+ settings: IEmailSettings;
143
+ isDraft: boolean;
144
+ createdAt: string;
145
+ updatedAt: string;
243
146
  }
244
147
 
245
- /** Update email template DTO */
246
- interface IUpdateEmailTemplateDto extends Partial<ICreateEmailTemplateDto> {
247
- id: string;
148
+ interface IEmailSettings {
149
+ backgroundColor: string;
150
+ contentWidth: number; // pixels (default: 600)
151
+ fontFamily: string;
152
+ padding: number; // px
248
153
  }
249
154
  ```
250
155
 
251
- ### Email Schema
252
-
253
- **Type Aliases:**
254
- - `EmailVariableType` = `'string' | 'number' | 'boolean' | 'date'`
255
- - `EmailSectionType` = `'header' | 'body' | 'footer'`
256
- - `EmailBlockContent` = `ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent`
257
-
258
- **Block Content Types:**
156
+ ### Block Types
259
157
 
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` |
158
+ Email templates are composed of blocks:
267
159
 
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
160
+ | Block Type | Description | Key Properties |
161
+ |------------|-------------|----------------|
162
+ | `header` | Logo + company name header | `logoFileId`, `companyName`, `backgroundColor` |
163
+ | `text` | Rich text paragraph | `content` (HTML), `fontSize`, `color`, `align` |
164
+ | `heading` | H1-H4 heading | `content`, `level`, `color`, `align` |
165
+ | `button` | Call-to-action button | `label`, `url`, `backgroundColor`, `textColor` |
166
+ | `image` | Image block | `fileId`, `altText`, `width`, `link` |
167
+ | `divider` | Horizontal rule | `color`, `thickness` |
168
+ | `spacer` | Vertical whitespace | `height` |
169
+ | `columns` | Two-column layout | `leftBlock`, `rightBlock` |
170
+ | `footer` | Unsubscribe/legal footer | `content`, `unsubscribeUrl` |
281
171
 
282
172
  ```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
- };
173
+ interface IEmailBlock {
174
+ id: string;
175
+ type: EmailBlockType;
176
+ [key: string]: unknown; // Block-specific properties
303
177
  }
304
178
  ```
305
179
 
306
- ### Email Sending
307
-
308
- ```typescript
309
- /** Test send result */
310
- interface ITestSendResult {
311
- success: boolean;
312
- messageId?: string;
313
- error?: string;
314
- }
180
+ **Variable Interpolation:**
315
181
 
316
- /** Send email request DTO (direct) */
317
- interface ISendEmailDto {
318
- to: string | string[];
319
- cc?: string | string[];
320
- bcc?: string | string[];
321
- subject: string;
322
- html: string;
323
- text?: string;
324
- from?: string;
325
- fromName?: string;
326
- replyTo?: string;
327
- emailConfigId?: string;
328
- }
182
+ Use `{{variableName}}` in any text content:
329
183
 
330
- /** Send template email request DTO */
331
- interface ISendTemplateEmailDto {
332
- templateId?: string;
333
- templateSlug?: string;
334
- to: string | string[];
335
- cc?: string | string[];
336
- bcc?: string | string[];
337
- variables?: Record<string, any>;
338
- from?: string;
339
- fromName?: string;
340
- replyTo?: string;
341
- emailConfigId?: string;
342
- }
184
+ ```
185
+ Subject: "Order {{orderNumber}} Confirmed"
186
+ Content: "Hi {{customerName}}, your order totaling ${{totalAmount}} is confirmed."
343
187
  ```
344
188
 
189
+ Variables are XSS-sanitized server-side before rendering.
190
+
345
191
  ---
346
192
 
347
193
  ## Services
348
194
 
349
- ### EmailConfigApiService
195
+ ### EmailProviderApiService
350
196
 
351
- Email configuration CRUD. Extends `ApiResourceService<ICreateEmailConfigDto | IUpdateEmailConfigDto, IEmailConfig>`.
197
+ Manages email provider configuration (SMTP, SendGrid, Mailgun):
352
198
 
353
199
  ```typescript
354
- @Injectable({ providedIn: 'root' })
355
- export class EmailConfigApiService extends ApiResourceService<
356
- ICreateEmailConfigDto | IUpdateEmailConfigDto,
357
- IEmailConfig
358
- > {
359
- private readonly appConfig = inject<IAppConfig>(APP_CONFIG);
360
-
361
- constructor() {
362
- // Resource name, HttpClient, service prefix
363
- super('email-config', inject(HttpClient), 'email');
200
+ import { EmailProviderApiService } from '@flusys/ng-email';
201
+
202
+ @Injectable({ ... })
203
+ export class MyService {
204
+ private providerApi = inject(EmailProviderApiService);
205
+
206
+ getProviders(): Observable<IListResponse<IEmailProvider>> {
207
+ return this.providerApi.getAll({});
364
208
  }
365
209
 
366
- /** Send test email to verify configuration */
367
- sendTest(configId: string, recipient: string): Observable<ISingleResponse<ITestSendResult>> {
368
- const emailBaseUrl = getServiceUrl(this.appConfig, 'email');
369
- return this.http.post<ISingleResponse<ITestSendResult>>(
370
- `${emailBaseUrl}/send/test`,
371
- { emailConfigId: configId, recipient },
372
- );
210
+ configureSmtp(config: ISmtpConfig): Observable<ISingleResponse<IEmailProvider>> {
211
+ return this.providerApi.insert({
212
+ type: EmailProviderEnum.SMTP,
213
+ ...config,
214
+ });
215
+ }
216
+
217
+ testProvider(id: string): Observable<IMessageResponse> {
218
+ return this.providerApi.test(id);
373
219
  }
374
220
  }
375
221
  ```
376
222
 
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
-
384
223
  ### EmailTemplateApiService
385
224
 
386
- Email template CRUD. Extends `ApiResourceService<ICreateEmailTemplateDto | IUpdateEmailTemplateDto, IEmailTemplate>`.
225
+ Manages email template CRUD and publishing:
387
226
 
388
227
  ```typescript
389
- @Injectable({ providedIn: 'root' })
390
- export class EmailTemplateApiService extends ApiResourceService<
391
- ICreateEmailTemplateDto | IUpdateEmailTemplateDto,
392
- IEmailTemplate
393
- > {
394
- constructor() {
395
- super('email-template', inject(HttpClient), 'email');
228
+ import { EmailTemplateApiService } from '@flusys/ng-email';
229
+
230
+ @Injectable({ ... })
231
+ export class MyService {
232
+ private templateApi = inject(EmailTemplateApiService);
233
+
234
+ getTemplates(): Observable<IListResponse<IEmailTemplate>> {
235
+ return this.templateApi.getAll({ pageSize: 50 });
236
+ }
237
+
238
+ getByKey(key: string): Observable<ISingleResponse<IEmailTemplate>> {
239
+ return this.templateApi.getByKey(key);
396
240
  }
397
241
 
398
- /** Get template by slug (POST-only RPC pattern) */
399
- getBySlug(slug: string): Observable<ISingleResponse<IEmailTemplate | null>> {
400
- return this.http.post<ISingleResponse<IEmailTemplate | null>>(
401
- `${this.baseUrl}/get-by-slug`,
402
- { slug },
403
- );
242
+ publish(id: string): Observable<IMessageResponse> {
243
+ return this.templateApi.publish(id);
404
244
  }
405
245
  }
406
246
  ```
407
247
 
408
248
  ### EmailSendService
409
249
 
410
- Handles email sending operations. Uses `getServiceUrl` from `@flusys/ng-core` for dynamic endpoint resolution.
250
+ Programmatic email sending from any feature module:
411
251
 
412
252
  ```typescript
253
+ import { EmailSendService } from '@flusys/ng-email';
254
+
413
255
  @Injectable({ providedIn: 'root' })
414
- export class EmailSendService {
415
- private readonly http = inject(HttpClient);
416
- private readonly appConfig = inject<IAppConfig>(APP_CONFIG);
417
- private readonly baseUrl = `${getServiceUrl(this.appConfig, 'email')}/send`;
418
-
419
- /** Send email directly (without template) */
420
- sendDirect(dto: ISendEmailDto): Observable<ISingleResponse<ITestSendResult>> {
421
- return this.http.post<ISingleResponse<ITestSendResult>>(
422
- `${this.baseUrl}/direct`,
423
- dto,
424
- );
256
+ export class NotificationBridgeService {
257
+ private emailSend = inject(EmailSendService);
258
+
259
+ sendWelcomeEmail(user: IUser): Observable<IMessageResponse> {
260
+ return this.emailSend.send({
261
+ templateKey: 'welcome',
262
+ to: user.email,
263
+ variables: {
264
+ firstName: user.firstName,
265
+ loginUrl: 'https://app.example.com/auth/login',
266
+ },
267
+ });
425
268
  }
426
269
 
427
- /** Send email using template */
428
- sendTemplate(dto: ISendTemplateEmailDto): Observable<ISingleResponse<ITestSendResult>> {
429
- return this.http.post<ISingleResponse<ITestSendResult>>(
430
- `${this.baseUrl}/template`,
431
- dto,
432
- );
270
+ sendPasswordReset(email: string, resetToken: string): Observable<IMessageResponse> {
271
+ return this.emailSend.send({
272
+ templateKey: 'password-reset',
273
+ to: email,
274
+ variables: { resetToken, expiresIn: '24 hours' },
275
+ });
433
276
  }
434
277
  }
435
278
  ```
436
279
 
280
+ **EmailSendService.send() Options:**
281
+
282
+ | Field | Type | Required | Description |
283
+ |-------|------|----------|-------------|
284
+ | `templateKey` | `string` | ✅ | Unique template key |
285
+ | `to` | `string \| string[]` | ✅ | Recipient(s) |
286
+ | `variables` | `Record<string, string \| number>` | — | Template variables |
287
+ | `cc` | `string[]` | — | CC recipients |
288
+ | `bcc` | `string[]` | — | BCC recipients |
289
+ | `replyTo` | `string` | — | Reply-to address |
290
+ | `attachments` | `IEmailAttachment[]` | — | File attachments |
291
+
437
292
  ### EmailBuilderStateService
438
293
 
439
- Manages email builder UI state. **Provided at component level**, not root.
294
+ Component-scoped service managing the visual builder state:
440
295
 
441
296
  ```typescript
442
- @Injectable() // NOT providedIn: 'root'
443
- export class EmailBuilderStateService {
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
297
+ // Component-scoped not provided at root
298
+ @Component({
299
+ providers: [EmailBuilderStateService],
300
+ })
301
+ export class EmailBuilderComponent {
302
+ private state = inject(EmailBuilderStateService);
303
+
304
+ schema = this.state.schema; // Signal<IEmailSchema>
305
+ selectedBlock = this.state.selectedBlock; // Signal<IEmailBlock | null>
306
+ isDirty = this.state.isDirty; // Signal<boolean>
307
+ previewMode = this.state.previewMode; // Signal<'desktop' | 'mobile'>
476
308
  }
477
309
  ```
478
310
 
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
-
508
311
  ---
509
312
 
510
313
  ## Components
511
314
 
512
- All components are standalone, use Angular 21 signals, and lazy-load via routes.
513
-
514
- ### Component Overview
515
-
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 |
315
+ ### EmailBuilderComponent
523
316
 
524
- ### EmailContainerComponent
317
+ Visual block-based email template editor:
525
318
 
526
- Filters tabs using `PermissionValidatorService.hasPermission()`. Tabs: Templates (`EMAIL_TEMPLATE_PERMISSIONS.READ`), Configurations (`EMAIL_CONFIG_PERMISSIONS.READ`).
527
-
528
- ### EmailConfigListComponent
529
-
530
- **Provider Metadata:** SMTP (`pi pi-server`, info), SendGrid (`pi pi-cloud`, success), Mailgun (`pi pi-cloud`, warn). Default icon: `pi pi-envelope`.
319
+ ```html
320
+ <flusys-email-builder
321
+ [templateId]="templateId"
322
+ (saved)="onTemplateSaved($event)"
323
+ />
324
+ ```
531
325
 
532
- **Signals:** `isLoading`, `configs`, `totalRecords`, `pageSize`, `first`, `showTestDialog`, `selectedConfig`, `isSendingTest`, `testRecipient`.
326
+ Features:
327
+ - Left panel: Block type palette
328
+ - Center: Email canvas (drag to reorder blocks)
329
+ - Right panel: Selected block properties
330
+ - Top bar: Save draft / Publish / Preview toggle
533
331
 
534
- ### EmailConfigFormComponent
332
+ ### EmailPreviewComponent
535
333
 
536
- **Type Guards:** `isSmtpConfig` (`'host' in config && 'auth' in config`), `isSendGridConfig` (`'apiKey' in config && !('domain' in config)`), `isMailgunConfig` (`'apiKey' in config && 'domain' in config`).
334
+ Renders an email template with mock variable values:
537
335
 
538
- **Form Pattern:** Private signal with flattened provider fields, public getter, typed `updateFormModel<K>()` method.
336
+ ```html
337
+ <flusys-email-preview
338
+ [schema]="emailSchema"
339
+ [variables]="mockVariables"
340
+ [viewport]="'desktop'"
341
+ />
342
+ ```
539
343
 
540
- ### TemplateListComponent
344
+ | Input | Type | Description |
345
+ |-------|------|-------------|
346
+ | `schema` | `IEmailSchema` | Template to preview |
347
+ | `variables` | `Record<string, string>` | Mock values for `{{vars}}` |
348
+ | `viewport` | `'desktop' \| 'mobile'` | Preview viewport width |
541
349
 
542
- **Variable Extraction:** Computed signal extracts `{{variableName}}` patterns via `matchAll(/\{\{(\w+)\}\}/g)`, deduplicates with Set, returns sorted array.
350
+ ---
543
351
 
544
- ### TemplateFormComponent
352
+ ## Email Provider Enum
545
353
 
546
- Uses Angular Signal Forms (`form`, `FormField`, `required` from `@angular/forms/signals`). Integrates with `EmailBuilderStateService` for schema management.
354
+ ```typescript
355
+ import { EmailProviderEnum } from '@flusys/ng-email';
547
356
 
548
- **Content Conversion:** Auto-syncs when switching modes. `textToHtml()` wraps in `<p>`, `htmlToPlainText()` uses `DOMParser` for XSS-safe parsing. Preview renders in sandboxed iframe.
357
+ enum EmailProviderEnum {
358
+ SMTP = 'SMTP',
359
+ SENDGRID = 'SENDGRID',
360
+ MAILGUN = 'MAILGUN',
361
+ }
362
+ ```
549
363
 
550
364
  ---
551
365
 
552
- ## Responsive Design Patterns
553
-
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' }"` |
366
+ ## API Endpoints
367
+
368
+ | Method | Endpoint | Description |
369
+ |--------|----------|-------------|
370
+ | POST | `/email/provider/get-all` | List email providers |
371
+ | POST | `/email/provider/get/:id` | Get provider config |
372
+ | POST | `/email/provider/insert` | Add provider |
373
+ | POST | `/email/provider/update` | Update provider |
374
+ | POST | `/email/provider/delete` | Delete provider |
375
+ | POST | `/email/provider/test/:id` | Send test email |
376
+ | POST | `/email/template/get-all` | List templates |
377
+ | POST | `/email/template/get/:id` | Get template |
378
+ | POST | `/email/template/insert` | Create template |
379
+ | POST | `/email/template/update` | Update template |
380
+ | POST | `/email/template/delete` | Delete template |
381
+ | POST | `/email/template/publish/:id` | Publish template |
382
+ | POST | `/email/send` | Send email |
561
383
 
562
384
  ---
563
385
 
564
- ## Dark Mode Support
386
+ ## Configuration Reference
565
387
 
566
- All components use PrimeNG/Tailwind dark mode classes:
388
+ | Config Key | Type | Default | Description |
389
+ |------------|------|---------|-------------|
390
+ | `services.email.enabled` | `boolean` | `false` | Enable email module |
567
391
 
568
- | Element | Classes |
569
- |---------|---------|
570
- | Borders | `border-surface` |
571
- | Text | `text-muted-color` |
572
- | Backgrounds | `bg-surface-0 dark:bg-surface-900`, `bg-surface-100 dark:bg-surface-700` |
573
- | Code blocks | `bg-surface-100 dark:bg-surface-700` |
574
- | Hover states | `hover:bg-surface-hover` |
575
- | Surface ground | `bg-surface-ground` |
392
+ Email provider credentials (API keys, SMTP host/port) are configured server-side in `nestjs-email`. The Angular client only calls the send API.
576
393
 
577
394
  ---
578
395
 
579
- ## Usage Examples
396
+ ## Troubleshooting
580
397
 
581
- ```typescript
582
- import { EmailSendService, EmailConfigApiService, EmailTemplateApiService, EmailBuilderStateService, EmailBlockType } from '@flusys/ng-email';
398
+ **`EmailSendService.send()` returns 404**
583
399
 
584
- // Inject services
585
- private readonly emailSendService = inject(EmailSendService);
586
- private readonly configService = inject(EmailConfigApiService);
587
- private readonly templateService = inject(EmailTemplateApiService);
400
+ Template with the given `key` doesn't exist or is still in draft. Publish the template first via the builder UI or `templateApi.publish(id)`.
588
401
 
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
- }));
402
+ **Visual builder blocks not reordering**
593
403
 
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
- }));
404
+ CDK drag-and-drop requires `@angular/cdk`. Install it and import `DragDropModule`.
599
405
 
600
- // Test configuration
601
- const success = (await firstValueFrom(this.configService.sendTest(configId, recipient))).data?.success;
406
+ **Images in email not displaying for recipients**
602
407
 
603
- // Get template by slug
604
- const template = (await firstValueFrom(this.templateService.getBySlug('welcome-email'))).data;
605
- ```
408
+ Images are referenced by file ID. The backend resolves these to absolute URLs when rendering. Ensure `ng-storage` is configured and the storage backend is publicly accessible (or uses presigned URLs).
606
409
 
607
- **Using EmailBuilderStateService:** Provide at component level, access signals (`headerSection`, `bodySection`), methods: `loadSchema()`, `addBlock(sectionId, EmailBlockType.TEXT)`, `updateBlock(sectionId, blockId, { content })`.
410
+ **Variables showing as `{{varName}}` in sent emails**
608
411
 
609
- ---
412
+ The `variables` object key doesn't match the template placeholder. Check for exact case match: `{{customerName}}` requires `{ customerName: '...' }`.
610
413
 
611
- ## Backend Integration
612
-
613
- All endpoints use **POST-only RPC** style (not REST).
614
-
615
- | Service | Endpoint | Description |
616
- |---------|----------|-------------|
617
- | Config | `POST /email/email-config/insert` | Create config |
618
- | Config | `POST /email/email-config/get/:id` | Get config by ID |
619
- | Config | `POST /email/email-config/get-all` | List configs (paginated) |
620
- | Config | `POST /email/email-config/update` | Update config |
621
- | Config | `POST /email/email-config/delete` | Delete config |
622
- | Template | `POST /email/email-template/insert` | Create template |
623
- | Template | `POST /email/email-template/get/:id` | Get template by ID |
624
- | Template | `POST /email/email-template/get-all` | List templates (paginated) |
625
- | Template | `POST /email/email-template/get-by-slug` | Get template by slug |
626
- | Template | `POST /email/email-template/update` | Update template |
627
- | Template | `POST /email/email-template/delete` | Delete template |
628
- | Send | `POST /email/send/direct` | Send email directly |
629
- | Send | `POST /email/send/template` | Send using template |
630
- | Send | `POST /email/send/test` | Test configuration |
414
+ **Provider test fails with "Connection refused"**
631
415
 
632
- ---
633
-
634
- ## Best Practices
635
-
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'` |
416
+ SMTP host/port is incorrect or the email provider's server is blocking the connection. Verify credentials in the Email Providers configuration page.
645
417
 
646
418
  ---
647
419
 
648
- ## Public API Exports
649
-
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` |
661
-
662
- ---
420
+ ## License
663
421
 
664
- **Last Updated:** 2026-02-25
665
- **Version:** 3.0.1
666
- **Angular Version:** 21
422
+ MIT © FLUSYS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/ng-email",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "Email management module for FLUSYS Angular applications",
5
5
  "license": "MIT",
6
6
  "peerDependencies": {
@@ -8,9 +8,9 @@
8
8
  "@angular/core": ">=21.0.0",
9
9
  "@angular/forms": ">=21.0.0",
10
10
  "@angular/router": ">=21.0.0",
11
- "@flusys/ng-core": ">=4.0.1",
12
- "@flusys/ng-layout": ">=4.0.1",
13
- "@flusys/ng-shared": ">=4.0.1",
11
+ "@flusys/ng-core": ">=4.1.0",
12
+ "@flusys/ng-layout": ">=4.1.0",
13
+ "@flusys/ng-shared": ">=4.1.0",
14
14
  "@primeuix/themes": ">=1.0.0",
15
15
  "primeicons": ">=7.0.0",
16
16
  "primeng": ">=21.0.0",