@flusys/ng-email 1.1.0-beta
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 +660 -0
- package/fesm2022/flusys-ng-email-email-config-form.component-8dOBD-Q-.mjs +651 -0
- package/fesm2022/flusys-ng-email-email-config-form.component-8dOBD-Q-.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-email-config-list.component-CJhSPkCZ.mjs +508 -0
- package/fesm2022/flusys-ng-email-email-config-list.component-CJhSPkCZ.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-template-form.component-DjzG_lG1.mjs +529 -0
- package/fesm2022/flusys-ng-email-template-form.component-DjzG_lG1.mjs.map +1 -0
- package/fesm2022/flusys-ng-email-template-list.component-CGJxfZMT.mjs +595 -0
- package/fesm2022/flusys-ng-email-template-list.component-CGJxfZMT.mjs.map +1 -0
- package/fesm2022/flusys-ng-email.mjs +571 -0
- package/fesm2022/flusys-ng-email.mjs.map +1 -0
- package/package.json +32 -0
- package/types/flusys-ng-email.d.ts +456 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
5
|
+
import * as i1 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import { MessageService } from 'primeng/api';
|
|
8
|
+
import { PrimeModule } from '@flusys/ng-shared';
|
|
9
|
+
import { EmailTemplateApiService, EmailBuilderStateService } from './flusys-ng-email.mjs';
|
|
10
|
+
import * as i2 from 'primeng/button';
|
|
11
|
+
import * as i5 from 'primeng/inputtext';
|
|
12
|
+
import * as i8 from 'primeng/tag';
|
|
13
|
+
import * as i5$1 from 'primeng/textarea';
|
|
14
|
+
import * as i9 from 'primeng/toast';
|
|
15
|
+
import * as i7 from 'primeng/toggleswitch';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Email template form component (create/edit)
|
|
19
|
+
*/
|
|
20
|
+
class TemplateFormComponent {
|
|
21
|
+
route = inject(ActivatedRoute);
|
|
22
|
+
router = inject(Router);
|
|
23
|
+
templateService = inject(EmailTemplateApiService);
|
|
24
|
+
messageService = inject(MessageService);
|
|
25
|
+
state = inject(EmailBuilderStateService);
|
|
26
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
27
|
+
existingTemplate = signal(null, ...(ngDevMode ? [{ debugName: "existingTemplate" }] : []));
|
|
28
|
+
isEditMode = computed(() => !!this.existingTemplate(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
29
|
+
editorMode = signal('html', ...(ngDevMode ? [{ debugName: "editorMode" }] : []));
|
|
30
|
+
formModel = {
|
|
31
|
+
id: '',
|
|
32
|
+
name: '',
|
|
33
|
+
slug: '',
|
|
34
|
+
description: '',
|
|
35
|
+
subject: '',
|
|
36
|
+
htmlContent: '',
|
|
37
|
+
textContent: '',
|
|
38
|
+
isActive: true,
|
|
39
|
+
isHtml: true,
|
|
40
|
+
};
|
|
41
|
+
ngOnInit() {
|
|
42
|
+
const id = this.route.snapshot.paramMap.get('id');
|
|
43
|
+
if (id) {
|
|
44
|
+
this.loadTemplate(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async loadTemplate(id) {
|
|
48
|
+
this.isLoading.set(true);
|
|
49
|
+
try {
|
|
50
|
+
const response = await this.templateService.findByIdAsync(id);
|
|
51
|
+
if (response.success && response.data) {
|
|
52
|
+
const template = response.data;
|
|
53
|
+
this.existingTemplate.set(template);
|
|
54
|
+
this.formModel = {
|
|
55
|
+
id: template.id,
|
|
56
|
+
name: template.name,
|
|
57
|
+
slug: template.slug,
|
|
58
|
+
description: template.description || '',
|
|
59
|
+
subject: template.subject,
|
|
60
|
+
htmlContent: template.htmlContent,
|
|
61
|
+
textContent: template.textContent || '',
|
|
62
|
+
isActive: template.isActive,
|
|
63
|
+
isHtml: template.isHtml ?? true,
|
|
64
|
+
};
|
|
65
|
+
// Set editor mode based on saved template type
|
|
66
|
+
this.editorMode.set(template.isHtml ? 'html' : 'text');
|
|
67
|
+
this.state.loadSchema(template.schema);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
this.isLoading.set(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
setEditorMode(mode) {
|
|
75
|
+
const currentMode = this.editorMode();
|
|
76
|
+
// Sync content when switching modes
|
|
77
|
+
if (currentMode === 'text' && mode === 'html') {
|
|
78
|
+
// Switching from text to html - convert text to basic HTML if htmlContent is empty
|
|
79
|
+
if (!this.formModel.htmlContent && this.formModel.textContent) {
|
|
80
|
+
this.formModel.htmlContent = this.textToHtml(this.formModel.textContent);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (currentMode === 'html' && mode === 'text') {
|
|
84
|
+
// Switching from html to text - generate text from HTML if textContent is empty
|
|
85
|
+
if (!this.formModel.textContent && this.formModel.htmlContent) {
|
|
86
|
+
this.formModel.textContent = this.htmlToPlainText(this.formModel.htmlContent);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.editorMode.set(mode);
|
|
90
|
+
this.formModel.isHtml = mode === 'html';
|
|
91
|
+
}
|
|
92
|
+
/** Convert plain text to basic HTML */
|
|
93
|
+
textToHtml(text) {
|
|
94
|
+
if (!text)
|
|
95
|
+
return '';
|
|
96
|
+
// Convert line breaks to paragraphs
|
|
97
|
+
return text
|
|
98
|
+
.split(/\n\n+/)
|
|
99
|
+
.map((p) => `<p>${p.replace(/\n/g, '<br>')}</p>`)
|
|
100
|
+
.join('\n');
|
|
101
|
+
}
|
|
102
|
+
onContentChange() {
|
|
103
|
+
// Trigger change detection for preview
|
|
104
|
+
}
|
|
105
|
+
/** Convert HTML to plain text */
|
|
106
|
+
htmlToPlainText(html) {
|
|
107
|
+
if (!html)
|
|
108
|
+
return '';
|
|
109
|
+
// Create a temporary element to parse HTML
|
|
110
|
+
const temp = document.createElement('div');
|
|
111
|
+
temp.innerHTML = html;
|
|
112
|
+
// Get text content and clean up whitespace
|
|
113
|
+
return (temp.textContent || temp.innerText || '')
|
|
114
|
+
.replace(/\s+/g, ' ')
|
|
115
|
+
.trim();
|
|
116
|
+
}
|
|
117
|
+
/** Get HTML for preview iframe */
|
|
118
|
+
getPreviewHtml() {
|
|
119
|
+
const baseStyles = `
|
|
120
|
+
<style>
|
|
121
|
+
body { font-family: Arial, sans-serif; margin: 16px; }
|
|
122
|
+
img { max-width: 100%; height: auto; }
|
|
123
|
+
</style>
|
|
124
|
+
`;
|
|
125
|
+
return baseStyles + (this.formModel.htmlContent || '');
|
|
126
|
+
}
|
|
127
|
+
async onSave() {
|
|
128
|
+
if (!this.formModel.name || !this.formModel.slug || !this.formModel.subject) {
|
|
129
|
+
this.messageService.add({
|
|
130
|
+
severity: 'warn',
|
|
131
|
+
summary: 'Validation',
|
|
132
|
+
detail: 'Please fill in all required fields.',
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.isLoading.set(true);
|
|
137
|
+
try {
|
|
138
|
+
const schema = this.state.schema();
|
|
139
|
+
// Use user-provided plain text, or auto-generate from HTML if empty
|
|
140
|
+
const textContent = this.formModel.textContent || this.htmlToPlainText(this.formModel.htmlContent);
|
|
141
|
+
const data = {
|
|
142
|
+
name: this.formModel.name,
|
|
143
|
+
slug: this.formModel.slug,
|
|
144
|
+
description: this.formModel.description || undefined,
|
|
145
|
+
subject: this.formModel.subject,
|
|
146
|
+
schema: {
|
|
147
|
+
...schema,
|
|
148
|
+
name: this.formModel.name,
|
|
149
|
+
subject: this.formModel.subject,
|
|
150
|
+
},
|
|
151
|
+
htmlContent: this.formModel.htmlContent || '<p>Email content</p>',
|
|
152
|
+
textContent: textContent || undefined,
|
|
153
|
+
isActive: this.formModel.isActive,
|
|
154
|
+
isHtml: this.formModel.isHtml,
|
|
155
|
+
};
|
|
156
|
+
if (this.isEditMode()) {
|
|
157
|
+
await this.templateService.updateAsync({ id: this.formModel.id, ...data });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
await this.templateService.insertAsync(data);
|
|
161
|
+
}
|
|
162
|
+
this.messageService.add({
|
|
163
|
+
severity: 'success',
|
|
164
|
+
summary: 'Success',
|
|
165
|
+
detail: `Template ${this.isEditMode() ? 'updated' : 'created'} successfully.`,
|
|
166
|
+
});
|
|
167
|
+
this.router.navigate(['../'], { relativeTo: this.route });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
this.messageService.add({
|
|
171
|
+
severity: 'error',
|
|
172
|
+
summary: 'Error',
|
|
173
|
+
detail: error?.message || 'Failed to save template.',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
this.isLoading.set(false);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TemplateFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
181
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: TemplateFormComponent, isStandalone: true, selector: "lib-template-form", providers: [MessageService, EmailBuilderStateService], ngImport: i0, template: `
|
|
182
|
+
<div class="template-form">
|
|
183
|
+
<!-- Header -->
|
|
184
|
+
<div class="flex justify-between items-center mb-4">
|
|
185
|
+
<div class="flex items-center gap-2">
|
|
186
|
+
<p-button
|
|
187
|
+
icon="pi pi-arrow-left"
|
|
188
|
+
[text]="true"
|
|
189
|
+
routerLink="../"
|
|
190
|
+
/>
|
|
191
|
+
<h2 class="text-xl font-semibold">
|
|
192
|
+
{{ isEditMode() ? 'Edit Template' : 'New Template' }}
|
|
193
|
+
</h2>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="flex gap-2">
|
|
196
|
+
<p-button
|
|
197
|
+
label="Cancel"
|
|
198
|
+
[text]="true"
|
|
199
|
+
routerLink="../"
|
|
200
|
+
/>
|
|
201
|
+
<p-button
|
|
202
|
+
label="Save"
|
|
203
|
+
icon="pi pi-save"
|
|
204
|
+
[loading]="isLoading()"
|
|
205
|
+
(onClick)="onSave()"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- Form Fields -->
|
|
211
|
+
<div class="card mb-4">
|
|
212
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
213
|
+
<div class="field">
|
|
214
|
+
<label for="name" class="block font-medium mb-2">Template Name *</label>
|
|
215
|
+
<input
|
|
216
|
+
pInputText
|
|
217
|
+
id="name"
|
|
218
|
+
[(ngModel)]="formModel.name"
|
|
219
|
+
class="w-full"
|
|
220
|
+
placeholder="e.g., Welcome Email"
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="field">
|
|
225
|
+
<label for="slug" class="block font-medium mb-2">Slug *</label>
|
|
226
|
+
<input
|
|
227
|
+
pInputText
|
|
228
|
+
id="slug"
|
|
229
|
+
[(ngModel)]="formModel.slug"
|
|
230
|
+
class="w-full"
|
|
231
|
+
placeholder="e.g., welcome-email"
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="field md:col-span-2">
|
|
236
|
+
<label for="subject" class="block font-medium mb-2">Subject *</label>
|
|
237
|
+
<input
|
|
238
|
+
pInputText
|
|
239
|
+
id="subject"
|
|
240
|
+
[(ngModel)]="formModel.subject"
|
|
241
|
+
class="w-full"
|
|
242
|
+
[placeholder]="'e.g., Welcome to ' + '{{' + 'appName' + '}}' + '!'"
|
|
243
|
+
/>
|
|
244
|
+
<small class="text-gray-500">Use {{ '{{variableName}}' }} for dynamic content</small>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="field">
|
|
248
|
+
<label for="description" class="block font-medium mb-2">Description</label>
|
|
249
|
+
<input
|
|
250
|
+
pInputText
|
|
251
|
+
id="description"
|
|
252
|
+
[(ngModel)]="formModel.description"
|
|
253
|
+
class="w-full"
|
|
254
|
+
placeholder="Brief description of the template"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="field flex items-center gap-2">
|
|
259
|
+
<p-toggleswitch [(ngModel)]="formModel.isActive" />
|
|
260
|
+
<label>Active</label>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Content Editor & Preview -->
|
|
266
|
+
<div class="grid grid-cols-1 gap-4" [class.lg:grid-cols-2]="editorMode() === 'html'">
|
|
267
|
+
<!-- Editor Panel -->
|
|
268
|
+
<div class="card">
|
|
269
|
+
<div class="flex justify-between items-center mb-3">
|
|
270
|
+
<h3 class="font-semibold">Content</h3>
|
|
271
|
+
<div class="flex gap-1">
|
|
272
|
+
<p-button
|
|
273
|
+
label="HTML"
|
|
274
|
+
[outlined]="editorMode() !== 'html'"
|
|
275
|
+
[severity]="editorMode() === 'html' ? 'primary' : 'secondary'"
|
|
276
|
+
size="small"
|
|
277
|
+
(onClick)="setEditorMode('html')"
|
|
278
|
+
/>
|
|
279
|
+
<p-button
|
|
280
|
+
label="Plain Text"
|
|
281
|
+
[outlined]="editorMode() !== 'text'"
|
|
282
|
+
[severity]="editorMode() === 'text' ? 'primary' : 'secondary'"
|
|
283
|
+
size="small"
|
|
284
|
+
(onClick)="setEditorMode('text')"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
@if (editorMode() === 'html') {
|
|
290
|
+
<textarea
|
|
291
|
+
pTextarea
|
|
292
|
+
[(ngModel)]="formModel.htmlContent"
|
|
293
|
+
class="w-full font-mono text-sm"
|
|
294
|
+
rows="20"
|
|
295
|
+
placeholder="<html>...</html>"
|
|
296
|
+
(ngModelChange)="onContentChange()"
|
|
297
|
+
></textarea>
|
|
298
|
+
} @else {
|
|
299
|
+
<textarea
|
|
300
|
+
pTextarea
|
|
301
|
+
[(ngModel)]="formModel.textContent"
|
|
302
|
+
class="w-full font-mono text-sm"
|
|
303
|
+
rows="20"
|
|
304
|
+
placeholder="Enter plain text content for email clients that don't support HTML"
|
|
305
|
+
></textarea>
|
|
306
|
+
<small class="text-gray-500 mt-1 block">
|
|
307
|
+
<i class="pi pi-info-circle mr-1"></i>
|
|
308
|
+
Plain text version sent to email clients without HTML support
|
|
309
|
+
</small>
|
|
310
|
+
}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- Preview Panel (HTML mode only) -->
|
|
314
|
+
@if (editorMode() === 'html') {
|
|
315
|
+
<div class="card">
|
|
316
|
+
<div class="flex justify-between items-center mb-3">
|
|
317
|
+
<h3 class="font-semibold">Preview</h3>
|
|
318
|
+
<p-tag value="Live Preview" severity="info" />
|
|
319
|
+
</div>
|
|
320
|
+
<div
|
|
321
|
+
class="border rounded bg-white min-h-[400px] overflow-auto"
|
|
322
|
+
style="max-height: 500px;"
|
|
323
|
+
>
|
|
324
|
+
@if (formModel.htmlContent) {
|
|
325
|
+
<iframe
|
|
326
|
+
[srcdoc]="getPreviewHtml()"
|
|
327
|
+
class="w-full h-full min-h-[400px] border-0"
|
|
328
|
+
sandbox="allow-same-origin"
|
|
329
|
+
></iframe>
|
|
330
|
+
} @else {
|
|
331
|
+
<div class="flex items-center justify-center h-full min-h-[400px] text-gray-400">
|
|
332
|
+
<div class="text-center">
|
|
333
|
+
<i class="pi pi-eye text-4xl mb-2"></i>
|
|
334
|
+
<p>Enter HTML content to see preview</p>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<p-toast />
|
|
345
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: i5.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i8.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i5$1.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "component", type: i9.Toast, selector: "p-toast", inputs: ["key", "autoZIndex", "baseZIndex", "life", "styleClass", "position", "preventOpenDuplicates", "preventDuplicates", "showTransformOptions", "hideTransformOptions", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "breakpoints"], outputs: ["onClose"] }, { kind: "component", type: i7.ToggleSwitch, selector: "p-toggleswitch, p-toggleSwitch, p-toggle-switch", inputs: ["styleClass", "tabindex", "inputId", "readonly", "trueValue", "falseValue", "ariaLabel", "size", "ariaLabelledBy", "autofocus"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
346
|
+
}
|
|
347
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: TemplateFormComponent, decorators: [{
|
|
348
|
+
type: Component,
|
|
349
|
+
args: [{
|
|
350
|
+
selector: 'lib-template-form',
|
|
351
|
+
standalone: true,
|
|
352
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
353
|
+
imports: [
|
|
354
|
+
CommonModule,
|
|
355
|
+
RouterLink,
|
|
356
|
+
FormsModule,
|
|
357
|
+
PrimeModule,
|
|
358
|
+
],
|
|
359
|
+
providers: [MessageService, EmailBuilderStateService],
|
|
360
|
+
template: `
|
|
361
|
+
<div class="template-form">
|
|
362
|
+
<!-- Header -->
|
|
363
|
+
<div class="flex justify-between items-center mb-4">
|
|
364
|
+
<div class="flex items-center gap-2">
|
|
365
|
+
<p-button
|
|
366
|
+
icon="pi pi-arrow-left"
|
|
367
|
+
[text]="true"
|
|
368
|
+
routerLink="../"
|
|
369
|
+
/>
|
|
370
|
+
<h2 class="text-xl font-semibold">
|
|
371
|
+
{{ isEditMode() ? 'Edit Template' : 'New Template' }}
|
|
372
|
+
</h2>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="flex gap-2">
|
|
375
|
+
<p-button
|
|
376
|
+
label="Cancel"
|
|
377
|
+
[text]="true"
|
|
378
|
+
routerLink="../"
|
|
379
|
+
/>
|
|
380
|
+
<p-button
|
|
381
|
+
label="Save"
|
|
382
|
+
icon="pi pi-save"
|
|
383
|
+
[loading]="isLoading()"
|
|
384
|
+
(onClick)="onSave()"
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<!-- Form Fields -->
|
|
390
|
+
<div class="card mb-4">
|
|
391
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
392
|
+
<div class="field">
|
|
393
|
+
<label for="name" class="block font-medium mb-2">Template Name *</label>
|
|
394
|
+
<input
|
|
395
|
+
pInputText
|
|
396
|
+
id="name"
|
|
397
|
+
[(ngModel)]="formModel.name"
|
|
398
|
+
class="w-full"
|
|
399
|
+
placeholder="e.g., Welcome Email"
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<div class="field">
|
|
404
|
+
<label for="slug" class="block font-medium mb-2">Slug *</label>
|
|
405
|
+
<input
|
|
406
|
+
pInputText
|
|
407
|
+
id="slug"
|
|
408
|
+
[(ngModel)]="formModel.slug"
|
|
409
|
+
class="w-full"
|
|
410
|
+
placeholder="e.g., welcome-email"
|
|
411
|
+
/>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div class="field md:col-span-2">
|
|
415
|
+
<label for="subject" class="block font-medium mb-2">Subject *</label>
|
|
416
|
+
<input
|
|
417
|
+
pInputText
|
|
418
|
+
id="subject"
|
|
419
|
+
[(ngModel)]="formModel.subject"
|
|
420
|
+
class="w-full"
|
|
421
|
+
[placeholder]="'e.g., Welcome to ' + '{{' + 'appName' + '}}' + '!'"
|
|
422
|
+
/>
|
|
423
|
+
<small class="text-gray-500">Use {{ '{{variableName}}' }} for dynamic content</small>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="field">
|
|
427
|
+
<label for="description" class="block font-medium mb-2">Description</label>
|
|
428
|
+
<input
|
|
429
|
+
pInputText
|
|
430
|
+
id="description"
|
|
431
|
+
[(ngModel)]="formModel.description"
|
|
432
|
+
class="w-full"
|
|
433
|
+
placeholder="Brief description of the template"
|
|
434
|
+
/>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div class="field flex items-center gap-2">
|
|
438
|
+
<p-toggleswitch [(ngModel)]="formModel.isActive" />
|
|
439
|
+
<label>Active</label>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<!-- Content Editor & Preview -->
|
|
445
|
+
<div class="grid grid-cols-1 gap-4" [class.lg:grid-cols-2]="editorMode() === 'html'">
|
|
446
|
+
<!-- Editor Panel -->
|
|
447
|
+
<div class="card">
|
|
448
|
+
<div class="flex justify-between items-center mb-3">
|
|
449
|
+
<h3 class="font-semibold">Content</h3>
|
|
450
|
+
<div class="flex gap-1">
|
|
451
|
+
<p-button
|
|
452
|
+
label="HTML"
|
|
453
|
+
[outlined]="editorMode() !== 'html'"
|
|
454
|
+
[severity]="editorMode() === 'html' ? 'primary' : 'secondary'"
|
|
455
|
+
size="small"
|
|
456
|
+
(onClick)="setEditorMode('html')"
|
|
457
|
+
/>
|
|
458
|
+
<p-button
|
|
459
|
+
label="Plain Text"
|
|
460
|
+
[outlined]="editorMode() !== 'text'"
|
|
461
|
+
[severity]="editorMode() === 'text' ? 'primary' : 'secondary'"
|
|
462
|
+
size="small"
|
|
463
|
+
(onClick)="setEditorMode('text')"
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
@if (editorMode() === 'html') {
|
|
469
|
+
<textarea
|
|
470
|
+
pTextarea
|
|
471
|
+
[(ngModel)]="formModel.htmlContent"
|
|
472
|
+
class="w-full font-mono text-sm"
|
|
473
|
+
rows="20"
|
|
474
|
+
placeholder="<html>...</html>"
|
|
475
|
+
(ngModelChange)="onContentChange()"
|
|
476
|
+
></textarea>
|
|
477
|
+
} @else {
|
|
478
|
+
<textarea
|
|
479
|
+
pTextarea
|
|
480
|
+
[(ngModel)]="formModel.textContent"
|
|
481
|
+
class="w-full font-mono text-sm"
|
|
482
|
+
rows="20"
|
|
483
|
+
placeholder="Enter plain text content for email clients that don't support HTML"
|
|
484
|
+
></textarea>
|
|
485
|
+
<small class="text-gray-500 mt-1 block">
|
|
486
|
+
<i class="pi pi-info-circle mr-1"></i>
|
|
487
|
+
Plain text version sent to email clients without HTML support
|
|
488
|
+
</small>
|
|
489
|
+
}
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<!-- Preview Panel (HTML mode only) -->
|
|
493
|
+
@if (editorMode() === 'html') {
|
|
494
|
+
<div class="card">
|
|
495
|
+
<div class="flex justify-between items-center mb-3">
|
|
496
|
+
<h3 class="font-semibold">Preview</h3>
|
|
497
|
+
<p-tag value="Live Preview" severity="info" />
|
|
498
|
+
</div>
|
|
499
|
+
<div
|
|
500
|
+
class="border rounded bg-white min-h-[400px] overflow-auto"
|
|
501
|
+
style="max-height: 500px;"
|
|
502
|
+
>
|
|
503
|
+
@if (formModel.htmlContent) {
|
|
504
|
+
<iframe
|
|
505
|
+
[srcdoc]="getPreviewHtml()"
|
|
506
|
+
class="w-full h-full min-h-[400px] border-0"
|
|
507
|
+
sandbox="allow-same-origin"
|
|
508
|
+
></iframe>
|
|
509
|
+
} @else {
|
|
510
|
+
<div class="flex items-center justify-center h-full min-h-[400px] text-gray-400">
|
|
511
|
+
<div class="text-center">
|
|
512
|
+
<i class="pi pi-eye text-4xl mb-2"></i>
|
|
513
|
+
<p>Enter HTML content to see preview</p>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
}
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<p-toast />
|
|
524
|
+
`,
|
|
525
|
+
}]
|
|
526
|
+
}] });
|
|
527
|
+
|
|
528
|
+
export { TemplateFormComponent };
|
|
529
|
+
//# sourceMappingURL=flusys-ng-email-template-form.component-DjzG_lG1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flusys-ng-email-template-form.component-DjzG_lG1.mjs","sources":["../../../projects/ng-email/pages/template/template-form.component.ts"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n inject,\n signal,\n computed,\n OnInit,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ActivatedRoute, Router, RouterLink } from '@angular/router';\nimport { FormsModule } from '@angular/forms';\nimport { MessageService } from 'primeng/api';\nimport { PrimeModule } from '@flusys/ng-shared';\nimport { EmailTemplateApiService } from '../../services/email-template-api.service';\nimport { EmailBuilderStateService } from '../../services/email-builder-state.service';\nimport { IEmailTemplate } from '../../interfaces/email-template.interface';\nimport { createEmailSchema } from '../../interfaces/email-schema.interface';\n\n/**\n * Email template form component (create/edit)\n */\n@Component({\n selector: 'lib-template-form',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n CommonModule,\n RouterLink,\n FormsModule,\n PrimeModule,\n ],\n providers: [MessageService, EmailBuilderStateService],\n template: `\n <div class=\"template-form\">\n <!-- Header -->\n <div class=\"flex justify-between items-center mb-4\">\n <div class=\"flex items-center gap-2\">\n <p-button\n icon=\"pi pi-arrow-left\"\n [text]=\"true\"\n routerLink=\"../\"\n />\n <h2 class=\"text-xl font-semibold\">\n {{ isEditMode() ? 'Edit Template' : 'New Template' }}\n </h2>\n </div>\n <div class=\"flex gap-2\">\n <p-button\n label=\"Cancel\"\n [text]=\"true\"\n routerLink=\"../\"\n />\n <p-button\n label=\"Save\"\n icon=\"pi pi-save\"\n [loading]=\"isLoading()\"\n (onClick)=\"onSave()\"\n />\n </div>\n </div>\n\n <!-- Form Fields -->\n <div class=\"card mb-4\">\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div class=\"field\">\n <label for=\"name\" class=\"block font-medium mb-2\">Template Name *</label>\n <input\n pInputText\n id=\"name\"\n [(ngModel)]=\"formModel.name\"\n class=\"w-full\"\n placeholder=\"e.g., Welcome Email\"\n />\n </div>\n\n <div class=\"field\">\n <label for=\"slug\" class=\"block font-medium mb-2\">Slug *</label>\n <input\n pInputText\n id=\"slug\"\n [(ngModel)]=\"formModel.slug\"\n class=\"w-full\"\n placeholder=\"e.g., welcome-email\"\n />\n </div>\n\n <div class=\"field md:col-span-2\">\n <label for=\"subject\" class=\"block font-medium mb-2\">Subject *</label>\n <input\n pInputText\n id=\"subject\"\n [(ngModel)]=\"formModel.subject\"\n class=\"w-full\"\n [placeholder]=\"'e.g., Welcome to ' + '{{' + 'appName' + '}}' + '!'\"\n />\n <small class=\"text-gray-500\">Use {{ '{{variableName}}' }} for dynamic content</small>\n </div>\n\n <div class=\"field\">\n <label for=\"description\" class=\"block font-medium mb-2\">Description</label>\n <input\n pInputText\n id=\"description\"\n [(ngModel)]=\"formModel.description\"\n class=\"w-full\"\n placeholder=\"Brief description of the template\"\n />\n </div>\n\n <div class=\"field flex items-center gap-2\">\n <p-toggleswitch [(ngModel)]=\"formModel.isActive\" />\n <label>Active</label>\n </div>\n </div>\n </div>\n\n <!-- Content Editor & Preview -->\n <div class=\"grid grid-cols-1 gap-4\" [class.lg:grid-cols-2]=\"editorMode() === 'html'\">\n <!-- Editor Panel -->\n <div class=\"card\">\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"font-semibold\">Content</h3>\n <div class=\"flex gap-1\">\n <p-button\n label=\"HTML\"\n [outlined]=\"editorMode() !== 'html'\"\n [severity]=\"editorMode() === 'html' ? 'primary' : 'secondary'\"\n size=\"small\"\n (onClick)=\"setEditorMode('html')\"\n />\n <p-button\n label=\"Plain Text\"\n [outlined]=\"editorMode() !== 'text'\"\n [severity]=\"editorMode() === 'text' ? 'primary' : 'secondary'\"\n size=\"small\"\n (onClick)=\"setEditorMode('text')\"\n />\n </div>\n </div>\n\n @if (editorMode() === 'html') {\n <textarea\n pTextarea\n [(ngModel)]=\"formModel.htmlContent\"\n class=\"w-full font-mono text-sm\"\n rows=\"20\"\n placeholder=\"<html>...</html>\"\n (ngModelChange)=\"onContentChange()\"\n ></textarea>\n } @else {\n <textarea\n pTextarea\n [(ngModel)]=\"formModel.textContent\"\n class=\"w-full font-mono text-sm\"\n rows=\"20\"\n placeholder=\"Enter plain text content for email clients that don't support HTML\"\n ></textarea>\n <small class=\"text-gray-500 mt-1 block\">\n <i class=\"pi pi-info-circle mr-1\"></i>\n Plain text version sent to email clients without HTML support\n </small>\n }\n </div>\n\n <!-- Preview Panel (HTML mode only) -->\n @if (editorMode() === 'html') {\n <div class=\"card\">\n <div class=\"flex justify-between items-center mb-3\">\n <h3 class=\"font-semibold\">Preview</h3>\n <p-tag value=\"Live Preview\" severity=\"info\" />\n </div>\n <div\n class=\"border rounded bg-white min-h-[400px] overflow-auto\"\n style=\"max-height: 500px;\"\n >\n @if (formModel.htmlContent) {\n <iframe\n [srcdoc]=\"getPreviewHtml()\"\n class=\"w-full h-full min-h-[400px] border-0\"\n sandbox=\"allow-same-origin\"\n ></iframe>\n } @else {\n <div class=\"flex items-center justify-center h-full min-h-[400px] text-gray-400\">\n <div class=\"text-center\">\n <i class=\"pi pi-eye text-4xl mb-2\"></i>\n <p>Enter HTML content to see preview</p>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <p-toast />\n `,\n})\nexport class TemplateFormComponent implements OnInit {\n private readonly route = inject(ActivatedRoute);\n private readonly router = inject(Router);\n private readonly templateService = inject(EmailTemplateApiService);\n private readonly messageService = inject(MessageService);\n readonly state = inject(EmailBuilderStateService);\n\n readonly isLoading = signal(false);\n readonly existingTemplate = signal<IEmailTemplate | null>(null);\n readonly isEditMode = computed(() => !!this.existingTemplate());\n readonly editorMode = signal<'html' | 'text'>('html');\n\n formModel = {\n id: '',\n name: '',\n slug: '',\n description: '',\n subject: '',\n htmlContent: '',\n textContent: '',\n isActive: true,\n isHtml: true,\n };\n\n ngOnInit(): void {\n const id = this.route.snapshot.paramMap.get('id');\n if (id) {\n this.loadTemplate(id);\n }\n }\n\n async loadTemplate(id: string): Promise<void> {\n this.isLoading.set(true);\n try {\n const response = await this.templateService.findByIdAsync(id);\n if (response.success && response.data) {\n const template = response.data;\n this.existingTemplate.set(template);\n this.formModel = {\n id: template.id,\n name: template.name,\n slug: template.slug,\n description: template.description || '',\n subject: template.subject,\n htmlContent: template.htmlContent,\n textContent: template.textContent || '',\n isActive: template.isActive,\n isHtml: template.isHtml ?? true,\n };\n // Set editor mode based on saved template type\n this.editorMode.set(template.isHtml ? 'html' : 'text');\n this.state.loadSchema(template.schema);\n }\n } finally {\n this.isLoading.set(false);\n }\n }\n\n setEditorMode(mode: 'html' | 'text'): void {\n const currentMode = this.editorMode();\n\n // Sync content when switching modes\n if (currentMode === 'text' && mode === 'html') {\n // Switching from text to html - convert text to basic HTML if htmlContent is empty\n if (!this.formModel.htmlContent && this.formModel.textContent) {\n this.formModel.htmlContent = this.textToHtml(this.formModel.textContent);\n }\n } else if (currentMode === 'html' && mode === 'text') {\n // Switching from html to text - generate text from HTML if textContent is empty\n if (!this.formModel.textContent && this.formModel.htmlContent) {\n this.formModel.textContent = this.htmlToPlainText(this.formModel.htmlContent);\n }\n }\n\n this.editorMode.set(mode);\n this.formModel.isHtml = mode === 'html';\n }\n\n /** Convert plain text to basic HTML */\n private textToHtml(text: string): string {\n if (!text) return '';\n // Convert line breaks to paragraphs\n return text\n .split(/\\n\\n+/)\n .map((p) => `<p>${p.replace(/\\n/g, '<br>')}</p>`)\n .join('\\n');\n }\n\n onContentChange(): void {\n // Trigger change detection for preview\n }\n\n /** Convert HTML to plain text */\n private htmlToPlainText(html: string): string {\n if (!html) return '';\n // Create a temporary element to parse HTML\n const temp = document.createElement('div');\n temp.innerHTML = html;\n // Get text content and clean up whitespace\n return (temp.textContent || temp.innerText || '')\n .replace(/\\s+/g, ' ')\n .trim();\n }\n\n /** Get HTML for preview iframe */\n getPreviewHtml(): string {\n const baseStyles = `\n <style>\n body { font-family: Arial, sans-serif; margin: 16px; }\n img { max-width: 100%; height: auto; }\n </style>\n `;\n return baseStyles + (this.formModel.htmlContent || '');\n }\n\n async onSave(): Promise<void> {\n if (!this.formModel.name || !this.formModel.slug || !this.formModel.subject) {\n this.messageService.add({\n severity: 'warn',\n summary: 'Validation',\n detail: 'Please fill in all required fields.',\n });\n return;\n }\n\n this.isLoading.set(true);\n try {\n const schema = this.state.schema();\n // Use user-provided plain text, or auto-generate from HTML if empty\n const textContent = this.formModel.textContent || this.htmlToPlainText(this.formModel.htmlContent);\n\n const data = {\n name: this.formModel.name,\n slug: this.formModel.slug,\n description: this.formModel.description || undefined,\n subject: this.formModel.subject,\n schema: {\n ...schema,\n name: this.formModel.name,\n subject: this.formModel.subject,\n },\n htmlContent: this.formModel.htmlContent || '<p>Email content</p>',\n textContent: textContent || undefined,\n isActive: this.formModel.isActive,\n isHtml: this.formModel.isHtml,\n };\n\n if (this.isEditMode()) {\n await this.templateService.updateAsync({ id: this.formModel.id, ...data });\n } else {\n await this.templateService.insertAsync(data as any);\n }\n\n this.messageService.add({\n severity: 'success',\n summary: 'Success',\n detail: `Template ${this.isEditMode() ? 'updated' : 'created'} successfully.`,\n });\n\n this.router.navigate(['../'], { relativeTo: this.route });\n } catch (error: any) {\n this.messageService.add({\n severity: 'error',\n summary: 'Error',\n detail: error?.message || 'Failed to save template.',\n });\n } finally {\n this.isLoading.set(false);\n }\n }\n}\n"],"names":["i3","i4","i5","i6"],"mappings":"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"}
|