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