@flusys/ng-email 3.0.0-rc → 3.0.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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # Email Package Guide
2
2
 
3
3
  > **Package:** `@flusys/ng-email`
4
+ > **Version:** 3.0.0
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, IBaseEntity)
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, etc.)
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
- ├── email-builder-state.service.ts # Template builder state management
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 | Description |
110
- |------|-----------|-------------|
111
- | `/email` | EmailContainerComponent | Main container with tab navigation |
112
- | `/email/templates` | TemplateListComponent | List email templates |
113
- | `/email/templates/new` | TemplateFormComponent | Create new template |
114
- | `/email/templates/:id` | TemplateFormComponent | Edit template |
115
- | `/email/configs` | EmailConfigListComponent | List email configurations |
116
- | `/email/configs/new` | EmailConfigFormComponent | Create new config |
117
- | `/email/configs/:id` | EmailConfigFormComponent | Edit config |
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: Record<string, any>; // Provider-specific config
187
+ config: EmailProviderConfig;
156
188
  fromEmail: string | null;
157
189
  fromName: string | null;
158
190
  isActive: boolean;
159
- isDefault: boolean; // Set as default configuration
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: Record<string, any>;
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; // true=HTML mode, false=plain text
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
- ```typescript
242
- interface IEmailSchema {
243
- id: string;
244
- version: string;
245
- name: string;
246
- subject: string;
247
- preheader?: string;
248
- sections: IEmailSection[];
249
- variables?: IEmailTemplateVariable[];
250
- settings?: IEmailSettings;
251
- }
253
+ **Type Aliases:**
254
+ - `EmailVariableType` = `'string' | 'number' | 'boolean' | 'date'`
255
+ - `EmailSectionType` = `'header' | 'body' | 'footer'`
256
+ - `EmailBlockContent` = `ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent`
252
257
 
253
- interface IEmailSection {
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
- interface IEmailBlock {
263
- id: string;
264
- type: EmailBlockType;
265
- order: number;
266
- content: ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent;
267
- style?: IEmailBlockStyle;
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
- interface IEmailTemplateVariable {
271
- name: string;
272
- type: 'string' | 'number' | 'boolean' | 'date';
273
- required?: boolean;
274
- defaultValue?: string;
275
- description?: string;
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
- // Test send result
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
- // Direct email
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
- // Template email
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
- const http = inject(HttpClient);
336
- super('email/email-config', http);
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
- `${this.baseUrl.replace('/email-config', '')}/send/test`,
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
- const http = inject(HttpClient);
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 baseUrl = `${inject(APP_CONFIG).apiBaseUrl}/email/send`;
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. Should be provided at component level, not root.
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
- // 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;
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
- ### 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
514
+ ### Component Overview
436
515
 
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>
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
- <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>
524
+ ### EmailContainerComponent
447
525
 
448
- <router-outlet />
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
- Lists email configurations with actions.
455
-
456
- **Features:**
457
- - Lazy pagination via `p-table` with `[paginator]="totalRecords() > 0"`
458
- - Horizontal scroll on mobile (`overflow-x-auto -mx-4 sm:mx-0`)
459
- - Provider type tags (SMTP, SendGrid, Mailgun)
460
- - Active/Inactive status badges
461
- - Default configuration badge
462
- - Test button with dialog for recipient input
463
- - Edit and delete actions
464
- - Company info display when enabled
465
-
466
- **Test Dialog:**
467
-
468
- ```typescript
469
- showTestDialog = false; // Plain property for ngModel
470
- readonly selectedConfig = signal<IEmailConfig | null>(null);
471
- readonly isSendingTest = signal(false);
472
- testRecipient = '';
473
-
474
- onTest(config: IEmailConfig): void {
475
- this.selectedConfig.set(config);
476
- this.testRecipient = '';
477
- this.showTestDialog = true;
478
- }
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
- async sendTestEmail(): Promise<void> {
481
- const config = this.selectedConfig();
482
- if (!config || !this.testRecipient) return;
483
-
484
- const response = await firstValueFrom(
485
- this.configService.sendTest(config.id, this.testRecipient)
486
- );
487
-
488
- if (response.data?.success) {
489
- this.messageService.add({
490
- severity: 'success',
491
- summary: 'Success',
492
- detail: `Test email sent! Message ID: ${response.data.messageId}`,
493
- });
494
- this.showTestDialog = false;
495
- }
496
- }
497
- ```
532
+ **Signals:** `isLoading`, `configs`, `totalRecords`, `pageSize`, `first`, `showTestDialog`, `selectedConfig`, `isSendingTest`, `testRecipient`.
498
533
 
499
534
  ### EmailConfigFormComponent
500
535
 
501
- Create/edit email configuration with dynamic provider fields.
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
- **Features:**
504
- - Responsive grid layout (`grid-cols-1 md:grid-cols-2`)
505
- - Dynamic form fields based on selected provider
506
- - SMTP: host, port, secure toggle, username, password
507
- - SendGrid: apiKey (masked input)
508
- - Mailgun: apiKey, domain, region
509
- - Active toggle and Set as Default toggle
510
- - Form actions right-aligned at bottom
511
- - Create/Edit mode auto-detection
538
+ **Form Pattern:** Private signal with flattened provider fields, public getter, typed `updateFormModel<K>()` method.
512
539
 
513
540
  ### TemplateListComponent
514
541
 
515
- Lists email templates with test send functionality.
516
-
517
- **Features:**
518
- - Lazy pagination with conditional display
519
- - Responsive table with hidden columns on mobile
520
- - HTML/Text type badges (based on `isHtml`)
521
- - Active/Inactive status badges
522
- - Test send with dynamic variables
523
- - Edit and delete actions
524
-
525
- **Test Send Dialog with Variables:**
526
-
527
- ```typescript
528
- /** Extract variables from template content */
529
- readonly templateVariables = computed(() => {
530
- const template = this.selectedTemplate();
531
- if (!template) return [];
532
-
533
- const content = [
534
- template.subject,
535
- template.htmlContent,
536
- template.textContent || '',
537
- ].join(' ');
538
-
539
- const matches = content.matchAll(/\{\{(\w+)\}\}/g);
540
- const variables = new Set<string>();
541
-
542
- for (const match of matches) {
543
- variables.add(match[1]);
544
- }
545
-
546
- return Array.from(variables).sort();
547
- });
548
-
549
- variableValues: Record<string, string> = {};
550
- ```
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
- Create/edit email template with HTML/Plain Text toggle and live preview.
555
-
556
- **Features:**
557
- - Responsive form grid (`grid-cols-1 md:grid-cols-2`)
558
- - Name, slug, subject, description fields
559
- - Subject line with variable support hint
560
- - Editor Mode Toggle (HTML/Plain Text buttons)
561
- - Same height content editor and preview (`h-[400px]`)
562
- - Horizontal scroll for code (`wrap="off"`)
563
- - Live HTML preview in iframe with white background
564
- - Content sync between modes when switching
565
- - Active toggle
566
- - Form actions right-aligned at bottom
546
+ Uses Angular Signal Forms (`form`, `FormField`, `required` from `@angular/forms/signals`). Integrates with `EmailBuilderStateService` for schema management.
567
547
 
568
- **Preview with Light Background:**
569
-
570
- ```typescript
571
- getPreviewHtml(): string {
572
- const baseStyles = `
573
- <style>
574
- body {
575
- font-family: Arial, sans-serif;
576
- margin: 16px;
577
- background-color: #ffffff;
578
- color: #333333;
579
- }
580
- img { max-width: 100%; height: auto; }
581
- </style>
582
- `;
583
- return baseStyles + (this.formModel.htmlContent || '');
584
- }
585
- ```
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
- ### Tables
592
-
593
- ```html
594
- <!-- Horizontal scroll wrapper -->
595
- <div class="overflow-x-auto -mx-4 sm:mx-0">
596
- <p-table
597
- [paginator]="totalRecords() > 0"
598
- styleClass="p-datatable-sm"
599
- [tableStyle]="{ 'min-width': '50rem' }"
600
- >
601
- <!-- Hidden columns on smaller screens -->
602
- <th class="hidden md:table-cell">Slug</th>
603
- <th class="hidden lg:table-cell">Subject</th>
604
- <th class="hidden xl:table-cell">Created</th>
605
- </p-table>
606
- </div>
607
- ```
608
-
609
- ### Forms
610
-
611
- ```html
612
- <!-- Responsive grid -->
613
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
614
- <div class="field">...</div>
615
- <div class="field md:col-span-2">...</div> <!-- Full width on all screens -->
616
- </div>
617
-
618
- <!-- Form actions -->
619
- <div class="flex justify-end gap-2 mt-6 pt-4 border-t border-surface">
620
- <p-button label="Cancel" severity="secondary" [outlined]="true" routerLink="../" />
621
- <p-button [label]="isEditMode() ? 'Update' : 'Create'" icon="pi pi-save" />
622
- </div>
623
- ```
624
-
625
- ### Headers
626
-
627
- ```html
628
- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4">
629
- <div>
630
- <h3 class="text-lg sm:text-xl font-semibold m-0">Title</h3>
631
- @if (showCompanyInfo()) {
632
- <p class="text-sm text-muted-color mt-1">Company: {{ currentCompanyName() }}</p>
633
- }
634
- </div>
635
- <p-button label="Action" styleClass="w-full sm:w-auto" />
636
- </div>
637
- ```
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
- async testConfig(configId: string, recipient: string): Promise<boolean> {
712
- const response = await firstValueFrom(
713
- this.configService.sendTest(configId, recipient)
714
- );
715
-
716
- return response.data?.success ?? false;
717
- }
718
- ```
719
-
720
- ### Get Template by Slug
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
- ```typescript
723
- import { EmailTemplateApiService } from '@flusys/ng-email';
724
-
725
- private readonly templateService = inject(EmailTemplateApiService);
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
- async getTemplate(slug: string): Promise<IEmailTemplate | null> {
728
- const response = await firstValueFrom(
729
- this.templateService.getBySlug(slug)
730
- );
600
+ // Test configuration
601
+ const success = (await firstValueFrom(this.configService.sendTest(configId, recipient))).data?.success;
731
602
 
732
- return response.data ?? null;
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
- ### 1. Use Template Slugs for Code References
764
-
765
- ```typescript
766
- // Use slug for programmatic access
767
- await this.emailSendService.sendTemplate({
768
- templateSlug: 'welcome-email', // Stable identifier
769
- ...
770
- });
771
-
772
- // ❌ Don't hardcode template IDs
773
- await this.emailSendService.sendTemplate({
774
- templateId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', // May change
775
- ...
776
- });
777
- ```
778
-
779
- ### 2. Provide Both HTML and Text Content
780
-
781
- ```typescript
782
- // ✅ Provide both for maximum compatibility
783
- {
784
- isHtml: true,
785
- htmlContent: '<h1>Hello!</h1>',
786
- textContent: 'Hello!', // Fallback for text-only clients
787
- }
788
- ```
789
-
790
- ### 3. Test Configurations Before Production
791
-
792
- Always use the test send feature to verify configurations work before using them in production.
793
-
794
- ### 4. Use Meaningful Variable Names
795
-
796
- ```typescript
797
- // ✅ Clear, descriptive variable names
798
- "Hello {{userName}}, your order #{{orderNumber}} shipped on {{shipDate}}."
799
-
800
- // ❌ Vague or abbreviated names
801
- "Hello {{u}}, order {{o}} shipped {{d}}."
802
- ```
803
-
804
- ### 5. Handle Send Failures Gracefully
805
-
806
- ```typescript
807
- async sendEmail(): Promise<void> {
808
- try {
809
- const response = await firstValueFrom(
810
- this.emailSendService.sendTemplate({ ... })
811
- );
812
-
813
- if (response.data?.success) {
814
- this.messageService.add({
815
- severity: 'success',
816
- summary: 'Email Sent',
817
- detail: `Message ID: ${response.data.messageId}`,
818
- });
819
- } else {
820
- this.messageService.add({
821
- severity: 'error',
822
- summary: 'Send Failed',
823
- detail: response.data?.error || 'Unknown error occurred',
824
- });
825
- }
826
- } catch (error: any) {
827
- this.messageService.add({
828
- severity: 'error',
829
- summary: 'Error',
830
- detail: error?.message || 'Failed to send email.',
831
- });
832
- }
833
- }
834
- ```
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
- ```typescript
841
- // Enums
842
- export { EmailProviderEnum } from '@flusys/ng-email';
843
- export { EmailBlockType } from '@flusys/ng-email';
844
-
845
- // Interfaces
846
- export {
847
- IEmailConfig, ICreateEmailConfigDto, IUpdateEmailConfigDto,
848
- ISmtpConfig, ISendGridConfig, IMailgunConfig,
849
- IEmailTemplate, ICreateEmailTemplateDto, IUpdateEmailTemplateDto,
850
- IEmailSchema, IEmailSection, IEmailBlock, IEmailTemplateVariable,
851
- ISendEmailDto, ISendTemplateEmailDto, ITestSendResult,
852
- } from '@flusys/ng-email';
853
-
854
- // Services
855
- export {
856
- EmailConfigApiService,
857
- EmailTemplateApiService,
858
- EmailSendService,
859
- EmailBuilderStateService,
860
- } from '@flusys/ng-email';
861
-
862
- // Components
863
- export { EmailContainerComponent } from '@flusys/ng-email';
864
-
865
- // Routes
866
- export { EMAIL_ROUTES } from '@flusys/ng-email';
867
- ```
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-21
664
+ **Last Updated:** 2026-02-25
665
+ **Version:** 3.0.0
872
666
  **Angular Version:** 21