@masterteam/notification 0.0.27 → 0.0.29
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/assets/i18n/ar.json +236 -45
- package/assets/i18n/en.json +236 -45
- package/assets/notification.css +1 -1
- package/fesm2022/masterteam-notification.mjs +1983 -848
- package/fesm2022/masterteam-notification.mjs.map +1 -1
- package/package.json +5 -3
- package/types/masterteam-notification.d.ts +790 -304
|
@@ -1,112 +1,73 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, input,
|
|
3
|
-
import { toSignal } from '@angular/core/rxjs-interop';
|
|
4
|
-
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { inject, Injectable, computed, input, ChangeDetectionStrategy, Component, output, signal, DestroyRef, effect, untracked, ElementRef, viewChild, HostListener } from '@angular/core';
|
|
3
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
5
4
|
import * as i1 from '@angular/forms';
|
|
6
|
-
import { FormControl, Validators, ReactiveFormsModule
|
|
7
|
-
import
|
|
8
|
-
import { TranslocoModule, TranslocoService } from '@jsverse/transloco';
|
|
9
|
-
import { Card } from '@masterteam/components/card';
|
|
5
|
+
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
6
|
+
import { TranslocoService, TranslocoDirective } from '@jsverse/transloco';
|
|
10
7
|
import { Button } from '@masterteam/components/button';
|
|
8
|
+
import { ConfirmationService } from '@masterteam/components/confirmation';
|
|
9
|
+
import { EmptyState } from '@masterteam/components/empty-state';
|
|
10
|
+
import { ModalService } from '@masterteam/components/modal';
|
|
11
11
|
import { SelectField } from '@masterteam/components/select-field';
|
|
12
|
+
import { Tabs } from '@masterteam/components/tabs';
|
|
12
13
|
import { TextField } from '@masterteam/components/text-field';
|
|
13
14
|
import { TextareaField } from '@masterteam/components/textarea-field';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { ToastService } from '@masterteam/components/toast';
|
|
16
|
+
import { Tooltip } from '@masterteam/components/tooltip';
|
|
17
|
+
import { FormulaBuilder } from '@masterteam/formula-builder';
|
|
17
18
|
import { Icon } from '@masterteam/icons';
|
|
18
|
-
import {
|
|
19
|
-
import { ModalService } from '@masterteam/components/modal';
|
|
19
|
+
import { EmailHtmlPreview, EmailVisualEditor, EmailUserTestDialog } from '@masterteam/system-settings';
|
|
20
20
|
import { Skeleton } from 'primeng/skeleton';
|
|
21
|
-
import {
|
|
22
|
-
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
21
|
+
import { map, tap, Subject, merge, debounceTime, filter, switchMap, of, catchError, startWith, Observable } from 'rxjs';
|
|
22
|
+
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
|
23
23
|
import { Action, Selector, State, Store, select } from '@ngxs/store';
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
30
|
-
templateNameControl = new FormControl('', [Validators.required]);
|
|
31
|
-
onSave() {
|
|
32
|
-
if (this.templateNameControl.valid) {
|
|
33
|
-
this.ref.close({ name: this.templateNameControl.value?.trim() });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
onCancel() {
|
|
37
|
-
this.ref.close();
|
|
38
|
-
}
|
|
39
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
40
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: AddTemplateDialog, isStandalone: true, selector: "mt-add-template-dialog", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"modalService.contentClass\" class=\"p-4\">\r\n <mt-text-field\r\n [formControl]=\"templateNameControl\"\r\n [label]=\"'notification.templateName' | transloco\"\r\n [placeholder]=\"'notification.enterTemplateName' | transloco\"\r\n [required]=\"true\"\r\n />\r\n</div>\r\n\r\n<div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"'notification.cancel' | transloco\"\r\n variant=\"outlined\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"'notification.save' | transloco\"\r\n [disabled]=\"!templateNameControl.valid\"\r\n (onClick)=\"onSave()\"\r\n />\r\n</div>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
|
|
41
|
-
}
|
|
42
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, decorators: [{
|
|
43
|
-
type: Component,
|
|
44
|
-
args: [{ selector: 'mt-add-template-dialog', standalone: true, imports: [
|
|
45
|
-
CommonModule,
|
|
46
|
-
ReactiveFormsModule,
|
|
47
|
-
TranslocoModule,
|
|
48
|
-
TextField,
|
|
49
|
-
Button,
|
|
50
|
-
], template: "<div [class]=\"modalService.contentClass\" class=\"p-4\">\r\n <mt-text-field\r\n [formControl]=\"templateNameControl\"\r\n [label]=\"'notification.templateName' | transloco\"\r\n [placeholder]=\"'notification.enterTemplateName' | transloco\"\r\n [required]=\"true\"\r\n />\r\n</div>\r\n\r\n<div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"'notification.cancel' | transloco\"\r\n variant=\"outlined\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"'notification.save' | transloco\"\r\n [disabled]=\"!templateNameControl.valid\"\r\n (onClick)=\"onSave()\"\r\n />\r\n</div>\r\n" }]
|
|
51
|
-
}], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }] } });
|
|
24
|
+
import { handleApiRequest } from '@masterteam/components';
|
|
25
|
+
import { ModalRef } from '@masterteam/components/dialog';
|
|
26
|
+
import { RadioCardsField } from '@masterteam/components/radio-cards-field';
|
|
27
|
+
import { ToggleField } from '@masterteam/components/toggle-field';
|
|
28
|
+
import { DatePipe } from '@angular/common';
|
|
52
29
|
|
|
53
30
|
// ============================================================================
|
|
54
|
-
// Action Keys
|
|
31
|
+
// Action Keys
|
|
55
32
|
// ============================================================================
|
|
56
33
|
var NotificationActionKey;
|
|
57
34
|
(function (NotificationActionKey) {
|
|
58
35
|
NotificationActionKey["GetEvents"] = "getEvents";
|
|
59
|
-
NotificationActionKey["
|
|
60
|
-
NotificationActionKey["
|
|
36
|
+
NotificationActionKey["GetContext"] = "getContext";
|
|
37
|
+
NotificationActionKey["GetDefaults"] = "getDefaults";
|
|
38
|
+
NotificationActionKey["GetPublications"] = "getPublications";
|
|
39
|
+
NotificationActionKey["CreateTemplate"] = "createTemplate";
|
|
40
|
+
NotificationActionKey["SaveTemplate"] = "saveTemplate";
|
|
61
41
|
NotificationActionKey["AddReceivers"] = "addReceivers";
|
|
62
42
|
NotificationActionKey["DeleteReceiver"] = "deleteReceiver";
|
|
63
|
-
NotificationActionKey["AddTemplate"] = "addTemplate";
|
|
64
|
-
NotificationActionKey["UpdateTemplate"] = "updateTemplate";
|
|
65
43
|
NotificationActionKey["DeleteTemplate"] = "deleteTemplate";
|
|
44
|
+
NotificationActionKey["PublishTemplate"] = "publishTemplate";
|
|
45
|
+
NotificationActionKey["SetActivation"] = "setActivation";
|
|
46
|
+
NotificationActionKey["RestorePublication"] = "restorePublication";
|
|
47
|
+
NotificationActionKey["InitializeDefaults"] = "initializeDefaults";
|
|
48
|
+
NotificationActionKey["RestoreDefault"] = "restoreDefault";
|
|
66
49
|
})(NotificationActionKey || (NotificationActionKey = {}));
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
key: 'app',
|
|
86
|
-
labelKey: 'notification.channel.app',
|
|
87
|
-
supportsBilingual: true,
|
|
88
|
-
bilingualOptional: false,
|
|
89
|
-
useRichEditor: true,
|
|
90
|
-
translatableKey: 'appTemplate',
|
|
91
|
-
},
|
|
50
|
+
/** Writes that must never run twice at once (the contract forbids double submits). */
|
|
51
|
+
const NOTIFICATION_MUTATION_KEYS = [
|
|
52
|
+
NotificationActionKey.CreateTemplate,
|
|
53
|
+
NotificationActionKey.SaveTemplate,
|
|
54
|
+
NotificationActionKey.AddReceivers,
|
|
55
|
+
NotificationActionKey.DeleteReceiver,
|
|
56
|
+
NotificationActionKey.DeleteTemplate,
|
|
57
|
+
NotificationActionKey.PublishTemplate,
|
|
58
|
+
NotificationActionKey.SetActivation,
|
|
59
|
+
NotificationActionKey.RestorePublication,
|
|
60
|
+
NotificationActionKey.InitializeDefaults,
|
|
61
|
+
NotificationActionKey.RestoreDefault,
|
|
62
|
+
];
|
|
63
|
+
const NOTIFICATION_CHANNELS = [
|
|
64
|
+
'App',
|
|
65
|
+
'Email',
|
|
66
|
+
'Sms',
|
|
92
67
|
];
|
|
93
|
-
/** Helper to get channel config by key */
|
|
94
|
-
function getChannelConfig(key) {
|
|
95
|
-
return CHANNEL_CONFIGS.find((c) => c.key === key) ?? CHANNEL_CONFIGS[0];
|
|
96
|
-
}
|
|
97
|
-
/** Build form control name for a channel field */
|
|
98
|
-
function getFormControlName(ch, type) {
|
|
99
|
-
if (type === 'single')
|
|
100
|
-
return ch.templateKey ?? null;
|
|
101
|
-
if (!ch.translatableKey)
|
|
102
|
-
return null;
|
|
103
|
-
return type === 'en'
|
|
104
|
-
? `${ch.translatableKey}.en`
|
|
105
|
-
: `${ch.translatableKey}.ar`;
|
|
106
|
-
}
|
|
107
68
|
|
|
108
69
|
// ============================================================================
|
|
109
|
-
//
|
|
70
|
+
// Scope
|
|
110
71
|
// ============================================================================
|
|
111
72
|
class SetModuleInfo {
|
|
112
73
|
moduleType;
|
|
@@ -123,56 +84,45 @@ class SetModuleInfo {
|
|
|
123
84
|
this.parentPath = parentPath;
|
|
124
85
|
}
|
|
125
86
|
}
|
|
87
|
+
class ResetNotificationState {
|
|
88
|
+
static type = '[Notification] Reset State';
|
|
89
|
+
}
|
|
126
90
|
// ============================================================================
|
|
127
|
-
//
|
|
91
|
+
// Loading
|
|
128
92
|
// ============================================================================
|
|
93
|
+
/** Loads (or re-reads) the scope's events, keeping the current selection when it still exists. */
|
|
129
94
|
class GetEvents {
|
|
130
95
|
static type = '[Notification] Get Events';
|
|
131
96
|
}
|
|
132
|
-
class
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
static type = '[Notification] Get Properties';
|
|
138
|
-
constructor(isMultiLang) {
|
|
139
|
-
this.isMultiLang = isMultiLang;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// ============================================================================
|
|
143
|
-
// Action Mode (Workflow)
|
|
144
|
-
// ============================================================================
|
|
145
|
-
class SetActionMode {
|
|
146
|
-
actionId;
|
|
147
|
-
static type = '[Notification] Set Action Mode';
|
|
148
|
-
constructor(actionId) {
|
|
149
|
-
this.actionId = actionId;
|
|
97
|
+
class GetAuthoringContext {
|
|
98
|
+
eventId;
|
|
99
|
+
static type = '[Notification] Get Authoring Context';
|
|
100
|
+
constructor(eventId) {
|
|
101
|
+
this.eventId = eventId;
|
|
150
102
|
}
|
|
151
103
|
}
|
|
152
|
-
class
|
|
153
|
-
|
|
154
|
-
static type = '[Notification] Get
|
|
155
|
-
constructor(
|
|
156
|
-
this.
|
|
104
|
+
class GetDefaultRecipes {
|
|
105
|
+
eventId;
|
|
106
|
+
static type = '[Notification] Get Default Recipes';
|
|
107
|
+
constructor(eventId) {
|
|
108
|
+
this.eventId = eventId;
|
|
157
109
|
}
|
|
158
110
|
}
|
|
159
|
-
class
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.actionId = actionId;
|
|
165
|
-
this.isMultiLang = isMultiLang;
|
|
111
|
+
class GetPublications {
|
|
112
|
+
templateId;
|
|
113
|
+
static type = '[Notification] Get Publications';
|
|
114
|
+
constructor(templateId) {
|
|
115
|
+
this.templateId = templateId;
|
|
166
116
|
}
|
|
167
117
|
}
|
|
168
118
|
// ============================================================================
|
|
169
|
-
// Selection
|
|
119
|
+
// Selection
|
|
170
120
|
// ============================================================================
|
|
171
121
|
class SelectEvent {
|
|
172
|
-
|
|
122
|
+
eventId;
|
|
173
123
|
static type = '[Notification] Select Event';
|
|
174
|
-
constructor(
|
|
175
|
-
this.
|
|
124
|
+
constructor(eventId) {
|
|
125
|
+
this.eventId = eventId;
|
|
176
126
|
}
|
|
177
127
|
}
|
|
178
128
|
class SelectTemplate {
|
|
@@ -183,58 +133,426 @@ class SelectTemplate {
|
|
|
183
133
|
}
|
|
184
134
|
}
|
|
185
135
|
// ============================================================================
|
|
186
|
-
//
|
|
136
|
+
// Template writes
|
|
187
137
|
// ============================================================================
|
|
188
|
-
class
|
|
138
|
+
class CreateTemplate {
|
|
139
|
+
eventId;
|
|
140
|
+
payload;
|
|
141
|
+
static type = '[Notification] Create Template';
|
|
142
|
+
constructor(eventId, payload) {
|
|
143
|
+
this.eventId = eventId;
|
|
144
|
+
this.payload = payload;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
class SaveTemplate {
|
|
189
148
|
templateId;
|
|
190
149
|
payload;
|
|
191
|
-
static type = '[Notification]
|
|
150
|
+
static type = '[Notification] Save Template';
|
|
192
151
|
constructor(templateId, payload) {
|
|
193
152
|
this.templateId = templateId;
|
|
194
153
|
this.payload = payload;
|
|
195
154
|
}
|
|
196
155
|
}
|
|
156
|
+
class DeleteTemplate {
|
|
157
|
+
templateId;
|
|
158
|
+
expectedRevision;
|
|
159
|
+
static type = '[Notification] Delete Template';
|
|
160
|
+
constructor(templateId, expectedRevision) {
|
|
161
|
+
this.templateId = templateId;
|
|
162
|
+
this.expectedRevision = expectedRevision;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
class AddReceivers {
|
|
166
|
+
templateId;
|
|
167
|
+
expectedRevision;
|
|
168
|
+
receivers;
|
|
169
|
+
static type = '[Notification] Add Receivers';
|
|
170
|
+
constructor(templateId, expectedRevision, receivers) {
|
|
171
|
+
this.templateId = templateId;
|
|
172
|
+
this.expectedRevision = expectedRevision;
|
|
173
|
+
this.receivers = receivers;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
197
176
|
class DeleteReceiver {
|
|
198
177
|
receiverId;
|
|
178
|
+
expectedRevision;
|
|
199
179
|
static type = '[Notification] Delete Receiver';
|
|
200
|
-
constructor(receiverId) {
|
|
180
|
+
constructor(receiverId, expectedRevision) {
|
|
201
181
|
this.receiverId = receiverId;
|
|
182
|
+
this.expectedRevision = expectedRevision;
|
|
202
183
|
}
|
|
203
184
|
}
|
|
204
185
|
// ============================================================================
|
|
205
|
-
//
|
|
186
|
+
// Lifecycle
|
|
206
187
|
// ============================================================================
|
|
207
|
-
class
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
188
|
+
class PublishTemplate {
|
|
189
|
+
templateId;
|
|
190
|
+
expectedRevision;
|
|
191
|
+
reviews;
|
|
192
|
+
static type = '[Notification] Publish Template';
|
|
193
|
+
constructor(templateId, expectedRevision, reviews) {
|
|
194
|
+
this.templateId = templateId;
|
|
195
|
+
this.expectedRevision = expectedRevision;
|
|
196
|
+
this.reviews = reviews;
|
|
214
197
|
}
|
|
215
198
|
}
|
|
216
|
-
class
|
|
199
|
+
class SetTemplateActivation {
|
|
217
200
|
templateId;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
201
|
+
expectedRevision;
|
|
202
|
+
enabled;
|
|
203
|
+
reviews;
|
|
204
|
+
static type = '[Notification] Set Template Activation';
|
|
205
|
+
constructor(templateId, expectedRevision, enabled, reviews) {
|
|
221
206
|
this.templateId = templateId;
|
|
222
|
-
this.
|
|
207
|
+
this.expectedRevision = expectedRevision;
|
|
208
|
+
this.enabled = enabled;
|
|
209
|
+
this.reviews = reviews;
|
|
223
210
|
}
|
|
224
211
|
}
|
|
225
|
-
class
|
|
212
|
+
class RestorePublication {
|
|
226
213
|
templateId;
|
|
227
|
-
|
|
228
|
-
|
|
214
|
+
expectedRevision;
|
|
215
|
+
publicationRevision;
|
|
216
|
+
reviews;
|
|
217
|
+
static type = '[Notification] Restore Publication';
|
|
218
|
+
constructor(templateId, expectedRevision, publicationRevision, reviews) {
|
|
229
219
|
this.templateId = templateId;
|
|
220
|
+
this.expectedRevision = expectedRevision;
|
|
221
|
+
this.publicationRevision = publicationRevision;
|
|
222
|
+
this.reviews = reviews;
|
|
230
223
|
}
|
|
231
224
|
}
|
|
232
225
|
// ============================================================================
|
|
233
|
-
//
|
|
226
|
+
// Defaults
|
|
234
227
|
// ============================================================================
|
|
235
|
-
class
|
|
236
|
-
|
|
228
|
+
class InitializeDefaults {
|
|
229
|
+
eventId;
|
|
230
|
+
static type = '[Notification] Initialize Defaults';
|
|
231
|
+
constructor(eventId) {
|
|
232
|
+
this.eventId = eventId;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
class RestoreDefault {
|
|
236
|
+
eventId;
|
|
237
|
+
payload;
|
|
238
|
+
static type = '[Notification] Restore Default';
|
|
239
|
+
constructor(eventId, payload) {
|
|
240
|
+
this.eventId = eventId;
|
|
241
|
+
this.payload = payload;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Notification authoring endpoints. Every call is marked `noMessage`: the
|
|
247
|
+
* backend answers failures with a bare stable code (`NOTIFICATION_*`) as the
|
|
248
|
+
* message, so the host's message interceptor would toast that raw code. The
|
|
249
|
+
* editor translates the code itself (see `notification.errors.ts`).
|
|
250
|
+
*/
|
|
251
|
+
class NotificationTemplateApi {
|
|
252
|
+
http = inject(HttpClient);
|
|
253
|
+
baseUrl = 'notifications';
|
|
254
|
+
headers = new HttpHeaders({ noMessage: 'true' });
|
|
255
|
+
getEvents(scopePath) {
|
|
256
|
+
return this.get(`${scopePath}/events`);
|
|
257
|
+
}
|
|
258
|
+
getContext(eventId) {
|
|
259
|
+
return this.get(`/events/${eventId}/authoring-context`);
|
|
260
|
+
}
|
|
261
|
+
getDefaults(eventId) {
|
|
262
|
+
return this.get(`/events/${eventId}/default-recipes`);
|
|
263
|
+
}
|
|
264
|
+
initializeDefaults(eventId) {
|
|
265
|
+
return this.post(`/events/${eventId}/defaults`, null);
|
|
266
|
+
}
|
|
267
|
+
restoreDefault(eventId, payload) {
|
|
268
|
+
return this.post(`/events/${eventId}/defaults/restore`, payload);
|
|
269
|
+
}
|
|
270
|
+
preview(eventId, request) {
|
|
271
|
+
return this.post(`/events/${eventId}/preview`, request);
|
|
272
|
+
}
|
|
273
|
+
createTemplate(eventId, payload) {
|
|
274
|
+
return this.post(`/events/${eventId}`, payload);
|
|
275
|
+
}
|
|
276
|
+
saveTemplate(templateId, payload) {
|
|
277
|
+
return this.http
|
|
278
|
+
.put(this.url(`/templates/${templateId}`), payload, { headers: this.headers })
|
|
279
|
+
.pipe(map((response) => response.data));
|
|
280
|
+
}
|
|
281
|
+
addReceivers(templateId, expectedRevision, receivers) {
|
|
282
|
+
return this.post(`/templates/${templateId}`, {
|
|
283
|
+
expectedRevision,
|
|
284
|
+
receivers,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
deleteReceiver(receiverId, expectedRevision) {
|
|
288
|
+
return this.delete(`/receivers/${receiverId}`, expectedRevision);
|
|
289
|
+
}
|
|
290
|
+
deleteTemplate(templateId, expectedRevision) {
|
|
291
|
+
return this.delete(`/templates/${templateId}`, expectedRevision);
|
|
292
|
+
}
|
|
293
|
+
getPublications(templateId) {
|
|
294
|
+
return this.get(`/templates/${templateId}/publications`);
|
|
295
|
+
}
|
|
296
|
+
publish(templateId, expectedRevision, reviews) {
|
|
297
|
+
return this.post(`/templates/${templateId}/publish`, {
|
|
298
|
+
expectedRevision,
|
|
299
|
+
...reviews,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
setActivation(templateId, expectedRevision, enabled, reviews) {
|
|
303
|
+
// Turning off needs no review; the flags are only sent when turning on.
|
|
304
|
+
const body = enabled
|
|
305
|
+
? { expectedRevision, enabled, ...reviews }
|
|
306
|
+
: { expectedRevision, enabled };
|
|
307
|
+
return this.http
|
|
308
|
+
.put(this.url(`/templates/${templateId}/activation`), body, { headers: this.headers })
|
|
309
|
+
.pipe(map((response) => response.data));
|
|
310
|
+
}
|
|
311
|
+
restorePublication(templateId, expectedRevision, publicationRevision, reviews) {
|
|
312
|
+
return this.post(`/templates/${templateId}/restore`, {
|
|
313
|
+
expectedRevision,
|
|
314
|
+
publicationRevision,
|
|
315
|
+
...reviews,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
get(path) {
|
|
319
|
+
return this.http
|
|
320
|
+
.get(this.url(path), { headers: this.headers })
|
|
321
|
+
.pipe(map((response) => response.data));
|
|
322
|
+
}
|
|
323
|
+
post(path, body) {
|
|
324
|
+
return this.http
|
|
325
|
+
.post(this.url(path), body, { headers: this.headers })
|
|
326
|
+
.pipe(map((response) => response.data));
|
|
327
|
+
}
|
|
328
|
+
delete(path, expectedRevision) {
|
|
329
|
+
return this.http
|
|
330
|
+
.delete(this.url(path), {
|
|
331
|
+
headers: this.headers,
|
|
332
|
+
params: { expectedRevision },
|
|
333
|
+
})
|
|
334
|
+
.pipe(map((response) => response.data));
|
|
335
|
+
}
|
|
336
|
+
url(path) {
|
|
337
|
+
return `${this.baseUrl}${path}`;
|
|
338
|
+
}
|
|
339
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplateApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
340
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplateApi, providedIn: 'root' });
|
|
341
|
+
}
|
|
342
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplateApi, decorators: [{
|
|
343
|
+
type: Injectable,
|
|
344
|
+
args: [{ providedIn: 'root' }]
|
|
345
|
+
}] });
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Reads the stable code out of a failed notification request.
|
|
349
|
+
*
|
|
350
|
+
* Domain failures arrive as `errors: { code, message, details }`. Command
|
|
351
|
+
* endpoints can wrap the domain code in `errors.details.code` under an outer
|
|
352
|
+
* validation code (e.g. `VAL_002`), so the nested code wins when present.
|
|
353
|
+
* Display text is never matched — only codes.
|
|
354
|
+
*/
|
|
355
|
+
function readNotificationFailure(error) {
|
|
356
|
+
const status = error instanceof HttpErrorResponse ? error.status : null;
|
|
357
|
+
const body = error instanceof HttpErrorResponse ? error.error : error;
|
|
358
|
+
const errors = body?.errors;
|
|
359
|
+
const details = errors?.details;
|
|
360
|
+
const nestedCode = details && typeof details === 'object' && !Array.isArray(details)
|
|
361
|
+
? details.code
|
|
362
|
+
: undefined;
|
|
363
|
+
const code = typeof nestedCode === 'string'
|
|
364
|
+
? nestedCode
|
|
365
|
+
: typeof errors?.code === 'string'
|
|
366
|
+
? errors.code
|
|
367
|
+
: null;
|
|
368
|
+
return {
|
|
369
|
+
code,
|
|
370
|
+
status,
|
|
371
|
+
diagnostics: Array.isArray(details) ? toDiagnostics(details) : [],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function isRevisionConflict(failure) {
|
|
375
|
+
return (failure.status === 409 ||
|
|
376
|
+
failure.code === 'NOTIFICATION_REVISION_CONFLICT' ||
|
|
377
|
+
failure.code === 'EMAIL_REVISION_CONFLICT');
|
|
237
378
|
}
|
|
379
|
+
function toDiagnostics(values) {
|
|
380
|
+
return values.filter((value) => typeof value === 'object' &&
|
|
381
|
+
value !== null &&
|
|
382
|
+
typeof value.code === 'string');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function recipientKey(type, id) {
|
|
386
|
+
return `${type ?? ''}:${id ?? ''}`;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Flattens the recipient catalog into select-ready groups. A node is a
|
|
390
|
+
* subgroup only when its `items` is an array — leaves carry `items: null` —
|
|
391
|
+
* and subgroup names are kept as a prefix so two "Manager" roles from
|
|
392
|
+
* different levels stay distinguishable.
|
|
393
|
+
*/
|
|
394
|
+
function flattenRecipientCatalog(groups, lang) {
|
|
395
|
+
return groups
|
|
396
|
+
.map((group) => ({
|
|
397
|
+
groupName: group.groupName,
|
|
398
|
+
items: flattenNodes(group.items, lang),
|
|
399
|
+
}))
|
|
400
|
+
.filter((group) => group.items.length > 0);
|
|
401
|
+
}
|
|
402
|
+
function flattenNodes(nodes, lang, prefix) {
|
|
403
|
+
return (nodes ?? []).flatMap((node) => {
|
|
404
|
+
if (Array.isArray(node.items)) {
|
|
405
|
+
const label = node.subGroupName
|
|
406
|
+
? prefix
|
|
407
|
+
? `${prefix} - ${node.subGroupName}`
|
|
408
|
+
: node.subGroupName
|
|
409
|
+
: prefix;
|
|
410
|
+
return flattenNodes(node.items, lang, label);
|
|
411
|
+
}
|
|
412
|
+
if (!node.id || !node.type)
|
|
413
|
+
return [];
|
|
414
|
+
const name = (lang === 'ar' ? node.nameAr : node.nameEn) || node.name || node.id;
|
|
415
|
+
return [
|
|
416
|
+
{
|
|
417
|
+
key: recipientKey(node.type, node.id),
|
|
418
|
+
id: node.id,
|
|
419
|
+
type: node.type,
|
|
420
|
+
name: prefix ? `${prefix} - ${name}` : name,
|
|
421
|
+
},
|
|
422
|
+
];
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function templateStatus(template) {
|
|
426
|
+
if (template.publishedRevision == null)
|
|
427
|
+
return 'draft';
|
|
428
|
+
return template.isEnabled ? 'on' : 'off';
|
|
429
|
+
}
|
|
430
|
+
/** The retained publication that is live right now, if it is in the loaded history. */
|
|
431
|
+
function currentPublication(template, publications) {
|
|
432
|
+
if (template.publishedRevision == null)
|
|
433
|
+
return null;
|
|
434
|
+
return (publications.find((publication) => publication.revision === template.publishedRevision) ?? null);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Whether the saved draft differs from what is published. Comparing
|
|
438
|
+
* `draftRevision` to `publishedRevision` is wrong (they count different
|
|
439
|
+
* things); the contract compares the draft against the `sourceDraftRevision`
|
|
440
|
+
* of the current publication. Null when that cannot be known yet.
|
|
441
|
+
*/
|
|
442
|
+
function hasUnpublishedChanges(template, publications) {
|
|
443
|
+
if (template.publishedRevision == null)
|
|
444
|
+
return null;
|
|
445
|
+
const publication = currentPublication(template, publications);
|
|
446
|
+
if (!publication)
|
|
447
|
+
return null;
|
|
448
|
+
return template.draftRevision !== publication.sourceDraftRevision;
|
|
449
|
+
}
|
|
450
|
+
/** The default slot that owns this template, if any (owned defaults need a language review). */
|
|
451
|
+
function isDefaultOwned(template, defaults) {
|
|
452
|
+
return (defaults?.slots ?? []).some((slot) => slot.templateId === template.id);
|
|
453
|
+
}
|
|
454
|
+
/** Channels with a recipe but no slot at all — the only ones "Add defaults" may create. */
|
|
455
|
+
function missingDefaultChannels(defaults) {
|
|
456
|
+
if (!defaults)
|
|
457
|
+
return [];
|
|
458
|
+
const slotted = new Set(defaults.slots.map((slot) => slot.channel));
|
|
459
|
+
return defaults.recipes
|
|
460
|
+
.map((recipe) => recipe.channel)
|
|
461
|
+
.filter((channel) => !slotted.has(channel));
|
|
462
|
+
}
|
|
463
|
+
function parseAppContent(value) {
|
|
464
|
+
if (!value)
|
|
465
|
+
return { en: '', ar: '' };
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(value);
|
|
468
|
+
return {
|
|
469
|
+
en: typeof parsed?.en === 'string' ? parsed.en : '',
|
|
470
|
+
ar: typeof parsed?.ar === 'string' ? parsed.ar : '',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return { en: '', ar: '' };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Maps retained publication content onto the event preview request. */
|
|
478
|
+
function publicationPreviewRequest(content) {
|
|
479
|
+
return contentPreviewRequest({
|
|
480
|
+
channel: content.channel,
|
|
481
|
+
subject: content.subject,
|
|
482
|
+
emailTemplate: content.emailContent,
|
|
483
|
+
smsTemplate: content.smsContent,
|
|
484
|
+
appTemplate: parseAppContent(content.appContent),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
function templatePreviewRequest(template) {
|
|
488
|
+
return contentPreviewRequest(template);
|
|
489
|
+
}
|
|
490
|
+
function contentPreviewRequest(content) {
|
|
491
|
+
switch (content.channel) {
|
|
492
|
+
case 'Email':
|
|
493
|
+
return {
|
|
494
|
+
channel: 'Email',
|
|
495
|
+
subject: content.subject ?? '',
|
|
496
|
+
emailTemplate: content.emailTemplate ?? '',
|
|
497
|
+
};
|
|
498
|
+
case 'Sms':
|
|
499
|
+
return { channel: 'Sms', smsTemplate: content.smsTemplate ?? '' };
|
|
500
|
+
case 'App':
|
|
501
|
+
return {
|
|
502
|
+
channel: 'App',
|
|
503
|
+
appTemplate: {
|
|
504
|
+
en: content.appTemplate?.en ?? '',
|
|
505
|
+
ar: content.appTemplate?.ar ?? '',
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const PREVIEW_TOKEN = /\{\{\s*([A-Za-z0-9_.]+)\s*\}\}/g;
|
|
511
|
+
const MAX_PREVIEW_SAMPLES = 100;
|
|
512
|
+
/** Sample for the record link: resolved by the server against its client base URL. */
|
|
513
|
+
const SAMPLE_WEB_URL = '/';
|
|
514
|
+
/**
|
|
515
|
+
* Readable sample values for a preview: every field the content uses shows as
|
|
516
|
+
* its own name in brackets (`[Item name]`) instead of the server's "-", and the
|
|
517
|
+
* record link resolves. Only keys in the event's catalog are sent — the server
|
|
518
|
+
* rejects any other sample key — and never more than the server's limit.
|
|
519
|
+
*/
|
|
520
|
+
function previewSamples(request, properties, lang) {
|
|
521
|
+
const catalog = new Map(properties
|
|
522
|
+
.flatMap((group) => group.properties)
|
|
523
|
+
.map((property) => [property.key.toLowerCase(), property]));
|
|
524
|
+
const text = [
|
|
525
|
+
request.emailTemplate,
|
|
526
|
+
request.smsTemplate,
|
|
527
|
+
request.appTemplate?.en,
|
|
528
|
+
request.appTemplate?.ar,
|
|
529
|
+
]
|
|
530
|
+
.filter((value) => !!value)
|
|
531
|
+
.join(' ');
|
|
532
|
+
const samples = {};
|
|
533
|
+
for (const match of text.matchAll(PREVIEW_TOKEN)) {
|
|
534
|
+
if (Object.keys(samples).length >= MAX_PREVIEW_SAMPLES)
|
|
535
|
+
break;
|
|
536
|
+
const property = catalog.get(match[1].toLowerCase());
|
|
537
|
+
if (!property || property.key in samples)
|
|
538
|
+
continue;
|
|
539
|
+
if (property.key.toLowerCase() === 'weburl') {
|
|
540
|
+
samples[property.key] = SAMPLE_WEB_URL;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (property.key.toLowerCase() === 'mobileuri')
|
|
544
|
+
continue;
|
|
545
|
+
const label = (lang === 'ar' ? property.nameAr : property.nameEn) || property.name;
|
|
546
|
+
samples[property.key] = `[${label}]`;
|
|
547
|
+
}
|
|
548
|
+
return samples;
|
|
549
|
+
}
|
|
550
|
+
function withPreviewSamples(request, properties, lang) {
|
|
551
|
+
const samples = previewSamples(request, properties, lang);
|
|
552
|
+
return Object.keys(samples).length > 0 ? { ...request, samples } : request;
|
|
553
|
+
}
|
|
554
|
+
/** The system principal that publishes generated App defaults. */
|
|
555
|
+
const DEFAULTS_PUBLISHER = 'system:notification-defaults';
|
|
238
556
|
|
|
239
557
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
240
558
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -242,59 +560,51 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
242
560
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
243
561
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
244
562
|
};
|
|
245
|
-
// Default State
|
|
246
563
|
const DEFAULT_STATE = {
|
|
247
|
-
// Module configuration
|
|
248
564
|
moduleType: null,
|
|
249
565
|
moduleId: null,
|
|
250
566
|
parentModuleType: null,
|
|
251
567
|
parentModuleId: null,
|
|
252
568
|
parentPath: '',
|
|
253
|
-
// Action mode
|
|
254
|
-
isActionMode: false,
|
|
255
|
-
selectedActionId: null,
|
|
256
|
-
// Data collections
|
|
257
569
|
events: [],
|
|
258
|
-
templates: [],
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
selectedEventName: null,
|
|
570
|
+
templates: [],
|
|
571
|
+
context: null,
|
|
572
|
+
defaults: null,
|
|
573
|
+
publications: [],
|
|
574
|
+
publicationsTemplateId: null,
|
|
575
|
+
selectedEventId: null,
|
|
265
576
|
selectedTemplateId: null,
|
|
266
|
-
// Loading state (from LoadingStateShape)
|
|
267
577
|
loadingActive: [],
|
|
268
578
|
errors: {},
|
|
269
579
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
static getState(state) {
|
|
276
|
-
return state;
|
|
277
|
-
}
|
|
580
|
+
let NotificationState = class NotificationState {
|
|
581
|
+
api = inject(NotificationTemplateApi);
|
|
582
|
+
// ==========================================================================
|
|
583
|
+
// Slice selectors
|
|
584
|
+
// ==========================================================================
|
|
278
585
|
static getEvents(state) {
|
|
279
586
|
return state.events;
|
|
280
587
|
}
|
|
281
588
|
static getTemplates(state) {
|
|
282
589
|
return state.templates;
|
|
283
590
|
}
|
|
284
|
-
static
|
|
285
|
-
return state.
|
|
591
|
+
static getSelectedEventId(state) {
|
|
592
|
+
return state.selectedEventId;
|
|
286
593
|
}
|
|
287
|
-
static
|
|
288
|
-
return state.
|
|
594
|
+
static getSelectedTemplateId(state) {
|
|
595
|
+
return state.selectedTemplateId;
|
|
289
596
|
}
|
|
290
|
-
static
|
|
291
|
-
return state.
|
|
597
|
+
static getContext(state) {
|
|
598
|
+
return state.context;
|
|
292
599
|
}
|
|
293
|
-
static
|
|
294
|
-
return state.
|
|
600
|
+
static getDefaults(state) {
|
|
601
|
+
return state.defaults;
|
|
295
602
|
}
|
|
296
|
-
static
|
|
297
|
-
return
|
|
603
|
+
static getPublicationsSlice(state) {
|
|
604
|
+
return {
|
|
605
|
+
templateId: state.publicationsTemplateId,
|
|
606
|
+
items: state.publications,
|
|
607
|
+
};
|
|
298
608
|
}
|
|
299
609
|
static getLoadingActive(state) {
|
|
300
610
|
return state.loadingActive;
|
|
@@ -302,119 +612,26 @@ let NotificationState = class NotificationState extends CrudStateBase {
|
|
|
302
612
|
static getErrors(state) {
|
|
303
613
|
return state.errors;
|
|
304
614
|
}
|
|
615
|
+
// ==========================================================================
|
|
305
616
|
// Derived selectors
|
|
306
|
-
|
|
307
|
-
|
|
617
|
+
// ==========================================================================
|
|
618
|
+
static getSelectedEvent(events, selectedEventId) {
|
|
619
|
+
return events.find((event) => event.id === selectedEventId) ?? null;
|
|
308
620
|
}
|
|
309
|
-
static getEventTemplates(templates,
|
|
310
|
-
if (
|
|
621
|
+
static getEventTemplates(templates, selectedEventId) {
|
|
622
|
+
if (selectedEventId == null)
|
|
311
623
|
return [];
|
|
312
|
-
return templates.filter((
|
|
624
|
+
return templates.filter((template) => template.eventId === selectedEventId);
|
|
313
625
|
}
|
|
314
626
|
static getSelectedTemplate(templates, selectedTemplateId) {
|
|
315
|
-
return templates.find((
|
|
316
|
-
}
|
|
317
|
-
static getAvailableReceivers(receivers, selectedTemplate) {
|
|
318
|
-
if (!selectedTemplate)
|
|
319
|
-
return receivers;
|
|
320
|
-
const templateReceivers = selectedTemplate.receivers ?? [];
|
|
321
|
-
const assignedIds = new Set(templateReceivers.map((r) => r.identifier));
|
|
322
|
-
return receivers.filter((r) => !assignedIds.has(String(r.id)));
|
|
323
|
-
}
|
|
324
|
-
static getAvailableReceiverGroups(receiverGroups, selectedTemplate) {
|
|
325
|
-
if (!selectedTemplate)
|
|
326
|
-
return receiverGroups;
|
|
327
|
-
const assignedIds = new Set((selectedTemplate.receivers ?? []).map((r) => r.identifier));
|
|
328
|
-
return receiverGroups
|
|
329
|
-
.map((group) => ({
|
|
330
|
-
...group,
|
|
331
|
-
items: group.items.filter((item) => !assignedIds.has(String(item.id))),
|
|
332
|
-
}))
|
|
333
|
-
.filter((group) => group.items.length > 0);
|
|
334
|
-
}
|
|
335
|
-
// Helper Methods
|
|
336
|
-
/** API returns events with nested templates - we flatten for state */
|
|
337
|
-
flattenEventsResponse(eventsWithTemplates) {
|
|
338
|
-
const events = [];
|
|
339
|
-
const templates = [];
|
|
340
|
-
for (const event of eventsWithTemplates) {
|
|
341
|
-
const { templates: eventTemplates, ...eventData } = event;
|
|
342
|
-
events.push(eventData);
|
|
343
|
-
templates.push(...eventTemplates.map((t) => ({ ...t, eventId: event.id })));
|
|
344
|
-
}
|
|
345
|
-
return { events, templates };
|
|
346
|
-
}
|
|
347
|
-
normalizeReceiverGroups(groups) {
|
|
348
|
-
return groups
|
|
349
|
-
.map((group) => ({
|
|
350
|
-
groupName: group.groupName,
|
|
351
|
-
items: this.flattenReceiverItems(group.items),
|
|
352
|
-
}))
|
|
353
|
-
.filter((group) => group.items.length > 0);
|
|
354
|
-
}
|
|
355
|
-
flattenReceiverItems(items, parentLabel) {
|
|
356
|
-
if (!Array.isArray(items)) {
|
|
357
|
-
return [];
|
|
358
|
-
}
|
|
359
|
-
return items.flatMap((item) => {
|
|
360
|
-
const subGroupItems = item.items;
|
|
361
|
-
if (Array.isArray(subGroupItems)) {
|
|
362
|
-
const subGroupName = item.subGroupName;
|
|
363
|
-
const nextLabel = subGroupName
|
|
364
|
-
? parentLabel
|
|
365
|
-
? `${parentLabel} - ${subGroupName}`
|
|
366
|
-
: subGroupName
|
|
367
|
-
: parentLabel;
|
|
368
|
-
return this.flattenReceiverItems(subGroupItems, nextLabel);
|
|
369
|
-
}
|
|
370
|
-
const receiver = item;
|
|
371
|
-
if (receiver.id == null || !receiver.name) {
|
|
372
|
-
return [];
|
|
373
|
-
}
|
|
374
|
-
return [
|
|
375
|
-
{
|
|
376
|
-
...receiver,
|
|
377
|
-
name: parentLabel
|
|
378
|
-
? `${parentLabel} - ${receiver.name}`
|
|
379
|
-
: receiver.name,
|
|
380
|
-
},
|
|
381
|
-
];
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
hasOwnField(value, field) {
|
|
385
|
-
return Object.prototype.hasOwnProperty.call(value, field);
|
|
627
|
+
return (templates.find((template) => template.id === selectedTemplateId) ?? null);
|
|
386
628
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
...existingTemplate,
|
|
390
|
-
name: updatedTemplate.name ?? existingTemplate.name,
|
|
391
|
-
};
|
|
392
|
-
if (this.hasOwnField(updatedTemplate, 'emailTemplate')) {
|
|
393
|
-
mergedTemplate.emailTemplate = updatedTemplate.emailTemplate ?? '';
|
|
394
|
-
}
|
|
395
|
-
if (this.hasOwnField(updatedTemplate, 'emailContentTranslatable') &&
|
|
396
|
-
updatedTemplate.emailContentTranslatable) {
|
|
397
|
-
mergedTemplate.emailContentTranslatable = {
|
|
398
|
-
...existingTemplate.emailContentTranslatable,
|
|
399
|
-
...updatedTemplate.emailContentTranslatable,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
if (this.hasOwnField(updatedTemplate, 'smsTemplate')) {
|
|
403
|
-
mergedTemplate.smsTemplate = updatedTemplate.smsTemplate ?? '';
|
|
404
|
-
}
|
|
405
|
-
if (this.hasOwnField(updatedTemplate, 'appTemplate') &&
|
|
406
|
-
updatedTemplate.appTemplate) {
|
|
407
|
-
mergedTemplate.appTemplate = {
|
|
408
|
-
...existingTemplate.appTemplate,
|
|
409
|
-
...updatedTemplate.appTemplate,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
if (this.hasOwnField(updatedTemplate, 'conditionFormula')) {
|
|
413
|
-
mergedTemplate.conditionFormula = updatedTemplate.conditionFormula;
|
|
414
|
-
}
|
|
415
|
-
return mergedTemplate;
|
|
629
|
+
static getPublications(slice, selectedTemplateId) {
|
|
630
|
+
return slice.templateId === selectedTemplateId ? slice.items : [];
|
|
416
631
|
}
|
|
417
|
-
//
|
|
632
|
+
// ==========================================================================
|
|
633
|
+
// Scope
|
|
634
|
+
// ==========================================================================
|
|
418
635
|
setModuleInfo(ctx, action) {
|
|
419
636
|
let parentPath = '';
|
|
420
637
|
if (action.parentModuleType && action.parentModuleId) {
|
|
@@ -429,218 +646,273 @@ let NotificationState = class NotificationState extends CrudStateBase {
|
|
|
429
646
|
parentModuleType: action.parentModuleType ?? null,
|
|
430
647
|
parentModuleId: action.parentModuleId ?? null,
|
|
431
648
|
parentPath,
|
|
432
|
-
isActionMode: action.moduleType === 'workflow',
|
|
433
649
|
});
|
|
434
650
|
}
|
|
435
651
|
resetState(ctx) {
|
|
436
652
|
ctx.setState(DEFAULT_STATE);
|
|
437
653
|
}
|
|
438
|
-
//
|
|
654
|
+
// ==========================================================================
|
|
655
|
+
// Loading
|
|
656
|
+
// ==========================================================================
|
|
439
657
|
getEvents(ctx) {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
request$: req$,
|
|
445
|
-
updateState: (_state, data) => {
|
|
446
|
-
const { events, templates } = this.flattenEventsResponse(data ?? []);
|
|
447
|
-
const firstEvent = events[0];
|
|
448
|
-
const firstTemplate = templates.find((t) => t.eventId === firstEvent?.id);
|
|
449
|
-
return {
|
|
450
|
-
events,
|
|
451
|
-
templates,
|
|
452
|
-
selectedEventName: firstEvent?.eventName ?? null,
|
|
453
|
-
selectedTemplateId: firstTemplate?.id ?? null,
|
|
454
|
-
};
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
getReceivers(ctx) {
|
|
459
|
-
const { parentPath, moduleType, moduleId } = ctx.getState();
|
|
460
|
-
const req$ = this.http.get(`${this.baseUrl}${parentPath}/${moduleType}/${moduleId}/receivers`);
|
|
658
|
+
const scope = scopePathOf(ctx.getState());
|
|
659
|
+
if (!scope)
|
|
660
|
+
return;
|
|
661
|
+
const previousEventId = ctx.getState().selectedEventId;
|
|
461
662
|
return handleApiRequest({
|
|
462
663
|
ctx,
|
|
463
|
-
key: NotificationActionKey.
|
|
464
|
-
request$:
|
|
465
|
-
onSuccess: (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
664
|
+
key: NotificationActionKey.GetEvents,
|
|
665
|
+
request$: this.api.getEvents(scope),
|
|
666
|
+
onSuccess: (data, state) => {
|
|
667
|
+
// A late answer for a scope the host already left must not land.
|
|
668
|
+
if (scopePathOf(state) !== scope)
|
|
669
|
+
return;
|
|
670
|
+
const { events, templates } = flattenEvents(data ?? []);
|
|
671
|
+
const selectedEventId = events.some((event) => event.id === state.selectedEventId)
|
|
672
|
+
? state.selectedEventId
|
|
673
|
+
: (events[0]?.id ?? null);
|
|
674
|
+
const eventTemplates = templates.filter((template) => template.eventId === selectedEventId);
|
|
675
|
+
const selectedTemplateId = eventTemplates.some((template) => template.id === state.selectedTemplateId)
|
|
676
|
+
? state.selectedTemplateId
|
|
677
|
+
: (eventTemplates[0]?.id ?? null);
|
|
678
|
+
return { events, templates, selectedEventId, selectedTemplateId };
|
|
471
679
|
},
|
|
472
|
-
})
|
|
680
|
+
}).pipe(tap(() => {
|
|
681
|
+
const state = ctx.getState();
|
|
682
|
+
if (scopePathOf(state) !== scope)
|
|
683
|
+
return;
|
|
684
|
+
const eventId = state.selectedEventId;
|
|
685
|
+
if (eventId != null &&
|
|
686
|
+
(eventId !== previousEventId || state.context?.eventId !== eventId)) {
|
|
687
|
+
ctx.patchState({ context: null, defaults: null });
|
|
688
|
+
ctx.dispatch([
|
|
689
|
+
new GetAuthoringContext(eventId),
|
|
690
|
+
new GetDefaultRecipes(eventId),
|
|
691
|
+
]);
|
|
692
|
+
}
|
|
693
|
+
if (state.selectedTemplateId != null) {
|
|
694
|
+
ctx.dispatch(new GetPublications(state.selectedTemplateId));
|
|
695
|
+
}
|
|
696
|
+
}));
|
|
473
697
|
}
|
|
474
|
-
|
|
475
|
-
const { moduleType, moduleId, parentPath } = ctx.getState();
|
|
476
|
-
const url = `${this.baseUrl}${parentPath}/${moduleType}/${moduleId}/properties?IsMultiLanguageEnabled=${action.isMultiLang ? 'true' : 'false'}`;
|
|
477
|
-
const req$ = this.http.get(url);
|
|
698
|
+
getAuthoringContext(ctx, action) {
|
|
478
699
|
return handleApiRequest({
|
|
479
700
|
ctx,
|
|
480
|
-
key: NotificationActionKey.
|
|
481
|
-
request$:
|
|
482
|
-
onSuccess: (
|
|
483
|
-
propertyGroups: response.data?.map((group) => ({
|
|
484
|
-
...group,
|
|
485
|
-
icon: 'custom.list-cards',
|
|
486
|
-
})) ?? [],
|
|
487
|
-
}),
|
|
701
|
+
key: NotificationActionKey.GetContext,
|
|
702
|
+
request$: this.api.getContext(action.eventId),
|
|
703
|
+
onSuccess: (context, state) => state.selectedEventId === action.eventId ? { context } : undefined,
|
|
488
704
|
});
|
|
489
705
|
}
|
|
490
|
-
|
|
491
|
-
setActionMode(ctx, action) {
|
|
492
|
-
ctx.patchState({
|
|
493
|
-
selectedActionId: action.actionId,
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
getActionReceivers(ctx, action) {
|
|
497
|
-
const req$ = this.http.get(`${this.baseUrl}/action/${action.actionId}/receivers`);
|
|
706
|
+
getDefaultRecipes(ctx, action) {
|
|
498
707
|
return handleApiRequest({
|
|
499
708
|
ctx,
|
|
500
|
-
key: NotificationActionKey.
|
|
501
|
-
request$:
|
|
502
|
-
onSuccess: (
|
|
503
|
-
const receiverGroups = this.normalizeReceiverGroups(response.data ?? []);
|
|
504
|
-
return {
|
|
505
|
-
receiverGroups,
|
|
506
|
-
receivers: receiverGroups.flatMap((group) => group.items),
|
|
507
|
-
};
|
|
508
|
-
},
|
|
709
|
+
key: NotificationActionKey.GetDefaults,
|
|
710
|
+
request$: this.api.getDefaults(action.eventId),
|
|
711
|
+
onSuccess: (defaults, state) => state.selectedEventId === action.eventId ? { defaults } : undefined,
|
|
509
712
|
});
|
|
510
713
|
}
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
714
|
+
getPublications(ctx, action) {
|
|
715
|
+
const template = ctx
|
|
716
|
+
.getState()
|
|
717
|
+
.templates.find((item) => item.id === action.templateId);
|
|
718
|
+
// A template that was never published has no history to read.
|
|
719
|
+
if (template?.publishedRevision == null) {
|
|
720
|
+
ctx.patchState({
|
|
721
|
+
publications: [],
|
|
722
|
+
publicationsTemplateId: action.templateId,
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
514
726
|
return handleApiRequest({
|
|
515
727
|
ctx,
|
|
516
|
-
key: NotificationActionKey.
|
|
517
|
-
request$:
|
|
518
|
-
onSuccess: (
|
|
519
|
-
|
|
520
|
-
|
|
728
|
+
key: NotificationActionKey.GetPublications,
|
|
729
|
+
request$: this.api.getPublications(action.templateId),
|
|
730
|
+
onSuccess: (publications, state) => state.selectedTemplateId === action.templateId
|
|
731
|
+
? { publications, publicationsTemplateId: action.templateId }
|
|
732
|
+
: undefined,
|
|
521
733
|
});
|
|
522
734
|
}
|
|
523
|
-
//
|
|
735
|
+
// ==========================================================================
|
|
736
|
+
// Selection
|
|
737
|
+
// ==========================================================================
|
|
524
738
|
selectEvent(ctx, action) {
|
|
525
739
|
const state = ctx.getState();
|
|
526
|
-
|
|
527
|
-
|
|
740
|
+
if (state.selectedEventId === action.eventId)
|
|
741
|
+
return;
|
|
742
|
+
const firstTemplate = state.templates.find((template) => template.eventId === action.eventId);
|
|
528
743
|
ctx.patchState({
|
|
529
|
-
|
|
530
|
-
selectedTemplateId:
|
|
744
|
+
selectedEventId: action.eventId,
|
|
745
|
+
selectedTemplateId: firstTemplate?.id ?? null,
|
|
746
|
+
context: null,
|
|
747
|
+
defaults: null,
|
|
748
|
+
publications: [],
|
|
749
|
+
publicationsTemplateId: null,
|
|
531
750
|
});
|
|
751
|
+
const loads = [
|
|
752
|
+
new GetAuthoringContext(action.eventId),
|
|
753
|
+
new GetDefaultRecipes(action.eventId),
|
|
754
|
+
];
|
|
755
|
+
if (firstTemplate)
|
|
756
|
+
loads.push(new GetPublications(firstTemplate.id));
|
|
757
|
+
return ctx.dispatch(loads);
|
|
532
758
|
}
|
|
533
759
|
selectTemplate(ctx, action) {
|
|
534
|
-
ctx.
|
|
760
|
+
if (ctx.getState().selectedTemplateId === action.templateId)
|
|
761
|
+
return;
|
|
762
|
+
ctx.patchState({
|
|
763
|
+
selectedTemplateId: action.templateId,
|
|
764
|
+
publications: [],
|
|
765
|
+
publicationsTemplateId: null,
|
|
766
|
+
});
|
|
767
|
+
return ctx.dispatch(new GetPublications(action.templateId));
|
|
535
768
|
}
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
769
|
+
// ==========================================================================
|
|
770
|
+
// Template writes — every one returns the whole template DTO, which becomes
|
|
771
|
+
// the new server baseline (its `revision` is the next expectedRevision).
|
|
772
|
+
// ==========================================================================
|
|
773
|
+
createTemplate(ctx, action) {
|
|
539
774
|
return handleApiRequest({
|
|
540
775
|
ctx,
|
|
541
|
-
key: NotificationActionKey.
|
|
542
|
-
request$:
|
|
543
|
-
onSuccess: (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
},
|
|
776
|
+
key: NotificationActionKey.CreateTemplate,
|
|
777
|
+
request$: this.api.createTemplate(action.eventId, action.payload),
|
|
778
|
+
onSuccess: (template, state) => ({
|
|
779
|
+
templates: upsertTemplate(state.templates, template),
|
|
780
|
+
...(state.selectedEventId === action.eventId
|
|
781
|
+
? {
|
|
782
|
+
selectedTemplateId: template.id,
|
|
783
|
+
publications: [],
|
|
784
|
+
publicationsTemplateId: template.id,
|
|
785
|
+
}
|
|
786
|
+
: {}),
|
|
787
|
+
}),
|
|
554
788
|
});
|
|
555
789
|
}
|
|
556
|
-
|
|
557
|
-
const state = ctx.getState();
|
|
558
|
-
const template = this.getSelectedTemplate(state);
|
|
559
|
-
if (!template) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
const req$ = this.http.delete(`${this.baseUrl}/receivers/${action.receiverId}`);
|
|
790
|
+
saveTemplate(ctx, action) {
|
|
563
791
|
return handleApiRequest({
|
|
564
792
|
ctx,
|
|
565
|
-
key: NotificationActionKey.
|
|
566
|
-
request$:
|
|
567
|
-
onSuccess: (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}, 'id');
|
|
571
|
-
return { templates };
|
|
572
|
-
},
|
|
793
|
+
key: NotificationActionKey.SaveTemplate,
|
|
794
|
+
request$: this.api.saveTemplate(action.templateId, action.payload),
|
|
795
|
+
onSuccess: (template, state) => ({
|
|
796
|
+
templates: upsertTemplate(state.templates, template),
|
|
797
|
+
}),
|
|
573
798
|
});
|
|
574
799
|
}
|
|
575
|
-
|
|
576
|
-
addTemplate(ctx, action) {
|
|
577
|
-
const req$ = this.http.post(`${this.baseUrl}/events/${action.eventId}`, action.payload);
|
|
800
|
+
addReceivers(ctx, action) {
|
|
578
801
|
return handleApiRequest({
|
|
579
802
|
ctx,
|
|
580
|
-
key: NotificationActionKey.
|
|
581
|
-
request$:
|
|
582
|
-
onSuccess: (
|
|
583
|
-
|
|
584
|
-
if (!newTemplate)
|
|
585
|
-
return {};
|
|
586
|
-
return {
|
|
587
|
-
templates: this.adapter.addOne(state.templates, {
|
|
588
|
-
...newTemplate,
|
|
589
|
-
eventId: action.eventId,
|
|
590
|
-
}),
|
|
591
|
-
selectedTemplateId: newTemplate.id,
|
|
592
|
-
};
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
updateTemplate(ctx, action) {
|
|
597
|
-
const req$ = this.http.put(`${this.baseUrl}/templates/${action.templateId}`, action.payload, {
|
|
598
|
-
headers: new HttpHeaders({
|
|
599
|
-
'Content-Type': 'application/json',
|
|
803
|
+
key: NotificationActionKey.AddReceivers,
|
|
804
|
+
request$: this.api.addReceivers(action.templateId, action.expectedRevision, action.receivers),
|
|
805
|
+
onSuccess: (template, state) => ({
|
|
806
|
+
templates: upsertTemplate(state.templates, template),
|
|
600
807
|
}),
|
|
601
808
|
});
|
|
809
|
+
}
|
|
810
|
+
deleteReceiver(ctx, action) {
|
|
602
811
|
return handleApiRequest({
|
|
603
812
|
ctx,
|
|
604
|
-
key: NotificationActionKey.
|
|
605
|
-
request$:
|
|
606
|
-
onSuccess: (
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return {};
|
|
610
|
-
// Preserve existing fields that API may not return or shouldn't change
|
|
611
|
-
const existingTemplate = state.templates.find((t) => t.id === action.templateId);
|
|
612
|
-
if (!existingTemplate)
|
|
613
|
-
return {};
|
|
614
|
-
// Only update name and content fields, preserve channel/eventId/receivers
|
|
615
|
-
const templates = this.adapter.upsertOne(state.templates, this.mergeUpdatedTemplate(existingTemplate, updatedTemplate), 'id');
|
|
616
|
-
return { templates };
|
|
617
|
-
},
|
|
813
|
+
key: NotificationActionKey.DeleteReceiver,
|
|
814
|
+
request$: this.api.deleteReceiver(action.receiverId, action.expectedRevision),
|
|
815
|
+
onSuccess: (template, state) => ({
|
|
816
|
+
templates: upsertTemplate(state.templates, template),
|
|
817
|
+
}),
|
|
618
818
|
});
|
|
619
819
|
}
|
|
620
820
|
deleteTemplate(ctx, action) {
|
|
621
|
-
const
|
|
821
|
+
const eventId = ctx
|
|
822
|
+
.getState()
|
|
823
|
+
.templates.find((template) => template.id === action.templateId)?.eventId;
|
|
622
824
|
return handleApiRequest({
|
|
623
825
|
ctx,
|
|
624
826
|
key: NotificationActionKey.DeleteTemplate,
|
|
625
|
-
request$:
|
|
827
|
+
request$: this.api.deleteTemplate(action.templateId, action.expectedRevision),
|
|
626
828
|
onSuccess: (_, state) => {
|
|
627
|
-
const templates =
|
|
628
|
-
|
|
629
|
-
const currentEvent = state.events.find((e) => e.eventName === state.selectedEventName);
|
|
630
|
-
const eventTemplates = templates.filter((t) => t.eventId === currentEvent?.id);
|
|
631
|
-
const systemTemplate = eventTemplates.find((t) => t.isSystem);
|
|
829
|
+
const templates = state.templates.filter((template) => template.id !== action.templateId);
|
|
830
|
+
const next = templates.find((template) => template.eventId === state.selectedEventId);
|
|
632
831
|
return {
|
|
633
832
|
templates,
|
|
634
|
-
selectedTemplateId:
|
|
833
|
+
selectedTemplateId: state.selectedTemplateId === action.templateId
|
|
834
|
+
? (next?.id ?? null)
|
|
835
|
+
: state.selectedTemplateId,
|
|
836
|
+
publications: [],
|
|
837
|
+
publicationsTemplateId: null,
|
|
635
838
|
};
|
|
636
839
|
},
|
|
840
|
+
}).pipe(tap(() => {
|
|
841
|
+
// Deleting a default leaves its slot behind as "intentionally removed".
|
|
842
|
+
if (eventId != null)
|
|
843
|
+
ctx.dispatch(new GetDefaultRecipes(eventId));
|
|
844
|
+
const selected = ctx.getState().selectedTemplateId;
|
|
845
|
+
if (selected != null)
|
|
846
|
+
ctx.dispatch(new GetPublications(selected));
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
// ==========================================================================
|
|
850
|
+
// Lifecycle
|
|
851
|
+
// ==========================================================================
|
|
852
|
+
publishTemplate(ctx, action) {
|
|
853
|
+
return handleApiRequest({
|
|
854
|
+
ctx,
|
|
855
|
+
key: NotificationActionKey.PublishTemplate,
|
|
856
|
+
request$: this.api.publish(action.templateId, action.expectedRevision, action.reviews),
|
|
857
|
+
onSuccess: (template, state) => ({
|
|
858
|
+
templates: upsertTemplate(state.templates, template),
|
|
859
|
+
}),
|
|
860
|
+
}).pipe(tap(() => ctx.dispatch(new GetPublications(action.templateId))));
|
|
861
|
+
}
|
|
862
|
+
setTemplateActivation(ctx, action) {
|
|
863
|
+
return handleApiRequest({
|
|
864
|
+
ctx,
|
|
865
|
+
key: NotificationActionKey.SetActivation,
|
|
866
|
+
request$: this.api.setActivation(action.templateId, action.expectedRevision, action.enabled, action.reviews),
|
|
867
|
+
onSuccess: (template, state) => ({
|
|
868
|
+
templates: upsertTemplate(state.templates, template),
|
|
869
|
+
}),
|
|
637
870
|
});
|
|
638
871
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
872
|
+
restorePublication(ctx, action) {
|
|
873
|
+
return handleApiRequest({
|
|
874
|
+
ctx,
|
|
875
|
+
key: NotificationActionKey.RestorePublication,
|
|
876
|
+
request$: this.api.restorePublication(action.templateId, action.expectedRevision, action.publicationRevision, action.reviews),
|
|
877
|
+
onSuccess: (template, state) => ({
|
|
878
|
+
templates: upsertTemplate(state.templates, template),
|
|
879
|
+
}),
|
|
880
|
+
}).pipe(tap(() => ctx.dispatch(new GetPublications(action.templateId))));
|
|
642
881
|
}
|
|
643
|
-
|
|
882
|
+
// ==========================================================================
|
|
883
|
+
// Defaults
|
|
884
|
+
// ==========================================================================
|
|
885
|
+
initializeDefaults(ctx, action) {
|
|
886
|
+
return handleApiRequest({
|
|
887
|
+
ctx,
|
|
888
|
+
key: NotificationActionKey.InitializeDefaults,
|
|
889
|
+
request$: this.api.initializeDefaults(action.eventId),
|
|
890
|
+
onSuccess: (result, state) => state.selectedEventId === action.eventId
|
|
891
|
+
? { defaults: result.defaults }
|
|
892
|
+
: undefined,
|
|
893
|
+
}).pipe(
|
|
894
|
+
// `createdTemplates` omits a template whose audit projection failed even
|
|
895
|
+
// though it was committed, so the event list is re-read instead.
|
|
896
|
+
tap(() => ctx.dispatch(new GetEvents())));
|
|
897
|
+
}
|
|
898
|
+
restoreDefault(ctx, action) {
|
|
899
|
+
return handleApiRequest({
|
|
900
|
+
ctx,
|
|
901
|
+
key: NotificationActionKey.RestoreDefault,
|
|
902
|
+
request$: this.api.restoreDefault(action.eventId, action.payload),
|
|
903
|
+
onSuccess: (template, state) => ({
|
|
904
|
+
templates: upsertTemplate(state.templates, template),
|
|
905
|
+
...(state.selectedEventId === action.eventId
|
|
906
|
+
? {
|
|
907
|
+
selectedTemplateId: template.id,
|
|
908
|
+
publications: [],
|
|
909
|
+
publicationsTemplateId: template.id,
|
|
910
|
+
}
|
|
911
|
+
: {}),
|
|
912
|
+
}),
|
|
913
|
+
}).pipe(tap(() => ctx.dispatch(new GetDefaultRecipes(action.eventId))));
|
|
914
|
+
}
|
|
915
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
644
916
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState });
|
|
645
917
|
};
|
|
646
918
|
__decorate([
|
|
@@ -650,47 +922,53 @@ __decorate([
|
|
|
650
922
|
Action(ResetNotificationState)
|
|
651
923
|
], NotificationState.prototype, "resetState", null);
|
|
652
924
|
__decorate([
|
|
653
|
-
Action(GetEvents)
|
|
925
|
+
Action(GetEvents, { cancelUncompleted: true })
|
|
654
926
|
], NotificationState.prototype, "getEvents", null);
|
|
655
927
|
__decorate([
|
|
656
|
-
Action(
|
|
657
|
-
], NotificationState.prototype, "
|
|
658
|
-
__decorate([
|
|
659
|
-
Action(GetProperties)
|
|
660
|
-
], NotificationState.prototype, "getProperties", null);
|
|
928
|
+
Action(GetAuthoringContext, { cancelUncompleted: true })
|
|
929
|
+
], NotificationState.prototype, "getAuthoringContext", null);
|
|
661
930
|
__decorate([
|
|
662
|
-
Action(
|
|
663
|
-
], NotificationState.prototype, "
|
|
931
|
+
Action(GetDefaultRecipes, { cancelUncompleted: true })
|
|
932
|
+
], NotificationState.prototype, "getDefaultRecipes", null);
|
|
664
933
|
__decorate([
|
|
665
|
-
Action(
|
|
666
|
-
], NotificationState.prototype, "
|
|
667
|
-
__decorate([
|
|
668
|
-
Action(GetActionProperties)
|
|
669
|
-
], NotificationState.prototype, "getActionProperties", null);
|
|
934
|
+
Action(GetPublications, { cancelUncompleted: true })
|
|
935
|
+
], NotificationState.prototype, "getPublications", null);
|
|
670
936
|
__decorate([
|
|
671
937
|
Action(SelectEvent)
|
|
672
938
|
], NotificationState.prototype, "selectEvent", null);
|
|
673
939
|
__decorate([
|
|
674
940
|
Action(SelectTemplate)
|
|
675
941
|
], NotificationState.prototype, "selectTemplate", null);
|
|
942
|
+
__decorate([
|
|
943
|
+
Action(CreateTemplate)
|
|
944
|
+
], NotificationState.prototype, "createTemplate", null);
|
|
945
|
+
__decorate([
|
|
946
|
+
Action(SaveTemplate)
|
|
947
|
+
], NotificationState.prototype, "saveTemplate", null);
|
|
676
948
|
__decorate([
|
|
677
949
|
Action(AddReceivers)
|
|
678
950
|
], NotificationState.prototype, "addReceivers", null);
|
|
679
951
|
__decorate([
|
|
680
952
|
Action(DeleteReceiver)
|
|
681
953
|
], NotificationState.prototype, "deleteReceiver", null);
|
|
682
|
-
__decorate([
|
|
683
|
-
Action(AddTemplate)
|
|
684
|
-
], NotificationState.prototype, "addTemplate", null);
|
|
685
|
-
__decorate([
|
|
686
|
-
Action(UpdateTemplate)
|
|
687
|
-
], NotificationState.prototype, "updateTemplate", null);
|
|
688
954
|
__decorate([
|
|
689
955
|
Action(DeleteTemplate)
|
|
690
956
|
], NotificationState.prototype, "deleteTemplate", null);
|
|
691
957
|
__decorate([
|
|
692
|
-
|
|
693
|
-
], NotificationState, "
|
|
958
|
+
Action(PublishTemplate)
|
|
959
|
+
], NotificationState.prototype, "publishTemplate", null);
|
|
960
|
+
__decorate([
|
|
961
|
+
Action(SetTemplateActivation)
|
|
962
|
+
], NotificationState.prototype, "setTemplateActivation", null);
|
|
963
|
+
__decorate([
|
|
964
|
+
Action(RestorePublication)
|
|
965
|
+
], NotificationState.prototype, "restorePublication", null);
|
|
966
|
+
__decorate([
|
|
967
|
+
Action(InitializeDefaults)
|
|
968
|
+
], NotificationState.prototype, "initializeDefaults", null);
|
|
969
|
+
__decorate([
|
|
970
|
+
Action(RestoreDefault)
|
|
971
|
+
], NotificationState.prototype, "restoreDefault", null);
|
|
694
972
|
__decorate([
|
|
695
973
|
Selector()
|
|
696
974
|
], NotificationState, "getEvents", null);
|
|
@@ -699,19 +977,19 @@ __decorate([
|
|
|
699
977
|
], NotificationState, "getTemplates", null);
|
|
700
978
|
__decorate([
|
|
701
979
|
Selector()
|
|
702
|
-
], NotificationState, "
|
|
980
|
+
], NotificationState, "getSelectedEventId", null);
|
|
703
981
|
__decorate([
|
|
704
982
|
Selector()
|
|
705
|
-
], NotificationState, "
|
|
983
|
+
], NotificationState, "getSelectedTemplateId", null);
|
|
706
984
|
__decorate([
|
|
707
985
|
Selector()
|
|
708
|
-
], NotificationState, "
|
|
986
|
+
], NotificationState, "getContext", null);
|
|
709
987
|
__decorate([
|
|
710
988
|
Selector()
|
|
711
|
-
], NotificationState, "
|
|
989
|
+
], NotificationState, "getDefaults", null);
|
|
712
990
|
__decorate([
|
|
713
991
|
Selector()
|
|
714
|
-
], NotificationState, "
|
|
992
|
+
], NotificationState, "getPublicationsSlice", null);
|
|
715
993
|
__decorate([
|
|
716
994
|
Selector()
|
|
717
995
|
], NotificationState, "getLoadingActive", null);
|
|
@@ -719,15 +997,12 @@ __decorate([
|
|
|
719
997
|
Selector()
|
|
720
998
|
], NotificationState, "getErrors", null);
|
|
721
999
|
__decorate([
|
|
722
|
-
Selector([
|
|
723
|
-
NotificationState.getEvents,
|
|
724
|
-
NotificationState.getSelectedEventName,
|
|
725
|
-
])
|
|
1000
|
+
Selector([NotificationState.getEvents, NotificationState.getSelectedEventId])
|
|
726
1001
|
], NotificationState, "getSelectedEvent", null);
|
|
727
1002
|
__decorate([
|
|
728
1003
|
Selector([
|
|
729
1004
|
NotificationState.getTemplates,
|
|
730
|
-
NotificationState.
|
|
1005
|
+
NotificationState.getSelectedEventId,
|
|
731
1006
|
])
|
|
732
1007
|
], NotificationState, "getEventTemplates", null);
|
|
733
1008
|
__decorate([
|
|
@@ -738,16 +1013,10 @@ __decorate([
|
|
|
738
1013
|
], NotificationState, "getSelectedTemplate", null);
|
|
739
1014
|
__decorate([
|
|
740
1015
|
Selector([
|
|
741
|
-
NotificationState.
|
|
742
|
-
NotificationState.
|
|
743
|
-
])
|
|
744
|
-
], NotificationState, "getAvailableReceivers", null);
|
|
745
|
-
__decorate([
|
|
746
|
-
Selector([
|
|
747
|
-
NotificationState.getReceiverGroups,
|
|
748
|
-
NotificationState.getSelectedTemplate,
|
|
1016
|
+
NotificationState.getPublicationsSlice,
|
|
1017
|
+
NotificationState.getSelectedTemplateId,
|
|
749
1018
|
])
|
|
750
|
-
], NotificationState, "
|
|
1019
|
+
], NotificationState, "getPublications", null);
|
|
751
1020
|
NotificationState = __decorate([
|
|
752
1021
|
State({
|
|
753
1022
|
name: 'notification',
|
|
@@ -756,48 +1025,71 @@ NotificationState = __decorate([
|
|
|
756
1025
|
], NotificationState);
|
|
757
1026
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState, decorators: [{
|
|
758
1027
|
type: Injectable
|
|
759
|
-
}], propDecorators: { setModuleInfo: [], resetState: [], getEvents: [],
|
|
1028
|
+
}], propDecorators: { setModuleInfo: [], resetState: [], getEvents: [], getAuthoringContext: [], getDefaultRecipes: [], getPublications: [], selectEvent: [], selectTemplate: [], createTemplate: [], saveTemplate: [], addReceivers: [], deleteReceiver: [], deleteTemplate: [], publishTemplate: [], setTemplateActivation: [], restorePublication: [], initializeDefaults: [], restoreDefault: [] } });
|
|
1029
|
+
function scopePathOf(state) {
|
|
1030
|
+
if (!state.moduleType || state.moduleId == null || state.moduleId === '') {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
return `${state.parentPath}/${state.moduleType}/${state.moduleId}`;
|
|
1034
|
+
}
|
|
1035
|
+
function flattenEvents(data) {
|
|
1036
|
+
const events = [];
|
|
1037
|
+
const templates = [];
|
|
1038
|
+
for (const { templates: eventTemplates, ...event } of data) {
|
|
1039
|
+
events.push(event);
|
|
1040
|
+
templates.push(...(eventTemplates ?? []).map((template) => ({
|
|
1041
|
+
...template,
|
|
1042
|
+
eventId: event.id,
|
|
1043
|
+
})));
|
|
1044
|
+
}
|
|
1045
|
+
return { events, templates };
|
|
1046
|
+
}
|
|
1047
|
+
function upsertTemplate(templates, template) {
|
|
1048
|
+
const index = templates.findIndex((item) => item.id === template.id);
|
|
1049
|
+
if (index === -1)
|
|
1050
|
+
return [...templates, template];
|
|
1051
|
+
const next = [...templates];
|
|
1052
|
+
next[index] = template;
|
|
1053
|
+
return next;
|
|
1054
|
+
}
|
|
760
1055
|
|
|
761
1056
|
class NotificationFacade {
|
|
762
1057
|
store = inject(Store);
|
|
763
|
-
|
|
764
|
-
//
|
|
765
|
-
//
|
|
1058
|
+
api = inject(NotificationTemplateApi);
|
|
1059
|
+
// ==========================================================================
|
|
1060
|
+
// Data
|
|
1061
|
+
// ==========================================================================
|
|
766
1062
|
events = select(NotificationState.getEvents);
|
|
1063
|
+
/** Templates of the selected event. */
|
|
767
1064
|
templates = select(NotificationState.getEventTemplates);
|
|
768
|
-
|
|
769
|
-
receiverGroups = select(NotificationState.getReceiverGroups);
|
|
770
|
-
propertyGroups = select(NotificationState.getPropertyGroups);
|
|
771
|
-
// Selection selectors
|
|
772
|
-
selectedEventName = select(NotificationState.getSelectedEventName);
|
|
1065
|
+
selectedEventId = select(NotificationState.getSelectedEventId);
|
|
773
1066
|
selectedTemplateId = select(NotificationState.getSelectedTemplateId);
|
|
774
1067
|
selectedEvent = select(NotificationState.getSelectedEvent);
|
|
775
1068
|
selectedTemplate = select(NotificationState.getSelectedTemplate);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
//
|
|
1069
|
+
context = select(NotificationState.getContext);
|
|
1070
|
+
defaults = select(NotificationState.getDefaults);
|
|
1071
|
+
publications = select(NotificationState.getPublications);
|
|
1072
|
+
// ==========================================================================
|
|
1073
|
+
// Loading / errors
|
|
1074
|
+
// ==========================================================================
|
|
781
1075
|
loadingActive = select(NotificationState.getLoadingActive);
|
|
782
1076
|
errors = select(NotificationState.getErrors);
|
|
783
|
-
// ============================================================================
|
|
784
|
-
// Loading Signals - Computed from slice (minimal reactivity)
|
|
785
|
-
// ============================================================================
|
|
786
1077
|
isLoadingEvents = computed(() => this.loadingActive().includes(NotificationActionKey.GetEvents), ...(ngDevMode ? [{ debugName: "isLoadingEvents" }] : /* istanbul ignore next */ []));
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
isUpdatingTemplate = computed(() => this.loadingActive().includes(NotificationActionKey.UpdateTemplate), ...(ngDevMode ? [{ debugName: "isUpdatingTemplate" }] : /* istanbul ignore next */ []));
|
|
793
|
-
isDeletingTemplate = computed(() => this.loadingActive().includes(NotificationActionKey.DeleteTemplate), ...(ngDevMode ? [{ debugName: "isDeletingTemplate" }] : /* istanbul ignore next */ []));
|
|
794
|
-
// ============================================================================
|
|
795
|
-
// Error Signals - Computed from slice (minimal reactivity)
|
|
796
|
-
// ============================================================================
|
|
1078
|
+
isLoadingContext = computed(() => this.loadingActive().includes(NotificationActionKey.GetContext), ...(ngDevMode ? [{ debugName: "isLoadingContext" }] : /* istanbul ignore next */ []));
|
|
1079
|
+
isLoadingDefaults = computed(() => this.loadingActive().includes(NotificationActionKey.GetDefaults), ...(ngDevMode ? [{ debugName: "isLoadingDefaults" }] : /* istanbul ignore next */ []));
|
|
1080
|
+
isLoadingPublications = computed(() => this.loadingActive().includes(NotificationActionKey.GetPublications), ...(ngDevMode ? [{ debugName: "isLoadingPublications" }] : /* istanbul ignore next */ []));
|
|
1081
|
+
/** True while any write is in flight — every write control disables on it. */
|
|
1082
|
+
isMutating = computed(() => this.loadingActive().some((key) => NOTIFICATION_MUTATION_KEYS.includes(key)), ...(ngDevMode ? [{ debugName: "isMutating" }] : /* istanbul ignore next */ []));
|
|
797
1083
|
eventsError = computed(() => this.errors()[NotificationActionKey.GetEvents] ?? null, ...(ngDevMode ? [{ debugName: "eventsError" }] : /* istanbul ignore next */ []));
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1084
|
+
contextError = computed(() => this.errors()[NotificationActionKey.GetContext] ?? null, ...(ngDevMode ? [{ debugName: "contextError" }] : /* istanbul ignore next */ []));
|
|
1085
|
+
defaultsError = computed(() => this.errors()[NotificationActionKey.GetDefaults] ?? null, ...(ngDevMode ? [{ debugName: "defaultsError" }] : /* istanbul ignore next */ []));
|
|
1086
|
+
/** Accepts the enum or its string value, so templates can name the key. */
|
|
1087
|
+
isLoading(key) {
|
|
1088
|
+
return this.loadingActive().includes(key);
|
|
1089
|
+
}
|
|
1090
|
+
// ==========================================================================
|
|
1091
|
+
// Scope
|
|
1092
|
+
// ==========================================================================
|
|
801
1093
|
/**
|
|
802
1094
|
* Scopes this (shared, singleton) state to a module/event context.
|
|
803
1095
|
*
|
|
@@ -815,68 +1107,70 @@ class NotificationFacade {
|
|
|
815
1107
|
resetState() {
|
|
816
1108
|
return this.store.dispatch(new ResetNotificationState());
|
|
817
1109
|
}
|
|
818
|
-
|
|
1110
|
+
/** Loads the scope's events; the selected event's catalog and defaults follow. */
|
|
819
1111
|
loadEvents() {
|
|
820
1112
|
return this.store.dispatch(new GetEvents());
|
|
821
1113
|
}
|
|
822
|
-
|
|
823
|
-
|
|
1114
|
+
/** Kept for existing hosts — both load the scope's events and what follows from them. */
|
|
1115
|
+
loadBaseData() {
|
|
1116
|
+
return this.loadEvents();
|
|
824
1117
|
}
|
|
825
|
-
|
|
826
|
-
return this.
|
|
1118
|
+
loadAllData() {
|
|
1119
|
+
return this.loadEvents();
|
|
827
1120
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
return this.store.dispatch(new GetProperties(isMultiLang));
|
|
1121
|
+
// ==========================================================================
|
|
1122
|
+
// Selection
|
|
1123
|
+
// ==========================================================================
|
|
1124
|
+
selectEvent(eventId) {
|
|
1125
|
+
return this.store.dispatch(new SelectEvent(eventId));
|
|
834
1126
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
return this.store.dispatch(new SetActionMode(actionId));
|
|
1127
|
+
selectTemplate(templateId) {
|
|
1128
|
+
return this.store.dispatch(new SelectTemplate(templateId));
|
|
838
1129
|
}
|
|
839
|
-
|
|
840
|
-
return this.store.dispatch(new
|
|
1130
|
+
reloadDefaults(eventId) {
|
|
1131
|
+
return this.store.dispatch(new GetDefaultRecipes(eventId));
|
|
841
1132
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1133
|
+
// ==========================================================================
|
|
1134
|
+
// Writes
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
createTemplate(eventId, payload) {
|
|
1137
|
+
return this.store.dispatch(new CreateTemplate(eventId, payload));
|
|
844
1138
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
return this.store.dispatch(new SelectEvent(eventName));
|
|
1139
|
+
saveTemplate(templateId, payload) {
|
|
1140
|
+
return this.store.dispatch(new SaveTemplate(templateId, payload));
|
|
848
1141
|
}
|
|
849
|
-
|
|
850
|
-
return this.store.dispatch(new
|
|
1142
|
+
deleteTemplate(templateId, expectedRevision) {
|
|
1143
|
+
return this.store.dispatch(new DeleteTemplate(templateId, expectedRevision));
|
|
851
1144
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return this.store.dispatch(new AddReceivers(templateId, payload));
|
|
1145
|
+
addReceivers(templateId, expectedRevision, receivers) {
|
|
1146
|
+
return this.store.dispatch(new AddReceivers(templateId, expectedRevision, receivers));
|
|
855
1147
|
}
|
|
856
|
-
deleteReceiver(receiverId) {
|
|
857
|
-
return this.store.dispatch(new DeleteReceiver(receiverId));
|
|
1148
|
+
deleteReceiver(receiverId, expectedRevision) {
|
|
1149
|
+
return this.store.dispatch(new DeleteReceiver(receiverId, expectedRevision));
|
|
858
1150
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return this.store.dispatch(new AddTemplate(eventId, payload));
|
|
1151
|
+
publish(templateId, expectedRevision, reviews) {
|
|
1152
|
+
return this.store.dispatch(new PublishTemplate(templateId, expectedRevision, reviews));
|
|
862
1153
|
}
|
|
863
|
-
|
|
864
|
-
return this.store.dispatch(new
|
|
1154
|
+
turnOn(templateId, expectedRevision, reviews) {
|
|
1155
|
+
return this.store.dispatch(new SetTemplateActivation(templateId, expectedRevision, true, reviews));
|
|
865
1156
|
}
|
|
866
|
-
|
|
867
|
-
return this.store.dispatch(new
|
|
1157
|
+
turnOff(templateId, expectedRevision) {
|
|
1158
|
+
return this.store.dispatch(new SetTemplateActivation(templateId, expectedRevision, false, null));
|
|
868
1159
|
}
|
|
869
|
-
|
|
870
|
-
this.
|
|
871
|
-
this.loadReceivers();
|
|
1160
|
+
restorePublication(templateId, expectedRevision, publicationRevision, reviews) {
|
|
1161
|
+
return this.store.dispatch(new RestorePublication(templateId, expectedRevision, publicationRevision, reviews));
|
|
872
1162
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
this.
|
|
878
|
-
|
|
879
|
-
|
|
1163
|
+
initializeDefaults(eventId) {
|
|
1164
|
+
return this.store.dispatch(new InitializeDefaults(eventId));
|
|
1165
|
+
}
|
|
1166
|
+
restoreDefault(eventId, payload) {
|
|
1167
|
+
return this.store.dispatch(new RestoreDefault(eventId, payload));
|
|
1168
|
+
}
|
|
1169
|
+
// ==========================================================================
|
|
1170
|
+
// Preview — render-only, never stored: it saves, publishes and sends nothing.
|
|
1171
|
+
// ==========================================================================
|
|
1172
|
+
preview(eventId, request) {
|
|
1173
|
+
return this.api.preview(eventId, request);
|
|
880
1174
|
}
|
|
881
1175
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
882
1176
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationFacade, providedIn: 'root' });
|
|
@@ -886,374 +1180,1215 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
886
1180
|
args: [{ providedIn: 'root' }]
|
|
887
1181
|
}] });
|
|
888
1182
|
|
|
889
|
-
|
|
1183
|
+
/**
|
|
1184
|
+
* Turns the backend's stable codes into translated sentences. Codes are the
|
|
1185
|
+
* contract; display text from the server is never matched or shown raw.
|
|
1186
|
+
*/
|
|
1187
|
+
class NotificationMessages {
|
|
1188
|
+
transloco = inject(TranslocoService);
|
|
1189
|
+
code(code) {
|
|
1190
|
+
if (code) {
|
|
1191
|
+
// EMAIL_* codes are shared with the Email design tab. The loaded keys
|
|
1192
|
+
// are checked first: a translate() miss logs a missing-key warning.
|
|
1193
|
+
const loaded = this.transloco.getTranslation(this.transloco.getActiveLang());
|
|
1194
|
+
for (const key of [
|
|
1195
|
+
`notification.codes.${code}`,
|
|
1196
|
+
`email-delivery.design.codes.${code}`,
|
|
1197
|
+
]) {
|
|
1198
|
+
if (Object.prototype.hasOwnProperty.call(loaded, key)) {
|
|
1199
|
+
return this.transloco.translate(key);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return this.transloco.translate('notification.codes.generic', {
|
|
1204
|
+
code: code ?? '—',
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
diagnostic(diagnostic) {
|
|
1208
|
+
const message = this.code(diagnostic.code);
|
|
1209
|
+
return diagnostic.path ? `${message} (${diagnostic.path})` : message;
|
|
1210
|
+
}
|
|
1211
|
+
channel(channel) {
|
|
1212
|
+
return this.transloco.translate(`notification.channel.${channel}`);
|
|
1213
|
+
}
|
|
1214
|
+
t(key, params) {
|
|
1215
|
+
return this.transloco.translate(`notification.${key}`, params);
|
|
1216
|
+
}
|
|
1217
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationMessages, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1218
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationMessages, providedIn: 'root' });
|
|
1219
|
+
}
|
|
1220
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationMessages, decorators: [{
|
|
1221
|
+
type: Injectable,
|
|
1222
|
+
args: [{ providedIn: 'root' }]
|
|
1223
|
+
}] });
|
|
1224
|
+
const CHANNEL_ICONS = {
|
|
1225
|
+
App: 'alert.bell-01',
|
|
1226
|
+
Email: 'communication.mail-01',
|
|
1227
|
+
Sms: 'communication.message-text-square-01',
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
/** A template's channel is chosen once, here — it cannot change afterwards. */
|
|
1231
|
+
class AddTemplateDialog {
|
|
1232
|
+
modalService = inject(ModalService);
|
|
1233
|
+
ref = inject(ModalRef);
|
|
1234
|
+
transloco = inject(TranslocoService);
|
|
1235
|
+
/** Channels the event's authoring context allows. */
|
|
1236
|
+
channels = input(NOTIFICATION_CHANNELS, ...(ngDevMode ? [{ debugName: "channels" }] : /* istanbul ignore next */ []));
|
|
1237
|
+
form = new FormGroup({
|
|
1238
|
+
name: new FormControl('', {
|
|
1239
|
+
nonNullable: true,
|
|
1240
|
+
validators: [Validators.required, Validators.maxLength(250)],
|
|
1241
|
+
}),
|
|
1242
|
+
channel: new FormControl(null, {
|
|
1243
|
+
validators: [Validators.required],
|
|
1244
|
+
}),
|
|
1245
|
+
});
|
|
1246
|
+
channelOptions = computed(() => this.channels().map((channel) => ({
|
|
1247
|
+
id: channel,
|
|
1248
|
+
name: this.transloco.translate(`notification.channel.${channel}`),
|
|
1249
|
+
description: this.transloco.translate(`notification.channel-description.${channel}`),
|
|
1250
|
+
icon: CHANNEL_ICONS[channel],
|
|
1251
|
+
})), ...(ngDevMode ? [{ debugName: "channelOptions" }] : /* istanbul ignore next */ []));
|
|
1252
|
+
onSave() {
|
|
1253
|
+
const { name, channel } = this.form.getRawValue();
|
|
1254
|
+
if (this.form.invalid || !channel || !name.trim()) {
|
|
1255
|
+
this.form.markAllAsTouched();
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const result = { name: name.trim(), channel };
|
|
1259
|
+
this.ref.close(result);
|
|
1260
|
+
}
|
|
1261
|
+
onCancel() {
|
|
1262
|
+
this.ref.close();
|
|
1263
|
+
}
|
|
1264
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1265
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: AddTemplateDialog, isStandalone: true, selector: "mt-add-template-dialog", inputs: { channels: { classPropertyName: "channels", publicName: "channels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'notification'\">\n <div\n [class]=\"modalService.contentClass\"\n class=\"flex flex-col gap-4 p-4\"\n [formGroup]=\"form\"\n >\n <mt-text-field\n formControlName=\"name\"\n [label]=\"t('name')\"\n [placeholder]=\"t('enterName')\"\n [required]=\"true\"\n />\n <mt-radio-cards-field\n formControlName=\"channel\"\n [options]=\"channelOptions()\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [label]=\"t('channel.label')\"\n [hint]=\"t('channel.fixed-hint')\"\n [required]=\"true\"\n />\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <mt-button\n [label]=\"t('cancel')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"onCancel()\"\n />\n <mt-button\n [label]=\"t('create')\"\n [disabled]=\"form.invalid\"\n (onClick)=\"onSave()\"\n />\n </div>\n</ng-container>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: RadioCardsField, selector: "mt-radio-cards-field", inputs: ["circle", "label", "hint", "readonly", "required", "color", "size", "optionLabel", "optionValue", "options"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1266
|
+
}
|
|
1267
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, decorators: [{
|
|
1268
|
+
type: Component,
|
|
1269
|
+
args: [{ selector: 'mt-add-template-dialog', imports: [
|
|
1270
|
+
ReactiveFormsModule,
|
|
1271
|
+
TranslocoDirective,
|
|
1272
|
+
TextField,
|
|
1273
|
+
RadioCardsField,
|
|
1274
|
+
Button,
|
|
1275
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'notification'\">\n <div\n [class]=\"modalService.contentClass\"\n class=\"flex flex-col gap-4 p-4\"\n [formGroup]=\"form\"\n >\n <mt-text-field\n formControlName=\"name\"\n [label]=\"t('name')\"\n [placeholder]=\"t('enterName')\"\n [required]=\"true\"\n />\n <mt-radio-cards-field\n formControlName=\"channel\"\n [options]=\"channelOptions()\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [label]=\"t('channel.label')\"\n [hint]=\"t('channel.fixed-hint')\"\n [required]=\"true\"\n />\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <mt-button\n [label]=\"t('cancel')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"onCancel()\"\n />\n <mt-button\n [label]=\"t('create')\"\n [disabled]=\"form.invalid\"\n (onClick)=\"onSave()\"\n />\n </div>\n</ng-container>\n" }]
|
|
1276
|
+
}], propDecorators: { channels: [{ type: i0.Input, args: [{ isSignal: true, alias: "channels", required: false }] }] } });
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Renders a notification preview the way it will be read: Email inside the
|
|
1280
|
+
* shared design, App as its English and Arabic messages, SMS as plain text.
|
|
1281
|
+
* HTML only ever goes into the sandboxed `mt-email-html-preview` frame.
|
|
1282
|
+
* Images always show: the server strips every image outside
|
|
1283
|
+
* `approvedImageOrigins`, and the frame's CSP allows only those origins.
|
|
1284
|
+
*/
|
|
1285
|
+
class NotificationContentPreview {
|
|
1286
|
+
messages = inject(NotificationMessages);
|
|
1287
|
+
preview = input(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
|
|
1288
|
+
status = input('idle', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1289
|
+
errorCode = input(null, ...(ngDevMode ? [{ debugName: "errorCode" }] : /* istanbul ignore next */ []));
|
|
1290
|
+
/** The shown render is from an older version of the content. */
|
|
1291
|
+
stale = input(false, ...(ngDevMode ? [{ debugName: "stale" }] : /* istanbul ignore next */ []));
|
|
1292
|
+
height = input('26rem', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
1293
|
+
/**
|
|
1294
|
+
* Stretch the rendered frames to the height the host gives this preview
|
|
1295
|
+
* (beside the editor), with `height` as the minimum.
|
|
1296
|
+
*/
|
|
1297
|
+
fill = input(false, ...(ngDevMode ? [{ debugName: "fill" }] : /* istanbul ignore next */ []));
|
|
1298
|
+
/** Off beside the editor, where the Subject field is in view. */
|
|
1299
|
+
showSubject = input(true, ...(ngDevMode ? [{ debugName: "showSubject" }] : /* istanbul ignore next */ []));
|
|
1300
|
+
retry = output();
|
|
1301
|
+
narrow = new FormControl(false, { nonNullable: true });
|
|
1302
|
+
narrowValue = signal(false, ...(ngDevMode ? [{ debugName: "narrowValue" }] : /* istanbul ignore next */ []));
|
|
1303
|
+
email = computed(() => {
|
|
1304
|
+
const preview = this.preview();
|
|
1305
|
+
return preview?.channel === 'Email' ? preview : null;
|
|
1306
|
+
}, ...(ngDevMode ? [{ debugName: "email" }] : /* istanbul ignore next */ []));
|
|
1307
|
+
app = computed(() => {
|
|
1308
|
+
const preview = this.preview();
|
|
1309
|
+
return preview?.channel === 'App' ? preview : null;
|
|
1310
|
+
}, ...(ngDevMode ? [{ debugName: "app" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
sms = computed(() => {
|
|
1312
|
+
const preview = this.preview();
|
|
1313
|
+
return preview?.channel === 'Sms' ? preview : null;
|
|
1314
|
+
}, ...(ngDevMode ? [{ debugName: "sms" }] : /* istanbul ignore next */ []));
|
|
1315
|
+
diagnostics = computed(() => {
|
|
1316
|
+
const preview = this.preview();
|
|
1317
|
+
if (!preview)
|
|
1318
|
+
return [];
|
|
1319
|
+
if (preview.channel === 'App') {
|
|
1320
|
+
return [...preview.english.diagnostics, ...preview.arabic.diagnostics];
|
|
1321
|
+
}
|
|
1322
|
+
return preview.diagnostics;
|
|
1323
|
+
}, ...(ngDevMode ? [{ debugName: "diagnostics" }] : /* istanbul ignore next */ []));
|
|
1324
|
+
constructor() {
|
|
1325
|
+
this.narrow.valueChanges
|
|
1326
|
+
.pipe(takeUntilDestroyed())
|
|
1327
|
+
.subscribe((value) => this.narrowValue.set(value));
|
|
1328
|
+
}
|
|
1329
|
+
describe(diagnostic) {
|
|
1330
|
+
return this.messages.diagnostic(diagnostic);
|
|
1331
|
+
}
|
|
1332
|
+
describeCode(code) {
|
|
1333
|
+
return this.messages.code(code);
|
|
1334
|
+
}
|
|
1335
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationContentPreview, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1336
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NotificationContentPreview, isStandalone: true, selector: "mt-notification-content-preview", inputs: { preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, errorCode: { classPropertyName: "errorCode", publicName: "errorCode", isSignal: true, isRequired: false, transformFunction: null }, stale: { classPropertyName: "stale", publicName: "stale", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, fill: { classPropertyName: "fill", publicName: "fill", isSignal: true, isRequired: false, transformFunction: null }, showSubject: { classPropertyName: "showSubject", publicName: "showSubject", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { properties: { "class.flex-1": "fill()" }, classAttribute: "flex min-w-0 flex-col" }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'notification.preview'\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n <!-- The dimmed render already shows an update in progress; this only\n announces it. -->\n <span class=\"sr-only\" aria-live=\"polite\">{{\n status() === \"updating\" ? t(\"updating\") : \"\"\n }}</span>\n @if (email()) {\n <div class=\"flex justify-end\">\n <mt-toggle-field [formControl]=\"narrow\" [label]=\"t('narrow')\" />\n </div>\n }\n\n @if (status() === \"error\") {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\n role=\"alert\"\n >\n <span>{{ describeCode(errorCode()) }}</span>\n <mt-button\n [label]=\"t('retry')\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"retry.emit()\"\n />\n </div>\n }\n\n @if (preview()) {\n <div\n class=\"flex min-w-0 flex-1 flex-col gap-3 transition-opacity\"\n [class.opacity-60]=\"stale() || status() === 'updating'\"\n >\n @if (email(); as email) {\n @if (showSubject()) {\n <div\n class=\"rounded-md border border-surface bg-surface-50 px-3 py-2 text-sm\"\n >\n <span class=\"text-muted-color\">{{ t(\"subject\") }}:</span>\n <span class=\"ms-1 font-medium\">{{ email.subject || \"\u2014\" }}</span>\n </div>\n }\n <div\n class=\"mx-auto w-full overflow-hidden rounded-md border border-surface\"\n [class.max-w-[375px]]=\"narrowValue()\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [style.min-height]=\"fill() ? height() : null\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"email.html\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"email.approvedImageOrigins\"\n [title]=\"t('email-title')\"\n [height]=\"fill() ? '100%' : height()\"\n />\n </div>\n }\n @if (app(); as app) {\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"english\")\n }}</span>\n <div\n class=\"overflow-hidden rounded-md border border-surface\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [class.min-h-48]=\"fill()\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"app.english.html\"\n [fragment]=\"true\"\n direction=\"ltr\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"app.approvedImageOrigins\"\n [title]=\"t('english')\"\n [height]=\"fill() ? '100%' : '12rem'\"\n />\n </div>\n </div>\n <div class=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"arabic\")\n }}</span>\n <div\n class=\"overflow-hidden rounded-md border border-surface\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [class.min-h-48]=\"fill()\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"app.arabic.html\"\n [fragment]=\"true\"\n direction=\"rtl\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"app.approvedImageOrigins\"\n [title]=\"t('arabic')\"\n [height]=\"fill() ? '100%' : '12rem'\"\n />\n </div>\n </div>\n </div>\n }\n @if (sms(); as sms) {\n <div\n class=\"whitespace-pre-wrap break-words rounded-md border border-surface bg-surface-50 px-4 py-3 text-sm\"\n dir=\"auto\"\n >\n {{ sms.text }}\n </div>\n <span class=\"text-xs text-muted-color\">{{\n t(\"sms-length\", { count: sms.text.length })\n }}</span>\n }\n\n @if (diagnostics().length > 0) {\n <ul class=\"list-disc ps-5 text-sm text-amber-700\">\n @for (diagnostic of diagnostics(); track $index) {\n <li>{{ describe(diagnostic) }}</li>\n }\n </ul>\n }\n </div>\n } @else if (status() !== \"error\") {\n <p-skeleton width=\"100%\" [height]=\"height()\" borderRadius=\"0.375rem\" />\n }\n </div>\n</ng-container>\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }, { kind: "component", type: EmailHtmlPreview, selector: "mt-email-html-preview", inputs: ["html", "showImages", "approvedImageOrigins", "fragment", "direction", "title", "height"] }, { kind: "component", type: Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1337
|
+
}
|
|
1338
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationContentPreview, decorators: [{
|
|
1339
|
+
type: Component,
|
|
1340
|
+
args: [{ selector: 'mt-notification-content-preview', imports: [
|
|
1341
|
+
TranslocoDirective,
|
|
1342
|
+
ReactiveFormsModule,
|
|
1343
|
+
Button,
|
|
1344
|
+
ToggleField,
|
|
1345
|
+
EmailHtmlPreview,
|
|
1346
|
+
Skeleton,
|
|
1347
|
+
], host: { class: 'flex min-w-0 flex-col', '[class.flex-1]': 'fill()' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'notification.preview'\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n <!-- The dimmed render already shows an update in progress; this only\n announces it. -->\n <span class=\"sr-only\" aria-live=\"polite\">{{\n status() === \"updating\" ? t(\"updating\") : \"\"\n }}</span>\n @if (email()) {\n <div class=\"flex justify-end\">\n <mt-toggle-field [formControl]=\"narrow\" [label]=\"t('narrow')\" />\n </div>\n }\n\n @if (status() === \"error\") {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\n role=\"alert\"\n >\n <span>{{ describeCode(errorCode()) }}</span>\n <mt-button\n [label]=\"t('retry')\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"retry.emit()\"\n />\n </div>\n }\n\n @if (preview()) {\n <div\n class=\"flex min-w-0 flex-1 flex-col gap-3 transition-opacity\"\n [class.opacity-60]=\"stale() || status() === 'updating'\"\n >\n @if (email(); as email) {\n @if (showSubject()) {\n <div\n class=\"rounded-md border border-surface bg-surface-50 px-3 py-2 text-sm\"\n >\n <span class=\"text-muted-color\">{{ t(\"subject\") }}:</span>\n <span class=\"ms-1 font-medium\">{{ email.subject || \"\u2014\" }}</span>\n </div>\n }\n <div\n class=\"mx-auto w-full overflow-hidden rounded-md border border-surface\"\n [class.max-w-[375px]]=\"narrowValue()\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [style.min-height]=\"fill() ? height() : null\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"email.html\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"email.approvedImageOrigins\"\n [title]=\"t('email-title')\"\n [height]=\"fill() ? '100%' : height()\"\n />\n </div>\n }\n @if (app(); as app) {\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n <div class=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"english\")\n }}</span>\n <div\n class=\"overflow-hidden rounded-md border border-surface\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [class.min-h-48]=\"fill()\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"app.english.html\"\n [fragment]=\"true\"\n direction=\"ltr\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"app.approvedImageOrigins\"\n [title]=\"t('english')\"\n [height]=\"fill() ? '100%' : '12rem'\"\n />\n </div>\n </div>\n <div class=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"arabic\")\n }}</span>\n <div\n class=\"overflow-hidden rounded-md border border-surface\"\n [class.relative]=\"fill()\"\n [class.flex-1]=\"fill()\"\n [class.min-h-48]=\"fill()\"\n >\n <mt-email-html-preview\n [class.absolute]=\"fill()\"\n [class.inset-0]=\"fill()\"\n [html]=\"app.arabic.html\"\n [fragment]=\"true\"\n direction=\"rtl\"\n [showImages]=\"true\"\n [approvedImageOrigins]=\"app.approvedImageOrigins\"\n [title]=\"t('arabic')\"\n [height]=\"fill() ? '100%' : '12rem'\"\n />\n </div>\n </div>\n </div>\n }\n @if (sms(); as sms) {\n <div\n class=\"whitespace-pre-wrap break-words rounded-md border border-surface bg-surface-50 px-4 py-3 text-sm\"\n dir=\"auto\"\n >\n {{ sms.text }}\n </div>\n <span class=\"text-xs text-muted-color\">{{\n t(\"sms-length\", { count: sms.text.length })\n }}</span>\n }\n\n @if (diagnostics().length > 0) {\n <ul class=\"list-disc ps-5 text-sm text-amber-700\">\n @for (diagnostic of diagnostics(); track $index) {\n <li>{{ describe(diagnostic) }}</li>\n }\n </ul>\n }\n </div>\n } @else if (status() !== \"error\") {\n <p-skeleton width=\"100%\" [height]=\"height()\" borderRadius=\"0.375rem\" />\n }\n </div>\n</ng-container>\n" }]
|
|
1348
|
+
}], ctorParameters: () => [], propDecorators: { preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], errorCode: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorCode", required: false }] }], stale: [{ type: i0.Input, args: [{ isSignal: true, alias: "stale", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], showSubject: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubject", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* One preview pipeline per editor. A new request cancels the one in flight,
|
|
1352
|
+
* a late answer for an older draft is dropped (`switchMap`), and Retry
|
|
1353
|
+
* re-renders the exact request that failed — so retrying a history preview
|
|
1354
|
+
* keeps the selected publication instead of falling back to the draft.
|
|
1355
|
+
* Previews never save, publish, enable or send anything.
|
|
1356
|
+
*/
|
|
1357
|
+
class NotificationPreviewRunner {
|
|
1358
|
+
status = signal('idle', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1359
|
+
/** Last successful render — kept on screen while the next one runs. */
|
|
1360
|
+
result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
|
|
1361
|
+
errorCode = signal(null, ...(ngDevMode ? [{ debugName: "errorCode" }] : /* istanbul ignore next */ []));
|
|
1362
|
+
/** Identity of the request `result` was rendered from. */
|
|
1363
|
+
resultKey = signal(null, ...(ngDevMode ? [{ debugName: "resultKey" }] : /* istanbul ignore next */ []));
|
|
1364
|
+
immediate$ = new Subject();
|
|
1365
|
+
debounced$ = new Subject();
|
|
1366
|
+
retry$ = new Subject();
|
|
1367
|
+
lastJob = null;
|
|
1368
|
+
/** Bumped by every request, so a pending debounced one can tell it was overtaken. */
|
|
1369
|
+
seq = 0;
|
|
1370
|
+
constructor(facade, destroyRef, debounceMs = 600) {
|
|
1371
|
+
merge(this.immediate$, this.debounced$.pipe(debounceTime(debounceMs), filter((job) => job.seq === this.seq)), this.retry$.pipe(map(() => this.lastJob)))
|
|
1372
|
+
.pipe(switchMap((job) => {
|
|
1373
|
+
this.lastJob = job;
|
|
1374
|
+
if (!job) {
|
|
1375
|
+
return of({ status: 'idle' });
|
|
1376
|
+
}
|
|
1377
|
+
return facade.preview(job.eventId, job.request).pipe(map((preview) => ({ status: 'ready', preview, job })), catchError((error) => of({
|
|
1378
|
+
status: 'error',
|
|
1379
|
+
code: readNotificationFailure(error).code,
|
|
1380
|
+
})), startWith({ status: 'updating' }));
|
|
1381
|
+
}), takeUntilDestroyed(destroyRef))
|
|
1382
|
+
.subscribe((state) => {
|
|
1383
|
+
this.status.set(state.status);
|
|
1384
|
+
if (state.status === 'ready') {
|
|
1385
|
+
this.result.set(state.preview);
|
|
1386
|
+
this.resultKey.set(state.job.key);
|
|
1387
|
+
this.errorCode.set(null);
|
|
1388
|
+
}
|
|
1389
|
+
else if (state.status === 'error') {
|
|
1390
|
+
this.errorCode.set(state.code);
|
|
1391
|
+
}
|
|
1392
|
+
else if (state.status === 'idle') {
|
|
1393
|
+
this.result.set(null);
|
|
1394
|
+
this.resultKey.set(null);
|
|
1395
|
+
this.errorCode.set(null);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
/** Render now (opening a review, selecting a history entry). */
|
|
1400
|
+
run(eventId, request) {
|
|
1401
|
+
this.seq++;
|
|
1402
|
+
this.immediate$.next(toJob(eventId, request));
|
|
1403
|
+
}
|
|
1404
|
+
/** Render after a typing pause. */
|
|
1405
|
+
schedule(eventId, request) {
|
|
1406
|
+
this.debounced$.next({ ...toJob(eventId, request), seq: ++this.seq });
|
|
1407
|
+
}
|
|
1408
|
+
retry() {
|
|
1409
|
+
this.retry$.next();
|
|
1410
|
+
}
|
|
1411
|
+
clear() {
|
|
1412
|
+
this.seq++;
|
|
1413
|
+
this.immediate$.next(null);
|
|
1414
|
+
}
|
|
1415
|
+
isCurrent(eventId, request) {
|
|
1416
|
+
return (this.status() === 'ready' &&
|
|
1417
|
+
this.resultKey() === toJob(eventId, request).key);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function toJob(eventId, request) {
|
|
1421
|
+
return { eventId, request, key: `${eventId}:${JSON.stringify(request)}` };
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Retained publications of one template, newest first. Selecting a version
|
|
1426
|
+
* previews that version's own content and recipients — never the draft's.
|
|
1427
|
+
* "Restore" hands the chosen version back to the editor, which runs the
|
|
1428
|
+
* review before anything is written.
|
|
1429
|
+
*/
|
|
1430
|
+
class TemplateHistoryDrawer {
|
|
1431
|
+
modalService = inject(ModalService);
|
|
1432
|
+
ref = inject(ModalRef);
|
|
890
1433
|
facade = inject(NotificationFacade);
|
|
1434
|
+
destroyRef = inject(DestroyRef);
|
|
891
1435
|
transloco = inject(TranslocoService);
|
|
892
|
-
|
|
1436
|
+
eventId = input.required(...(ngDevMode ? [{ debugName: "eventId" }] : /* istanbul ignore next */ []));
|
|
1437
|
+
/** Restoring is blocked while the editor holds unsaved edits. */
|
|
1438
|
+
draftDirty = input(false, ...(ngDevMode ? [{ debugName: "draftDirty" }] : /* istanbul ignore next */ []));
|
|
1439
|
+
historyLimit = input(null, ...(ngDevMode ? [{ debugName: "historyLimit" }] : /* istanbul ignore next */ []));
|
|
1440
|
+
runner = new NotificationPreviewRunner(this.facade, this.destroyRef);
|
|
1441
|
+
publications = this.facade.publications;
|
|
1442
|
+
template = this.facade.selectedTemplate;
|
|
1443
|
+
selectedRevision = signal(null, ...(ngDevMode ? [{ debugName: "selectedRevision" }] : /* istanbul ignore next */ []));
|
|
1444
|
+
selected = computed(() => this.publications().find((publication) => publication.revision === this.selectedRevision()) ?? null, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
|
|
1445
|
+
defaultsPublisher = DEFAULTS_PUBLISHER;
|
|
1446
|
+
receiverLabels = computed(() => new Map(flattenRecipientCatalog(this.facade.context()?.recipients ?? [], this.transloco.getActiveLang())
|
|
1447
|
+
.flatMap((group) => group.items)
|
|
1448
|
+
.map((item) => [item.key.toLowerCase(), item.name])), ...(ngDevMode ? [{ debugName: "receiverLabels" }] : /* istanbul ignore next */ []));
|
|
1449
|
+
constructor() {
|
|
1450
|
+
// Open on the live version once the history arrives.
|
|
1451
|
+
effect(() => {
|
|
1452
|
+
const publications = this.publications();
|
|
1453
|
+
const live = this.template()?.publishedRevision ?? null;
|
|
1454
|
+
untracked(() => {
|
|
1455
|
+
if (this.selectedRevision() !== null || publications.length === 0) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
this.select(publications.find((item) => item.revision === live) ??
|
|
1459
|
+
publications[0]);
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
select(publication) {
|
|
1464
|
+
this.selectedRevision.set(publication.revision);
|
|
1465
|
+
this.runner.run(this.eventId(), withPreviewSamples(publicationPreviewRequest(publication.content), this.facade.context()?.properties ?? [], this.transloco.getActiveLang()));
|
|
1466
|
+
}
|
|
1467
|
+
/** The catalog's (localized) name; the snapshot keeps whatever was saved. */
|
|
1468
|
+
recipientName(recipient) {
|
|
1469
|
+
return (this.receiverLabels().get(recipientKey(recipient.type, recipient.identifier).toLowerCase()) ??
|
|
1470
|
+
(recipient.name || recipient.identifier));
|
|
1471
|
+
}
|
|
1472
|
+
restore() {
|
|
1473
|
+
const publication = this.selected();
|
|
1474
|
+
if (!publication || this.draftDirty())
|
|
1475
|
+
return;
|
|
1476
|
+
const result = { publication };
|
|
1477
|
+
this.ref.close(result);
|
|
1478
|
+
}
|
|
1479
|
+
close() {
|
|
1480
|
+
this.ref.close();
|
|
1481
|
+
}
|
|
1482
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TemplateHistoryDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1483
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: TemplateHistoryDrawer, isStandalone: true, selector: "mt-notification-template-history-drawer", inputs: { eventId: { classPropertyName: "eventId", publicName: "eventId", isSignal: true, isRequired: true, transformFunction: null }, draftDirty: { classPropertyName: "draftDirty", publicName: "draftDirty", isSignal: true, isRequired: false, transformFunction: null }, historyLimit: { classPropertyName: "historyLimit", publicName: "historyLimit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'notification.history'\">\n <div\n [class]=\"modalService.contentClass\"\n class=\"flex min-h-0 flex-col gap-4 p-4\"\n >\n @if (facade.isLoadingPublications() && publications().length === 0) {\n <div class=\"flex flex-col gap-2\">\n <p-skeleton width=\"100%\" height=\"3rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"3rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"20rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else if (publications().length === 0) {\n <p class=\"text-sm text-muted-color\">{{ t(\"empty\") }}</p>\n } @else {\n @if (historyLimit(); as limit) {\n <p class=\"text-xs text-muted-color\">\n {{ t(\"limit\", { limit: limit }) }}\n </p>\n }\n <ul\n class=\"flex flex-col gap-2\"\n role=\"listbox\"\n [attr.aria-label]=\"t('title')\"\n >\n @for (publication of publications(); track publication.revision) {\n <li>\n <button\n type=\"button\"\n role=\"option\"\n [attr.aria-selected]=\"publication.revision === selectedRevision()\"\n class=\"flex w-full flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-start text-sm transition-colors\"\n [class]=\"\n publication.revision === selectedRevision()\n ? 'border-primary bg-primary-50'\n : 'border-surface bg-content hover:bg-surface-50'\n \"\n (click)=\"select(publication)\"\n >\n <span class=\"flex items-center gap-2\">\n <span class=\"font-medium\">{{\n t(\"version\", { revision: publication.revision })\n }}</span>\n @if (publication.revision === template()?.publishedRevision) {\n <span\n class=\"rounded-md border border-green-200 bg-green-50 px-1.5 py-0.5 text-xs text-green-700\"\n >{{ t(\"live\") }}</span\n >\n }\n </span>\n <span class=\"text-xs text-muted-color\">\n {{ publication.publishedAtUtc | date: \"medium\" }}\n \u00B7\n {{\n publication.publishedBy === defaultsPublisher\n ? t(\"by-system\")\n : t(\"by-user\")\n }}\n </span>\n </button>\n </li>\n }\n </ul>\n\n @if (selected(); as selected) {\n <section class=\"flex flex-col gap-3 border-t border-surface pt-4\">\n <h4 class=\"text-sm font-semibold\">\n {{ t(\"version\", { revision: selected.revision }) }} \u2014\n {{ selected.content.name }}\n </h4>\n <mt-notification-content-preview\n [preview]=\"runner.result()\"\n [status]=\"runner.status()\"\n [errorCode]=\"runner.errorCode()\"\n height=\"22rem\"\n (retry)=\"runner.retry()\"\n />\n <div class=\"flex flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"recipients\")\n }}</span>\n @if (selected.content.receivers.length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (recipient of selected.content.receivers; track $index) {\n <li\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-1 text-sm\"\n >\n {{ recipientName(recipient) }}\n </li>\n }\n </ul>\n } @else {\n <span class=\"text-sm text-muted-color\">{{\n t(\"no-recipients\")\n }}</span>\n }\n </div>\n @if (selected.content.conditionFormula; as condition) {\n <div class=\"flex flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"condition\")\n }}</span>\n <code\n class=\"block break-all rounded-md border border-surface bg-surface-50 px-3 py-2 text-xs\"\n dir=\"ltr\"\n >{{ condition }}</code\n >\n </div>\n }\n </section>\n }\n }\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <mt-button\n [label]=\"t('close')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"close()\"\n />\n <mt-button\n icon=\"time.clock-rewind\"\n [label]=\"t('restore')\"\n [tooltip]=\"draftDirty() ? t('restore-blocked') : ''\"\n [disabled]=\"!selected() || draftDirty()\"\n (onClick)=\"restore()\"\n />\n </div>\n</ng-container>\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: NotificationContentPreview, selector: "mt-notification-content-preview", inputs: ["preview", "status", "errorCode", "stale", "height", "fill", "showSubject"], outputs: ["retry"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1484
|
+
}
|
|
1485
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TemplateHistoryDrawer, decorators: [{
|
|
1486
|
+
type: Component,
|
|
1487
|
+
args: [{ selector: 'mt-notification-template-history-drawer', imports: [
|
|
1488
|
+
TranslocoDirective,
|
|
1489
|
+
DatePipe,
|
|
1490
|
+
Button,
|
|
1491
|
+
Skeleton,
|
|
1492
|
+
NotificationContentPreview,
|
|
1493
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'notification.history'\">\n <div\n [class]=\"modalService.contentClass\"\n class=\"flex min-h-0 flex-col gap-4 p-4\"\n >\n @if (facade.isLoadingPublications() && publications().length === 0) {\n <div class=\"flex flex-col gap-2\">\n <p-skeleton width=\"100%\" height=\"3rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"3rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"20rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else if (publications().length === 0) {\n <p class=\"text-sm text-muted-color\">{{ t(\"empty\") }}</p>\n } @else {\n @if (historyLimit(); as limit) {\n <p class=\"text-xs text-muted-color\">\n {{ t(\"limit\", { limit: limit }) }}\n </p>\n }\n <ul\n class=\"flex flex-col gap-2\"\n role=\"listbox\"\n [attr.aria-label]=\"t('title')\"\n >\n @for (publication of publications(); track publication.revision) {\n <li>\n <button\n type=\"button\"\n role=\"option\"\n [attr.aria-selected]=\"publication.revision === selectedRevision()\"\n class=\"flex w-full flex-wrap items-center justify-between gap-2 rounded-md border px-3 py-2 text-start text-sm transition-colors\"\n [class]=\"\n publication.revision === selectedRevision()\n ? 'border-primary bg-primary-50'\n : 'border-surface bg-content hover:bg-surface-50'\n \"\n (click)=\"select(publication)\"\n >\n <span class=\"flex items-center gap-2\">\n <span class=\"font-medium\">{{\n t(\"version\", { revision: publication.revision })\n }}</span>\n @if (publication.revision === template()?.publishedRevision) {\n <span\n class=\"rounded-md border border-green-200 bg-green-50 px-1.5 py-0.5 text-xs text-green-700\"\n >{{ t(\"live\") }}</span\n >\n }\n </span>\n <span class=\"text-xs text-muted-color\">\n {{ publication.publishedAtUtc | date: \"medium\" }}\n \u00B7\n {{\n publication.publishedBy === defaultsPublisher\n ? t(\"by-system\")\n : t(\"by-user\")\n }}\n </span>\n </button>\n </li>\n }\n </ul>\n\n @if (selected(); as selected) {\n <section class=\"flex flex-col gap-3 border-t border-surface pt-4\">\n <h4 class=\"text-sm font-semibold\">\n {{ t(\"version\", { revision: selected.revision }) }} \u2014\n {{ selected.content.name }}\n </h4>\n <mt-notification-content-preview\n [preview]=\"runner.result()\"\n [status]=\"runner.status()\"\n [errorCode]=\"runner.errorCode()\"\n height=\"22rem\"\n (retry)=\"runner.retry()\"\n />\n <div class=\"flex flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"recipients\")\n }}</span>\n @if (selected.content.receivers.length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (recipient of selected.content.receivers; track $index) {\n <li\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-1 text-sm\"\n >\n {{ recipientName(recipient) }}\n </li>\n }\n </ul>\n } @else {\n <span class=\"text-sm text-muted-color\">{{\n t(\"no-recipients\")\n }}</span>\n }\n </div>\n @if (selected.content.conditionFormula; as condition) {\n <div class=\"flex flex-col gap-1\">\n <span class=\"text-xs font-medium text-muted-color\">{{\n t(\"condition\")\n }}</span>\n <code\n class=\"block break-all rounded-md border border-surface bg-surface-50 px-3 py-2 text-xs\"\n dir=\"ltr\"\n >{{ condition }}</code\n >\n </div>\n }\n </section>\n }\n }\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <mt-button\n [label]=\"t('close')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"close()\"\n />\n <mt-button\n icon=\"time.clock-rewind\"\n [label]=\"t('restore')\"\n [tooltip]=\"draftDirty() ? t('restore-blocked') : ''\"\n [disabled]=\"!selected() || draftDirty()\"\n (onClick)=\"restore()\"\n />\n </div>\n</ng-container>\n" }]
|
|
1494
|
+
}], ctorParameters: () => [], propDecorators: { eventId: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventId", required: true }] }], draftDirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "draftDirty", required: false }] }], historyLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "historyLimit", required: false }] }] } });
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* The explicit review before a write changes what is sent: the actual content
|
|
1498
|
+
* (rendered by the server) and the actual receivers of the version being
|
|
1499
|
+
* published / turned on / restored — never the working draft's.
|
|
1500
|
+
*
|
|
1501
|
+
* The review flags are not separate tick boxes (product decision, 2026-10-27):
|
|
1502
|
+
* confirming this dialog, which only enables once the server preview has
|
|
1503
|
+
* rendered, is the acknowledgement. They are sent only from that confirmation,
|
|
1504
|
+
* never as defaults elsewhere.
|
|
1505
|
+
*/
|
|
1506
|
+
class TemplateReviewDialog {
|
|
893
1507
|
modalService = inject(ModalService);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1508
|
+
ref = inject(ModalRef);
|
|
1509
|
+
facade = inject(NotificationFacade);
|
|
1510
|
+
destroyRef = inject(DestroyRef);
|
|
1511
|
+
mode = input.required(...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1512
|
+
eventId = input.required(...(ngDevMode ? [{ debugName: "eventId" }] : /* istanbul ignore next */ []));
|
|
1513
|
+
request = input.required(...(ngDevMode ? [{ debugName: "request" }] : /* istanbul ignore next */ []));
|
|
1514
|
+
recipients = input([], ...(ngDevMode ? [{ debugName: "recipients" }] : /* istanbul ignore next */ []));
|
|
1515
|
+
condition = input(null, ...(ngDevMode ? [{ debugName: "condition" }] : /* istanbul ignore next */ []));
|
|
1516
|
+
/** Templates a ready-made slot owns also need `languageReviewed`. */
|
|
1517
|
+
requireLanguageReview = input(false, ...(ngDevMode ? [{ debugName: "requireLanguageReview" }] : /* istanbul ignore next */ []));
|
|
1518
|
+
/** Publishing an enabled template changes live sending at once. */
|
|
1519
|
+
isEnabled = input(false, ...(ngDevMode ? [{ debugName: "isEnabled" }] : /* istanbul ignore next */ []));
|
|
1520
|
+
runner = new NotificationPreviewRunner(this.facade, this.destroyRef);
|
|
1521
|
+
previewReady = computed(() => this.runner.status() === 'ready', ...(ngDevMode ? [{ debugName: "previewReady" }] : /* istanbul ignore next */ []));
|
|
1522
|
+
ngOnInit() {
|
|
1523
|
+
this.runner.run(this.eventId(), this.request());
|
|
1524
|
+
}
|
|
1525
|
+
confirm() {
|
|
1526
|
+
if (!this.previewReady())
|
|
1527
|
+
return;
|
|
1528
|
+
const reviewed = this.mode() !== 'restoreDefault';
|
|
1529
|
+
const result = {
|
|
1530
|
+
recipientsReviewed: reviewed,
|
|
1531
|
+
languageReviewed: reviewed && this.requireLanguageReview(),
|
|
1532
|
+
};
|
|
1533
|
+
this.ref.close(result);
|
|
1534
|
+
}
|
|
1535
|
+
cancel() {
|
|
1536
|
+
this.ref.close();
|
|
1537
|
+
}
|
|
1538
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TemplateReviewDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1539
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: TemplateReviewDialog, isStandalone: true, selector: "mt-notification-template-review-dialog", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: true, transformFunction: null }, eventId: { classPropertyName: "eventId", publicName: "eventId", isSignal: true, isRequired: true, transformFunction: null }, request: { classPropertyName: "request", publicName: "request", isSignal: true, isRequired: true, transformFunction: null }, recipients: { classPropertyName: "recipients", publicName: "recipients", isSignal: true, isRequired: false, transformFunction: null }, condition: { classPropertyName: "condition", publicName: "condition", isSignal: true, isRequired: false, transformFunction: null }, requireLanguageReview: { classPropertyName: "requireLanguageReview", publicName: "requireLanguageReview", isSignal: true, isRequired: false, transformFunction: null }, isEnabled: { classPropertyName: "isEnabled", publicName: "isEnabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'notification.review'\">\n <div [class]=\"modalService.contentClass\" class=\"flex flex-col gap-4 p-4\">\n <div\n class=\"rounded-md border px-4 py-3 text-sm\"\n [class]=\"\n mode() === 'publish' && isEnabled()\n ? 'border-amber-200 bg-amber-50 text-amber-700'\n : 'border-surface bg-surface-50'\n \"\n >\n {{\n t(\n \"intro.\" +\n mode() +\n (mode() === \"publish\" && isEnabled() ? \"-live\" : \"\")\n )\n }}\n </div>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"recipients\") }}</h4>\n @if (recipients().length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (recipient of recipients(); track $index) {\n <li\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-1 text-sm\"\n >\n <span class=\"font-medium\">{{ recipient.name }}</span>\n <span class=\"ms-1 text-xs text-muted-color\">{{\n t(\"recipient-type.\" + recipient.type)\n }}</span>\n </li>\n }\n </ul>\n } @else {\n <p class=\"text-sm text-amber-700\">{{ t(\"no-recipients\") }}</p>\n }\n </section>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"condition\") }}</h4>\n @if (condition(); as condition) {\n <code\n class=\"block break-all rounded-md border border-surface bg-surface-50 px-3 py-2 text-xs\"\n dir=\"ltr\"\n >{{ condition }}</code\n >\n } @else {\n <p class=\"text-sm text-muted-color\">{{ t(\"always\") }}</p>\n }\n </section>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"content\") }}</h4>\n <mt-notification-content-preview\n [preview]=\"runner.result()\"\n [status]=\"runner.status()\"\n [errorCode]=\"runner.errorCode()\"\n height=\"20rem\"\n (retry)=\"runner.retry()\"\n />\n </section>\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <div class=\"flex w-full justify-end gap-2\">\n <mt-button\n [label]=\"t('cancel')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"cancel()\"\n />\n <mt-button\n [label]=\"t('confirm.' + mode())\"\n [tooltip]=\"previewReady() ? '' : t('waiting-preview')\"\n [disabled]=\"!previewReady()\"\n (onClick)=\"confirm()\"\n />\n </div>\n </div>\n</ng-container>\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: NotificationContentPreview, selector: "mt-notification-content-preview", inputs: ["preview", "status", "errorCode", "stale", "height", "fill", "showSubject"], outputs: ["retry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1540
|
+
}
|
|
1541
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TemplateReviewDialog, decorators: [{
|
|
1542
|
+
type: Component,
|
|
1543
|
+
args: [{ selector: 'mt-notification-template-review-dialog', imports: [TranslocoDirective, Button, NotificationContentPreview], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'notification.review'\">\n <div [class]=\"modalService.contentClass\" class=\"flex flex-col gap-4 p-4\">\n <div\n class=\"rounded-md border px-4 py-3 text-sm\"\n [class]=\"\n mode() === 'publish' && isEnabled()\n ? 'border-amber-200 bg-amber-50 text-amber-700'\n : 'border-surface bg-surface-50'\n \"\n >\n {{\n t(\n \"intro.\" +\n mode() +\n (mode() === \"publish\" && isEnabled() ? \"-live\" : \"\")\n )\n }}\n </div>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"recipients\") }}</h4>\n @if (recipients().length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (recipient of recipients(); track $index) {\n <li\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-1 text-sm\"\n >\n <span class=\"font-medium\">{{ recipient.name }}</span>\n <span class=\"ms-1 text-xs text-muted-color\">{{\n t(\"recipient-type.\" + recipient.type)\n }}</span>\n </li>\n }\n </ul>\n } @else {\n <p class=\"text-sm text-amber-700\">{{ t(\"no-recipients\") }}</p>\n }\n </section>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"condition\") }}</h4>\n @if (condition(); as condition) {\n <code\n class=\"block break-all rounded-md border border-surface bg-surface-50 px-3 py-2 text-xs\"\n dir=\"ltr\"\n >{{ condition }}</code\n >\n } @else {\n <p class=\"text-sm text-muted-color\">{{ t(\"always\") }}</p>\n }\n </section>\n\n <section class=\"flex flex-col gap-2\">\n <h4 class=\"text-sm font-semibold\">{{ t(\"content\") }}</h4>\n <mt-notification-content-preview\n [preview]=\"runner.result()\"\n [status]=\"runner.status()\"\n [errorCode]=\"runner.errorCode()\"\n height=\"20rem\"\n (retry)=\"runner.retry()\"\n />\n </section>\n </div>\n\n <div [class]=\"modalService.footerClass\">\n <div class=\"flex w-full justify-end gap-2\">\n <mt-button\n [label]=\"t('cancel')\"\n severity=\"secondary\"\n variant=\"outlined\"\n (onClick)=\"cancel()\"\n />\n <mt-button\n [label]=\"t('confirm.' + mode())\"\n [tooltip]=\"previewReady() ? '' : t('waiting-preview')\"\n [disabled]=\"!previewReady()\"\n (onClick)=\"confirm()\"\n />\n </div>\n </div>\n</ng-container>\n" }]
|
|
1544
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: true }] }], eventId: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventId", required: true }] }], request: [{ type: i0.Input, args: [{ isSignal: true, alias: "request", required: true }] }], recipients: [{ type: i0.Input, args: [{ isSignal: true, alias: "recipients", required: false }] }], condition: [{ type: i0.Input, args: [{ isSignal: true, alias: "condition", required: false }] }], requireLanguageReview: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireLanguageReview", required: false }] }], isEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEnabled", required: false }] }] } });
|
|
1545
|
+
|
|
1546
|
+
const WIDE_LAYOUT_MIN_WIDTH = 960;
|
|
1547
|
+
const DRAWER_STYLE_CLASS = '!shadow-none !w-full !max-w-full md:!w-[70%] lg:!w-[50%]';
|
|
1548
|
+
/**
|
|
1549
|
+
* Control Panel notification templates for one scope (Level, Module,
|
|
1550
|
+
* LevelModule, Request/Step, EscalationRequest/Step, Delegation).
|
|
1551
|
+
*
|
|
1552
|
+
* Saving and previewing a draft never changes what is sent: only Publish
|
|
1553
|
+
* (snapshot the saved draft) and Turn on/off do, and both go through an
|
|
1554
|
+
* explicit review of the actual content and recipients first. Every write
|
|
1555
|
+
* carries the last revision read; a conflict keeps the user's edits.
|
|
1556
|
+
*/
|
|
1557
|
+
class NotificationTemplate {
|
|
1558
|
+
facade = inject(NotificationFacade);
|
|
1559
|
+
messages = inject(NotificationMessages);
|
|
1560
|
+
transloco = inject(TranslocoService);
|
|
1561
|
+
confirmation = inject(ConfirmationService);
|
|
1562
|
+
modal = inject(ModalService);
|
|
1563
|
+
toast = inject(ToastService);
|
|
1564
|
+
destroyRef = inject(DestroyRef);
|
|
1565
|
+
lang = toSignal(this.transloco.langChanges$, {
|
|
1566
|
+
initialValue: this.transloco.getActiveLang(),
|
|
1567
|
+
});
|
|
1568
|
+
// ==========================================================================
|
|
1569
|
+
// Store
|
|
1570
|
+
// ==========================================================================
|
|
897
1571
|
events = this.facade.events;
|
|
898
1572
|
templates = this.facade.templates;
|
|
899
|
-
receivers = this.facade.receivers;
|
|
900
|
-
propertyGroups = this.facade.propertyGroups;
|
|
901
|
-
// Selection Signals
|
|
902
|
-
selectedEventName = this.facade.selectedEventName;
|
|
903
|
-
selectedTemplateId = this.facade.selectedTemplateId;
|
|
904
1573
|
selectedEvent = this.facade.selectedEvent;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
initialValue: this.channelControl.value,
|
|
1574
|
+
selectedEventId = this.facade.selectedEventId;
|
|
1575
|
+
selectedTemplateId = this.facade.selectedTemplateId;
|
|
1576
|
+
template = this.facade.selectedTemplate;
|
|
1577
|
+
context = this.facade.context;
|
|
1578
|
+
defaults = this.facade.defaults;
|
|
1579
|
+
publications = this.facade.publications;
|
|
1580
|
+
isMutating = this.facade.isMutating;
|
|
1581
|
+
// ==========================================================================
|
|
1582
|
+
// Controls
|
|
1583
|
+
// ==========================================================================
|
|
1584
|
+
eventControl = new FormControl(null);
|
|
1585
|
+
placeholderControl = new FormControl(null);
|
|
1586
|
+
recipientControl = new FormControl(null);
|
|
1587
|
+
form = new FormGroup({
|
|
1588
|
+
name: new FormControl('', {
|
|
1589
|
+
nonNullable: true,
|
|
1590
|
+
validators: [Validators.required, Validators.maxLength(250)],
|
|
1591
|
+
}),
|
|
1592
|
+
subject: new FormControl('', {
|
|
1593
|
+
nonNullable: true,
|
|
1594
|
+
validators: [Validators.maxLength(250)],
|
|
1595
|
+
}),
|
|
1596
|
+
emailTemplate: new FormControl('', { nonNullable: true }),
|
|
1597
|
+
smsTemplate: new FormControl('', {
|
|
1598
|
+
nonNullable: true,
|
|
1599
|
+
validators: [Validators.maxLength(4000)],
|
|
1600
|
+
}),
|
|
1601
|
+
appEn: new FormControl('', { nonNullable: true }),
|
|
1602
|
+
appAr: new FormControl('', { nonNullable: true }),
|
|
1603
|
+
conditionFormula: new FormControl('', { nonNullable: true }),
|
|
936
1604
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1605
|
+
formValue = signal(this.form.getRawValue(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
|
|
1606
|
+
/** Mirrors the tabs' own `active` model so a cancelled switch can snap back. */
|
|
1607
|
+
activeTab = signal(null, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
1608
|
+
activeField = signal('emailTemplate', ...(ngDevMode ? [{ debugName: "activeField" }] : /* istanbul ignore next */ []));
|
|
1609
|
+
channelIcons = CHANNEL_ICONS;
|
|
1610
|
+
host = inject(ElementRef);
|
|
1611
|
+
/** Editor and preview side by side; see the ResizeObserver in the constructor. */
|
|
1612
|
+
wide = signal(false, ...(ngDevMode ? [{ debugName: "wide" }] : /* istanbul ignore next */ []));
|
|
1613
|
+
conflict = signal(false, ...(ngDevMode ? [{ debugName: "conflict" }] : /* istanbul ignore next */ []));
|
|
1614
|
+
writeDiagnostics = signal([], ...(ngDevMode ? [{ debugName: "writeDiagnostics" }] : /* istanbul ignore next */ []));
|
|
1615
|
+
preview = new NotificationPreviewRunner(this.facade, this.destroyRef);
|
|
1616
|
+
/** A newly opened template renders at once instead of after the typing pause. */
|
|
1617
|
+
renderPreviewNow = false;
|
|
1618
|
+
emailEditor = viewChild('emailEditor', { ...(ngDevMode ? { debugName: "emailEditor" } : /* istanbul ignore next */ {}), read: EmailVisualEditor });
|
|
1619
|
+
appEnEditor = viewChild('appEnEditor', { ...(ngDevMode ? { debugName: "appEnEditor" } : /* istanbul ignore next */ {}), read: EmailVisualEditor });
|
|
1620
|
+
appArEditor = viewChild('appArEditor', { ...(ngDevMode ? { debugName: "appArEditor" } : /* istanbul ignore next */ {}), read: EmailVisualEditor });
|
|
1621
|
+
smsSource = viewChild('smsSource', ...(ngDevMode ? [{ debugName: "smsSource" }] : /* istanbul ignore next */ []));
|
|
1622
|
+
// ==========================================================================
|
|
1623
|
+
// Derived state
|
|
1624
|
+
// ==========================================================================
|
|
1625
|
+
channel = computed(() => this.template()?.channel ?? null, ...(ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []));
|
|
1626
|
+
status = computed(() => {
|
|
1627
|
+
const template = this.template();
|
|
1628
|
+
return template ? templateStatus(template) : null;
|
|
1629
|
+
}, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1630
|
+
unpublishedChanges = computed(() => {
|
|
1631
|
+
const template = this.template();
|
|
1632
|
+
return template
|
|
1633
|
+
? hasUnpublishedChanges(template, this.publications())
|
|
1634
|
+
: null;
|
|
1635
|
+
}, ...(ngDevMode ? [{ debugName: "unpublishedChanges" }] : /* istanbul ignore next */ []));
|
|
1636
|
+
defaultOwned = computed(() => {
|
|
1637
|
+
const template = this.template();
|
|
1638
|
+
return !!template && isDefaultOwned(template, this.defaults());
|
|
1639
|
+
}, ...(ngDevMode ? [{ debugName: "defaultOwned" }] : /* istanbul ignore next */ []));
|
|
1640
|
+
/** Unsaved edits, measured against the server baseline rather than control flags. */
|
|
1641
|
+
dirty = computed(() => {
|
|
1642
|
+
const template = this.template();
|
|
1643
|
+
if (!template)
|
|
971
1644
|
return false;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1645
|
+
const value = this.formValue();
|
|
1646
|
+
const same = (current, saved) => current === (saved ?? '');
|
|
1647
|
+
if (!same(value.name, template.name))
|
|
1648
|
+
return true;
|
|
1649
|
+
if (value.conditionFormula.trim() !== (template.conditionFormula ?? '').trim()) {
|
|
1650
|
+
return true;
|
|
1651
|
+
}
|
|
1652
|
+
switch (template.channel) {
|
|
1653
|
+
case 'Email':
|
|
1654
|
+
return (!same(value.subject, template.subject) ||
|
|
1655
|
+
!same(value.emailTemplate, template.emailTemplate));
|
|
1656
|
+
case 'Sms':
|
|
1657
|
+
return !same(value.smsTemplate, template.smsTemplate);
|
|
1658
|
+
case 'App':
|
|
1659
|
+
return (!same(value.appEn, template.appTemplate?.en) ||
|
|
1660
|
+
!same(value.appAr, template.appTemplate?.ar));
|
|
1661
|
+
}
|
|
1662
|
+
}, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
|
|
1663
|
+
nameValid = computed(() => this.formValue().name.trim().length > 0 &&
|
|
1664
|
+
this.formValue().name.length <= 250, ...(ngDevMode ? [{ debugName: "nameValid" }] : /* istanbul ignore next */ []));
|
|
1665
|
+
canSave = computed(() => this.dirty() && this.nameValid() && !this.isMutating(), ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
1666
|
+
canPublish = computed(() => !!this.template() && !this.dirty() && !this.isMutating(), ...(ngDevMode ? [{ debugName: "canPublish" }] : /* istanbul ignore next */ []));
|
|
1667
|
+
canTurnOn = computed(() => {
|
|
1668
|
+
const template = this.template();
|
|
1669
|
+
return (!!template &&
|
|
1670
|
+
template.publishedRevision != null &&
|
|
1671
|
+
!!currentPublication(template, this.publications()) &&
|
|
1672
|
+
!this.isMutating());
|
|
1673
|
+
}, ...(ngDevMode ? [{ debugName: "canTurnOn" }] : /* istanbul ignore next */ []));
|
|
1674
|
+
templateTabs = computed(() => this.templates().map((template) => ({
|
|
1675
|
+
label: template.name || this.messages.t('untitled'),
|
|
1676
|
+
value: template.id,
|
|
1677
|
+
icon: CHANNEL_ICONS[template.channel],
|
|
985
1678
|
})), ...(ngDevMode ? [{ debugName: "templateTabs" }] : /* istanbul ignore next */ []));
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1679
|
+
placeholderGroups = computed(() => {
|
|
1680
|
+
const ar = this.lang() === 'ar';
|
|
1681
|
+
return (this.context()?.properties ?? []).map((group) => ({
|
|
1682
|
+
groupName: group.groupName,
|
|
1683
|
+
properties: group.properties.map((property) => ({
|
|
1684
|
+
key: property.key,
|
|
1685
|
+
label: (ar ? property.nameAr : property.nameEn) || property.name,
|
|
1686
|
+
})),
|
|
1687
|
+
}));
|
|
1688
|
+
}, ...(ngDevMode ? [{ debugName: "placeholderGroups" }] : /* istanbul ignore next */ []));
|
|
1689
|
+
/** Image hosts the server accepts, as reported by the latest email preview. */
|
|
1690
|
+
emailImageOrigins = computed(() => {
|
|
1691
|
+
const result = this.preview.result();
|
|
1692
|
+
return result?.channel === 'Email' ? result.approvedImageOrigins : [];
|
|
1693
|
+
}, ...(ngDevMode ? [{ debugName: "emailImageOrigins" }] : /* istanbul ignore next */ []));
|
|
1694
|
+
/** Image hosts the server accepts in App messages, from the latest App preview. */
|
|
1695
|
+
appImageOrigins = computed(() => {
|
|
1696
|
+
const result = this.preview.result();
|
|
1697
|
+
return result?.channel === 'App' ? result.approvedImageOrigins : [];
|
|
1698
|
+
}, ...(ngDevMode ? [{ debugName: "appImageOrigins" }] : /* istanbul ignore next */ []));
|
|
1699
|
+
/** Catalog names by `type:id`, so saved receivers read as the catalog does. */
|
|
1700
|
+
receiverLabels = computed(() => new Map(flattenRecipientCatalog(this.context()?.recipients ?? [], this.lang())
|
|
1701
|
+
.flatMap((group) => group.items)
|
|
1702
|
+
.map((item) => [item.key.toLowerCase(), item.name])), ...(ngDevMode ? [{ debugName: "receiverLabels" }] : /* istanbul ignore next */ []));
|
|
1703
|
+
/** Field labels for the email editor's placeholder chips. */
|
|
1704
|
+
placeholderOptions = computed(() => this.placeholderGroups().flatMap((group) => group.properties), ...(ngDevMode ? [{ debugName: "placeholderOptions" }] : /* istanbul ignore next */ []));
|
|
1705
|
+
recipientGroups = computed(() => {
|
|
1706
|
+
const assigned = new Set((this.template()?.receivers ?? []).map((receiver) => recipientKey(receiver.type, receiver.identifier)));
|
|
1707
|
+
return flattenRecipientCatalog(this.context()?.recipients ?? [], this.lang())
|
|
1708
|
+
.map((group) => ({
|
|
1709
|
+
...group,
|
|
1710
|
+
items: group.items.filter((item) => !assigned.has(item.key)),
|
|
1711
|
+
}))
|
|
1712
|
+
.filter((group) => group.items.length > 0);
|
|
1713
|
+
}, ...(ngDevMode ? [{ debugName: "recipientGroups" }] : /* istanbul ignore next */ []));
|
|
1714
|
+
conditionPropertiesByPath = computed(() => {
|
|
1715
|
+
const ar = this.lang() === 'ar';
|
|
1716
|
+
return {
|
|
1717
|
+
Current: (this.context()?.conditionProperties ?? []).map((property) => ({
|
|
1718
|
+
key: property.key,
|
|
1719
|
+
name: (ar ? property.nameAr : property.nameEn) || property.name,
|
|
1720
|
+
})),
|
|
1721
|
+
};
|
|
1722
|
+
}, ...(ngDevMode ? [{ debugName: "conditionPropertiesByPath" }] : /* istanbul ignore next */ []));
|
|
1723
|
+
conditionKeys = computed(() => (this.context()?.conditionProperties ?? []).map((property) => property.key), ...(ngDevMode ? [{ debugName: "conditionKeys" }] : /* istanbul ignore next */ []));
|
|
1724
|
+
missingDefaults = computed(() => missingDefaultChannels(this.defaults()), ...(ngDevMode ? [{ debugName: "missingDefaults" }] : /* istanbul ignore next */ []));
|
|
1725
|
+
removedDefaults = computed(() => {
|
|
1726
|
+
const defaults = this.defaults();
|
|
1727
|
+
if (!defaults)
|
|
1728
|
+
return [];
|
|
1729
|
+
return defaults.slots
|
|
1730
|
+
.filter((slot) => slot.templateId == null)
|
|
1731
|
+
.map((slot) => ({
|
|
1732
|
+
slot,
|
|
1733
|
+
recipe: defaults.recipes.find((recipe) => recipe.channel === slot.channel) ??
|
|
1734
|
+
null,
|
|
1735
|
+
}))
|
|
1736
|
+
.filter((item) => item.recipe !== null);
|
|
1737
|
+
}, ...(ngDevMode ? [{ debugName: "removedDefaults" }] : /* istanbul ignore next */ []));
|
|
1738
|
+
/** What the live preview renders: the editor's current, unsaved values. */
|
|
1739
|
+
previewRequest = computed(() => {
|
|
1740
|
+
const template = this.template();
|
|
1741
|
+
if (!template)
|
|
1742
|
+
return null;
|
|
1743
|
+
const value = this.formValue();
|
|
1744
|
+
return this.withSamples(templatePreviewRequest({
|
|
1745
|
+
channel: template.channel,
|
|
1746
|
+
subject: value.subject,
|
|
1747
|
+
emailTemplate: value.emailTemplate,
|
|
1748
|
+
smsTemplate: value.smsTemplate,
|
|
1749
|
+
appTemplate: { en: value.appEn, ar: value.appAr },
|
|
1750
|
+
}));
|
|
1751
|
+
}, ...(ngDevMode ? [{ debugName: "previewRequest" }] : /* istanbul ignore next */ []));
|
|
1752
|
+
previewStale = computed(() => {
|
|
1753
|
+
const eventId = this.selectedEventId();
|
|
1754
|
+
const request = this.previewRequest();
|
|
1755
|
+
return (eventId == null ||
|
|
1756
|
+
request == null ||
|
|
1757
|
+
!this.preview.isCurrent(eventId, request));
|
|
1758
|
+
}, ...(ngDevMode ? [{ debugName: "previewStale" }] : /* istanbul ignore next */ []));
|
|
997
1759
|
constructor() {
|
|
1760
|
+
this.form.valueChanges
|
|
1761
|
+
.pipe(takeUntilDestroyed())
|
|
1762
|
+
.subscribe(() => this.formValue.set(this.form.getRawValue()));
|
|
998
1763
|
effect(() => {
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
if (this.
|
|
1002
|
-
this.
|
|
1003
|
-
emitEvent: false,
|
|
1004
|
-
});
|
|
1764
|
+
const eventId = this.selectedEventId();
|
|
1765
|
+
untracked(() => {
|
|
1766
|
+
if (this.eventControl.value !== eventId) {
|
|
1767
|
+
this.eventControl.setValue(eventId, { emitEvent: false });
|
|
1005
1768
|
}
|
|
1006
|
-
}
|
|
1007
|
-
else {
|
|
1008
|
-
this.form.controls['event'].setValue(null, { emitEvent: false });
|
|
1009
|
-
}
|
|
1769
|
+
});
|
|
1010
1770
|
});
|
|
1771
|
+
// A newly selected template replaces the editor; a write on the same
|
|
1772
|
+
// template (recipients, publish, activation) never touches unsaved edits.
|
|
1011
1773
|
effect(() => {
|
|
1012
|
-
this.selectedTemplateId();
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
this.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1774
|
+
const templateId = this.selectedTemplateId();
|
|
1775
|
+
this.activeTab.set(templateId);
|
|
1776
|
+
untracked(() => {
|
|
1777
|
+
this.conflict.set(false);
|
|
1778
|
+
this.writeDiagnostics.set([]);
|
|
1779
|
+
// Never leave the previous template's render (possibly another
|
|
1780
|
+
// channel) on screen under the new one.
|
|
1781
|
+
this.preview.clear();
|
|
1782
|
+
this.renderPreviewNow = true;
|
|
1783
|
+
const template = this.template();
|
|
1784
|
+
if (template) {
|
|
1785
|
+
this.patchFromTemplate(template);
|
|
1786
|
+
this.activeField.set(defaultField(template.channel));
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
this.form.reset();
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1020
1792
|
});
|
|
1021
1793
|
effect(() => {
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1794
|
+
const eventId = this.selectedEventId();
|
|
1795
|
+
const request = this.previewRequest();
|
|
1796
|
+
// The field catalog supplies the preview's sample values; rendering
|
|
1797
|
+
// before it arrives shows "-" and a warning per field for a moment. A
|
|
1798
|
+
// failed catalog load still previews, just without samples. (Not keyed
|
|
1799
|
+
// on the loading flag: a reload cancels and restarts the catalog read.)
|
|
1800
|
+
if (eventId != null &&
|
|
1801
|
+
this.context()?.eventId !== eventId &&
|
|
1802
|
+
this.facade.contextError() == null) {
|
|
1024
1803
|
return;
|
|
1025
1804
|
}
|
|
1026
|
-
|
|
1805
|
+
untracked(() => {
|
|
1806
|
+
if (eventId == null || request == null) {
|
|
1807
|
+
this.preview.clear();
|
|
1808
|
+
}
|
|
1809
|
+
else if (this.renderPreviewNow) {
|
|
1810
|
+
this.renderPreviewNow = false;
|
|
1811
|
+
this.preview.run(eventId, request);
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
this.preview.schedule(eventId, request);
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1027
1817
|
});
|
|
1818
|
+
// Two columns only when the editor itself is wide enough. Hosts range from
|
|
1819
|
+
// a full page to a builder drawer, so the viewport says little; a
|
|
1820
|
+
// ResizeObserver is used rather than container queries, which re-parent
|
|
1821
|
+
// PrimeNG's in-place overlays.
|
|
1822
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1823
|
+
const observer = new ResizeObserver(([entry]) => this.wide.set(entry.contentRect.width >= WIDE_LAYOUT_MIN_WIDTH));
|
|
1824
|
+
observer.observe(this.host.nativeElement);
|
|
1825
|
+
this.destroyRef.onDestroy(() => observer.disconnect());
|
|
1826
|
+
}
|
|
1028
1827
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1828
|
+
onBeforeUnload(event) {
|
|
1829
|
+
if (this.dirty() || this.isMutating())
|
|
1830
|
+
event.preventDefault();
|
|
1831
|
+
}
|
|
1832
|
+
// ==========================================================================
|
|
1833
|
+
// Selection
|
|
1834
|
+
// ==========================================================================
|
|
1835
|
+
onEventChange(eventId) {
|
|
1836
|
+
if (eventId == null) {
|
|
1837
|
+
// Clearing the picker is not a selection; keep showing the current event.
|
|
1838
|
+
this.eventControl.setValue(this.selectedEventId(), { emitEvent: false });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (eventId === this.selectedEventId())
|
|
1842
|
+
return;
|
|
1843
|
+
this.confirmDiscard().subscribe((discard) => {
|
|
1844
|
+
if (discard) {
|
|
1845
|
+
this.facade.selectEvent(eventId);
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
this.eventControl.setValue(this.selectedEventId(), {
|
|
1849
|
+
emitEvent: false,
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1035
1853
|
}
|
|
1036
1854
|
onTemplateTabChange(templateId) {
|
|
1037
|
-
this.
|
|
1038
|
-
|
|
1039
|
-
// Channel is fixed per template - set on creation only
|
|
1040
|
-
onAddProperty(propertyKey) {
|
|
1041
|
-
if (!propertyKey)
|
|
1855
|
+
const current = this.selectedTemplateId();
|
|
1856
|
+
if (templateId === current)
|
|
1042
1857
|
return;
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (group) {
|
|
1048
|
-
const enCtrl = group.get('en');
|
|
1049
|
-
const arCtrl = group.get('ar');
|
|
1050
|
-
enCtrl?.setValue(`${enCtrl.value ?? ''} ${tag}`);
|
|
1051
|
-
arCtrl?.setValue(`${arCtrl.value ?? ''} ${tag}`);
|
|
1858
|
+
this.activeTab.set(templateId);
|
|
1859
|
+
this.confirmDiscard().subscribe((discard) => {
|
|
1860
|
+
if (discard) {
|
|
1861
|
+
this.facade.selectTemplate(templateId);
|
|
1052
1862
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1057
|
-
setTimeout(() => {
|
|
1058
|
-
this.propertyPickerControl.reset();
|
|
1059
|
-
}, 0);
|
|
1863
|
+
else {
|
|
1864
|
+
this.activeTab.set(current);
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1060
1867
|
}
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1064
|
-
|
|
1868
|
+
retryLoad() {
|
|
1869
|
+
const eventId = this.selectedEventId();
|
|
1870
|
+
if (this.facade.eventsError() || eventId == null) {
|
|
1871
|
+
this.facade.loadEvents();
|
|
1065
1872
|
return;
|
|
1066
1873
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1874
|
+
this.facade.reloadDefaults(eventId);
|
|
1875
|
+
this.facade.loadEvents();
|
|
1876
|
+
}
|
|
1877
|
+
// ==========================================================================
|
|
1878
|
+
// Content editing
|
|
1879
|
+
// ==========================================================================
|
|
1880
|
+
setActiveField(field) {
|
|
1881
|
+
this.activeField.set(field);
|
|
1882
|
+
}
|
|
1883
|
+
onInsertPlaceholder(key) {
|
|
1884
|
+
if (!key)
|
|
1885
|
+
return;
|
|
1886
|
+
// After the select has finished applying its own pick, or the reset is overwritten.
|
|
1887
|
+
setTimeout(() => this.placeholderControl.reset());
|
|
1888
|
+
const field = this.activeField();
|
|
1889
|
+
const label = this.placeholderOptions().find((item) => item.key === key)?.label;
|
|
1890
|
+
const editor = field === 'emailTemplate'
|
|
1891
|
+
? this.emailEditor()
|
|
1892
|
+
: field === 'appEn'
|
|
1893
|
+
? this.appEnEditor()
|
|
1894
|
+
: field === 'appAr'
|
|
1895
|
+
? this.appArEditor()
|
|
1896
|
+
: undefined;
|
|
1897
|
+
if (editor) {
|
|
1898
|
+
editor.insertToken(key, label);
|
|
1069
1899
|
return;
|
|
1070
1900
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1901
|
+
// SMS: plain text at the caret.
|
|
1902
|
+
const token = `{{${key}}}`;
|
|
1903
|
+
const control = this.form.controls.smsTemplate;
|
|
1904
|
+
const textarea = this.smsSource()?.nativeElement.querySelector('textarea');
|
|
1905
|
+
const start = textarea?.selectionStart ?? control.value.length;
|
|
1906
|
+
const end = textarea?.selectionEnd ?? start;
|
|
1907
|
+
control.setValue(control.value.slice(0, start) + token + control.value.slice(end));
|
|
1908
|
+
const caret = start + token.length;
|
|
1909
|
+
// ngModel writes the new value into the textarea asynchronously, which
|
|
1910
|
+
// moves the caret to the end; restore it after that write.
|
|
1911
|
+
setTimeout(() => {
|
|
1912
|
+
textarea?.focus();
|
|
1913
|
+
textarea?.setSelectionRange(caret, caret);
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
onSave() {
|
|
1917
|
+
const template = this.template();
|
|
1918
|
+
if (!template || !this.canSave()) {
|
|
1919
|
+
this.form.markAllAsTouched();
|
|
1074
1920
|
return;
|
|
1075
1921
|
}
|
|
1076
|
-
this.addingReceiverIds.update((ids) => [...ids, receiverId]);
|
|
1077
1922
|
this.facade
|
|
1078
|
-
.
|
|
1079
|
-
receivers: [
|
|
1080
|
-
{
|
|
1081
|
-
id: selectedReceiver.id,
|
|
1082
|
-
type: selectedReceiver.type,
|
|
1083
|
-
},
|
|
1084
|
-
],
|
|
1085
|
-
})
|
|
1923
|
+
.saveTemplate(template.id, this.buildSavePayload(template))
|
|
1086
1924
|
.subscribe({
|
|
1087
|
-
|
|
1088
|
-
|
|
1925
|
+
next: () => {
|
|
1926
|
+
const saved = this.template();
|
|
1927
|
+
// Show exactly what the server stored (it sanitizes on save).
|
|
1928
|
+
if (saved)
|
|
1929
|
+
this.patchFromTemplate(saved);
|
|
1930
|
+
this.afterWrite('saved');
|
|
1931
|
+
},
|
|
1932
|
+
error: (error) => this.onWriteError(error),
|
|
1089
1933
|
});
|
|
1090
|
-
setTimeout(() => this.receiverPickerControl.reset(), 0);
|
|
1091
1934
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
this.
|
|
1098
|
-
complete: () => {
|
|
1099
|
-
this.deletingReceiverIds.update((ids) => ids.filter((id) => id !== receiver.id));
|
|
1100
|
-
},
|
|
1101
|
-
error: () => {
|
|
1102
|
-
this.deletingReceiverIds.update((ids) => ids.filter((id) => id !== receiver.id));
|
|
1103
|
-
},
|
|
1104
|
-
});
|
|
1105
|
-
},
|
|
1935
|
+
onDiscard() {
|
|
1936
|
+
// Discarded edits cannot be brought back, so ask first (as switching does).
|
|
1937
|
+
this.confirmDiscard().subscribe((discard) => {
|
|
1938
|
+
const template = this.template();
|
|
1939
|
+
if (discard && template)
|
|
1940
|
+
this.patchFromTemplate(template);
|
|
1106
1941
|
});
|
|
1107
1942
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1943
|
+
// ==========================================================================
|
|
1944
|
+
// Recipients
|
|
1945
|
+
// ==========================================================================
|
|
1946
|
+
onAddRecipient(key) {
|
|
1947
|
+
const template = this.template();
|
|
1948
|
+
setTimeout(() => this.recipientControl.reset());
|
|
1949
|
+
if (!key || !template || this.isMutating())
|
|
1950
|
+
return;
|
|
1951
|
+
const option = this.recipientGroups()
|
|
1952
|
+
.flatMap((group) => group.items)
|
|
1953
|
+
.find((item) => item.key === key);
|
|
1954
|
+
if (!option)
|
|
1955
|
+
return;
|
|
1956
|
+
this.facade
|
|
1957
|
+
.addReceivers(template.id, template.revision, [
|
|
1958
|
+
{ type: option.type, id: option.id },
|
|
1959
|
+
])
|
|
1960
|
+
.subscribe({
|
|
1961
|
+
next: () => this.afterWrite(null),
|
|
1962
|
+
error: (error) => this.onWriteError(error),
|
|
1963
|
+
});
|
|
1110
1964
|
}
|
|
1111
|
-
|
|
1112
|
-
const template = this.
|
|
1113
|
-
if (!template || this.
|
|
1114
|
-
this.form.markAllAsTouched();
|
|
1965
|
+
onRemoveRecipient(receiver) {
|
|
1966
|
+
const template = this.template();
|
|
1967
|
+
if (!template || receiver.isSystem || this.isMutating())
|
|
1115
1968
|
return;
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1969
|
+
this.confirmation.confirmDelete({
|
|
1970
|
+
type: 'dialog',
|
|
1971
|
+
message: this.messages.t('recipients.remove-message', {
|
|
1972
|
+
name: this.receiverName(receiver),
|
|
1973
|
+
}),
|
|
1974
|
+
accept: () => this.facade.deleteReceiver(receiver.id, template.revision).subscribe({
|
|
1975
|
+
next: () => this.afterWrite(null),
|
|
1976
|
+
error: (error) => this.onWriteError(error),
|
|
1977
|
+
}),
|
|
1978
|
+
});
|
|
1120
1979
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1980
|
+
/**
|
|
1981
|
+
* The catalog's name first: ready-made templates come back with the raw
|
|
1982
|
+
* identifier as their saved name (`creator`), while the catalog has the
|
|
1983
|
+
* localized label.
|
|
1984
|
+
*/
|
|
1985
|
+
receiverName(receiver) {
|
|
1986
|
+
const fromCatalog = this.receiverLabels().get(recipientKey(receiver.type, receiver.identifier).toLowerCase());
|
|
1987
|
+
if (fromCatalog)
|
|
1988
|
+
return fromCatalog;
|
|
1989
|
+
const name = receiver.name;
|
|
1990
|
+
if (typeof name === 'string')
|
|
1991
|
+
return name || receiver.identifier || '';
|
|
1992
|
+
const ar = this.lang() === 'ar';
|
|
1993
|
+
return ((ar ? name?.ar : name?.en) || name?.display || receiver.identifier || '');
|
|
1994
|
+
}
|
|
1995
|
+
// ==========================================================================
|
|
1996
|
+
// Lifecycle
|
|
1997
|
+
// ==========================================================================
|
|
1998
|
+
onPublish() {
|
|
1999
|
+
const template = this.template();
|
|
2000
|
+
if (!template || !this.canPublish())
|
|
2001
|
+
return;
|
|
2002
|
+
const revision = template.revision;
|
|
2003
|
+
this.review('publish', {
|
|
2004
|
+
request: templatePreviewRequest(template),
|
|
2005
|
+
recipients: template.receivers.map((receiver) => ({
|
|
2006
|
+
name: this.receiverName(receiver),
|
|
2007
|
+
type: receiver.type,
|
|
2008
|
+
})),
|
|
2009
|
+
condition: template.conditionFormula,
|
|
2010
|
+
isEnabled: template.isEnabled,
|
|
2011
|
+
}).subscribe((reviews) => this.facade.publish(template.id, revision, reviews).subscribe({
|
|
2012
|
+
next: () => this.afterWrite('published'),
|
|
2013
|
+
error: (error) => this.onWriteError(error),
|
|
2014
|
+
}));
|
|
2015
|
+
}
|
|
2016
|
+
onTurnOn() {
|
|
2017
|
+
const template = this.template();
|
|
2018
|
+
if (!template || !this.canTurnOn())
|
|
2019
|
+
return;
|
|
2020
|
+
const publication = currentPublication(template, this.publications());
|
|
2021
|
+
if (!publication)
|
|
2022
|
+
return;
|
|
2023
|
+
const revision = template.revision;
|
|
2024
|
+
// Turning on sends the *published* version — review that, not the draft.
|
|
2025
|
+
this.review('enable', publicationReview(publication, (receiver) => this.receiverName(receiver))).subscribe((reviews) => this.facade.turnOn(template.id, revision, reviews).subscribe({
|
|
2026
|
+
next: () => this.afterWrite('turned-on'),
|
|
2027
|
+
error: (error) => this.onWriteError(error),
|
|
2028
|
+
}));
|
|
2029
|
+
}
|
|
2030
|
+
onTurnOff() {
|
|
2031
|
+
const template = this.template();
|
|
2032
|
+
if (!template || this.isMutating())
|
|
1124
2033
|
return;
|
|
1125
|
-
|
|
2034
|
+
const revision = template.revision;
|
|
2035
|
+
this.confirmation.confirm({
|
|
1126
2036
|
type: 'dialog',
|
|
1127
|
-
|
|
1128
|
-
|
|
2037
|
+
header: this.messages.t('turn-off.header'),
|
|
2038
|
+
message: this.messages.t('turn-off.message'),
|
|
2039
|
+
icon: 'general.toggle-01-left',
|
|
2040
|
+
acceptLabel: this.messages.t('actions.turn-off'),
|
|
1129
2041
|
acceptButton: { severity: 'danger' },
|
|
1130
|
-
accept: () => this.facade.
|
|
2042
|
+
accept: () => this.facade.turnOff(template.id, revision).subscribe({
|
|
2043
|
+
next: () => this.afterWrite('turned-off'),
|
|
2044
|
+
error: (error) => this.onWriteError(error),
|
|
2045
|
+
}),
|
|
1131
2046
|
});
|
|
1132
2047
|
}
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
if (
|
|
2048
|
+
onOpenHistory() {
|
|
2049
|
+
const eventId = this.selectedEventId();
|
|
2050
|
+
if (eventId == null)
|
|
1136
2051
|
return;
|
|
1137
|
-
const ref = this.
|
|
1138
|
-
header: this.
|
|
1139
|
-
styleClass:
|
|
2052
|
+
const ref = this.modal.openModal(TemplateHistoryDrawer, 'drawer', {
|
|
2053
|
+
header: this.messages.t('history.title'),
|
|
2054
|
+
styleClass: DRAWER_STYLE_CLASS,
|
|
2055
|
+
position: 'end',
|
|
2056
|
+
dismissableMask: true,
|
|
2057
|
+
dismissible: true,
|
|
2058
|
+
inputValues: {
|
|
2059
|
+
eventId,
|
|
2060
|
+
draftDirty: this.dirty(),
|
|
2061
|
+
historyLimit: this.context()?.publicationHistoryLimit ?? null,
|
|
2062
|
+
},
|
|
1140
2063
|
});
|
|
1141
2064
|
ref.onClose.subscribe((result) => {
|
|
1142
|
-
if (result?.
|
|
1143
|
-
this.
|
|
2065
|
+
if (result?.publication)
|
|
2066
|
+
this.restoreVersion(result.publication);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
restoreVersion(publication) {
|
|
2070
|
+
const template = this.template();
|
|
2071
|
+
if (!template || this.dirty() || this.isMutating())
|
|
2072
|
+
return;
|
|
2073
|
+
const revision = template.revision;
|
|
2074
|
+
this.review('restore', {
|
|
2075
|
+
...publicationReview(publication, (receiver) => this.receiverName(receiver)),
|
|
2076
|
+
isEnabled: template.isEnabled,
|
|
2077
|
+
}).subscribe((reviews) => this.facade
|
|
2078
|
+
.restorePublication(template.id, revision, publication.revision, reviews)
|
|
2079
|
+
.subscribe({
|
|
2080
|
+
next: () => {
|
|
2081
|
+
const restored = this.template();
|
|
2082
|
+
if (restored)
|
|
2083
|
+
this.patchFromTemplate(restored);
|
|
2084
|
+
this.afterWrite('restored');
|
|
2085
|
+
},
|
|
2086
|
+
error: (error) => this.onWriteError(error),
|
|
2087
|
+
}));
|
|
2088
|
+
}
|
|
2089
|
+
onDelete() {
|
|
2090
|
+
const template = this.template();
|
|
2091
|
+
if (!template || template.isSystem || this.isMutating())
|
|
2092
|
+
return;
|
|
2093
|
+
this.confirmation.confirmDelete({
|
|
2094
|
+
type: 'dialog',
|
|
2095
|
+
message: this.messages.t(this.defaultOwned() ? 'delete.default-message' : 'delete.message'),
|
|
2096
|
+
accept: () => this.facade.deleteTemplate(template.id, template.revision).subscribe({
|
|
2097
|
+
next: () => this.toast.success(this.messages.t('toast.deleted')),
|
|
2098
|
+
error: (error) => this.onWriteError(error),
|
|
2099
|
+
}),
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
onAddTemplate() {
|
|
2103
|
+
const event = this.selectedEvent();
|
|
2104
|
+
if (!event || this.isMutating())
|
|
2105
|
+
return;
|
|
2106
|
+
this.confirmDiscard().subscribe((discard) => {
|
|
2107
|
+
if (!discard)
|
|
2108
|
+
return;
|
|
2109
|
+
const ref = this.modal.openModal(AddTemplateDialog, 'dialog', {
|
|
2110
|
+
header: this.messages.t('addTemplate'),
|
|
2111
|
+
styleClass: '!w-[34rem] max-w-[96vw]',
|
|
2112
|
+
dismissible: true,
|
|
2113
|
+
inputValues: {
|
|
2114
|
+
channels: this.context()?.channels ?? NOTIFICATION_CHANNELS,
|
|
2115
|
+
},
|
|
2116
|
+
});
|
|
2117
|
+
ref.onClose.subscribe((result) => {
|
|
2118
|
+
if (!result)
|
|
2119
|
+
return;
|
|
2120
|
+
this.facade
|
|
2121
|
+
.createTemplate(event.id, {
|
|
2122
|
+
channel: result.channel,
|
|
1144
2123
|
name: result.name,
|
|
1145
|
-
|
|
2124
|
+
subject: null,
|
|
2125
|
+
emailTemplate: null,
|
|
2126
|
+
smsTemplate: null,
|
|
2127
|
+
appTemplate: null,
|
|
2128
|
+
conditionFormula: null,
|
|
2129
|
+
receivers: [],
|
|
2130
|
+
})
|
|
2131
|
+
.subscribe({
|
|
2132
|
+
next: () => this.afterWrite('created'),
|
|
2133
|
+
error: (error) => this.onWriteError(error),
|
|
1146
2134
|
});
|
|
1147
|
-
}
|
|
2135
|
+
});
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
onTestEmail() {
|
|
2139
|
+
const event = this.selectedEvent();
|
|
2140
|
+
const template = this.template();
|
|
2141
|
+
if (!event || !template || template.channel !== 'Email')
|
|
2142
|
+
return;
|
|
2143
|
+
if (this.dirty()) {
|
|
2144
|
+
this.toast.warn(this.messages.t('saveBeforeEmailTest'));
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
// Sends the saved draft — never the editor's unsaved values.
|
|
2148
|
+
this.modal.openModal(EmailUserTestDialog, 'dialog', {
|
|
2149
|
+
header: this.messages.t('testEmail'),
|
|
2150
|
+
styleClass: '!w-[96vw] sm:!w-[38rem]',
|
|
2151
|
+
inputValues: {
|
|
2152
|
+
kind: 'template',
|
|
2153
|
+
eventId: event.id,
|
|
2154
|
+
templateId: template.id,
|
|
2155
|
+
sourceLabel: `${event.name} — ${template.name ?? ''}`,
|
|
2156
|
+
// The same readable values as the preview, instead of a field per
|
|
2157
|
+
// placeholder.
|
|
2158
|
+
sampleValues: previewSamples({
|
|
2159
|
+
channel: 'Email',
|
|
2160
|
+
subject: template.subject,
|
|
2161
|
+
emailTemplate: template.emailTemplate,
|
|
2162
|
+
}, this.context()?.properties ?? [], this.transloco.getActiveLang()),
|
|
2163
|
+
},
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
// ==========================================================================
|
|
2167
|
+
// Defaults
|
|
2168
|
+
// ==========================================================================
|
|
2169
|
+
onInitializeDefaults() {
|
|
2170
|
+
const eventId = this.selectedEventId();
|
|
2171
|
+
if (eventId == null || this.isMutating())
|
|
2172
|
+
return;
|
|
2173
|
+
const autoApp = (this.defaults()?.recipes ?? []).some((recipe) => recipe.autoPublishApp &&
|
|
2174
|
+
this.missingDefaults().includes(recipe.channel));
|
|
2175
|
+
this.facade.initializeDefaults(eventId).subscribe({
|
|
2176
|
+
next: () => this.toast.success(this.messages.t(autoApp ? 'defaults.added-app-on' : 'defaults.added')),
|
|
2177
|
+
error: (error) => this.onWriteError(error),
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
onRestoreDefault(channel) {
|
|
2181
|
+
const eventId = this.selectedEventId();
|
|
2182
|
+
if (eventId == null || this.isMutating())
|
|
2183
|
+
return;
|
|
2184
|
+
// Review the catalog as it is now, not as it was when the page loaded.
|
|
2185
|
+
this.facade.reloadDefaults(eventId).subscribe(() => {
|
|
2186
|
+
const defaults = this.defaults();
|
|
2187
|
+
const slot = defaults?.slots.find((item) => item.channel === channel && item.templateId == null);
|
|
2188
|
+
const recipe = defaults?.recipes.find((item) => item.channel === channel);
|
|
2189
|
+
if (!slot || !recipe)
|
|
2190
|
+
return;
|
|
2191
|
+
this.review('restoreDefault', {
|
|
2192
|
+
request: templatePreviewRequest(recipe.draft),
|
|
2193
|
+
recipients: recipe.draft.receivers.map((receiver) => ({
|
|
2194
|
+
name: this.receiverName(receiver),
|
|
2195
|
+
type: receiver.type,
|
|
2196
|
+
})),
|
|
2197
|
+
condition: recipe.draft.conditionFormula,
|
|
2198
|
+
}).subscribe(() => this.facade
|
|
2199
|
+
.restoreDefault(eventId, {
|
|
2200
|
+
channel,
|
|
2201
|
+
expectedSlotRevision: slot.revision,
|
|
2202
|
+
recipeHash: recipe.recipeHash,
|
|
2203
|
+
})
|
|
2204
|
+
.subscribe({
|
|
2205
|
+
next: () => this.afterWrite('default-restored'),
|
|
2206
|
+
error: (error) => this.onWriteError(error),
|
|
2207
|
+
}));
|
|
1148
2208
|
});
|
|
1149
2209
|
}
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
2210
|
+
// ==========================================================================
|
|
2211
|
+
// Conflicts and errors
|
|
2212
|
+
// ==========================================================================
|
|
1153
2213
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
* effect (or the user's own pick) put in that control. An event with no
|
|
1158
|
-
* templates yet — a brand-new one, or one whose last template was just
|
|
1159
|
-
* deleted — resolves `selectedTemplate` to null, and this effect runs after
|
|
1160
|
-
* the event effect in the same flush: resetting the whole group there would
|
|
1161
|
-
* blank the event picker the user just used, while `selectedEvent()` stays
|
|
1162
|
-
* truthy and keeps the tabs/add-template row visible. Leaving the control
|
|
1163
|
-
* alone also keeps the two effects ordering-independent — on a full
|
|
1164
|
-
* `ResetNotificationState` the event effect has already nulled it, and that
|
|
1165
|
-
* null is carried through.
|
|
1166
|
-
*
|
|
1167
|
-
* `channel` is restored through {@link channelControl} with events enabled
|
|
1168
|
-
* because `channelSignal` is a `toSignal` of its `valueChanges` and is the
|
|
1169
|
-
* only consumer of them on this form — a silent reset would leave
|
|
1170
|
-
* `activeChannel()` reporting the previous template's channel.
|
|
2214
|
+
* After a revision conflict: re-read the latest version, then either keep
|
|
2215
|
+
* the user's edits on top of it (the next Save applies them deliberately)
|
|
2216
|
+
* or replace them with what is saved.
|
|
1171
2217
|
*/
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
appendToControl(key, value) {
|
|
1180
|
-
const ctrl = this.form.controls[key];
|
|
1181
|
-
ctrl?.setValue(`${ctrl.value ?? ''} ${value}`);
|
|
1182
|
-
}
|
|
1183
|
-
patchForm(template) {
|
|
1184
|
-
const channel = template.channel || 'email';
|
|
1185
|
-
this.form.patchValue({
|
|
1186
|
-
name: template.name,
|
|
1187
|
-
channel: channel,
|
|
1188
|
-
});
|
|
1189
|
-
// Patch all channel controls dynamically
|
|
1190
|
-
const t = template;
|
|
1191
|
-
for (const ch of CHANNEL_CONFIGS) {
|
|
1192
|
-
if (ch.templateKey) {
|
|
1193
|
-
this.form
|
|
1194
|
-
.get(ch.templateKey)
|
|
1195
|
-
?.setValue(t[ch.templateKey] ?? '');
|
|
1196
|
-
}
|
|
1197
|
-
if (ch.translatableKey) {
|
|
1198
|
-
const trans = t[ch.translatableKey] ?? {};
|
|
1199
|
-
this.form.get(ch.translatableKey)?.patchValue({
|
|
1200
|
-
en: trans.en ?? '',
|
|
1201
|
-
ar: trans.ar ?? '',
|
|
1202
|
-
});
|
|
2218
|
+
resolveConflict(keepMine) {
|
|
2219
|
+
this.facade.loadEvents().subscribe(() => {
|
|
2220
|
+
if (!keepMine) {
|
|
2221
|
+
const template = this.template();
|
|
2222
|
+
if (template)
|
|
2223
|
+
this.patchFromTemplate(template);
|
|
1203
2224
|
}
|
|
2225
|
+
this.conflict.set(false);
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
describeDiagnostic(diagnostic) {
|
|
2229
|
+
return this.messages.diagnostic(diagnostic);
|
|
2230
|
+
}
|
|
2231
|
+
channelLabel(channel) {
|
|
2232
|
+
return this.messages.channel(channel);
|
|
2233
|
+
}
|
|
2234
|
+
afterWrite(toastKey) {
|
|
2235
|
+
this.writeDiagnostics.set([]);
|
|
2236
|
+
const template = this.template();
|
|
2237
|
+
// The write committed even when its audit record did not; never repeat it.
|
|
2238
|
+
if (template?.auditState === 'Failed') {
|
|
2239
|
+
this.toast.warn(this.messages.t('toast.audit-warning'));
|
|
2240
|
+
return;
|
|
1204
2241
|
}
|
|
2242
|
+
if (toastKey)
|
|
2243
|
+
this.toast.success(this.messages.t(`toast.${toastKey}`));
|
|
1205
2244
|
}
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
2245
|
+
onWriteError(error) {
|
|
2246
|
+
const failure = readNotificationFailure(error);
|
|
2247
|
+
if (isRevisionConflict(failure)) {
|
|
2248
|
+
this.conflict.set(true);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
this.writeDiagnostics.set(failure.diagnostics);
|
|
2252
|
+
this.toast.error(this.messages.code(failure.code));
|
|
2253
|
+
const eventId = this.selectedEventId();
|
|
2254
|
+
if (eventId != null &&
|
|
2255
|
+
(failure.code === 'NOTIFICATION_RECIPE_CHANGED' ||
|
|
2256
|
+
failure.code === 'NOTIFICATION_DEFAULT_ALREADY_PRESENT' ||
|
|
2257
|
+
failure.code === 'NOTIFICATION_DEFAULT_ADOPTION_AMBIGUOUS')) {
|
|
2258
|
+
this.facade.reloadDefaults(eventId);
|
|
2259
|
+
}
|
|
2260
|
+
else if (failure.code?.endsWith('_NOT_FOUND')) {
|
|
2261
|
+
this.facade.loadEvents();
|
|
1221
2262
|
}
|
|
1222
|
-
return payload;
|
|
1223
2263
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
2264
|
+
// ==========================================================================
|
|
2265
|
+
// Helpers
|
|
2266
|
+
// ==========================================================================
|
|
2267
|
+
review(mode, content) {
|
|
2268
|
+
const eventId = this.selectedEventId();
|
|
2269
|
+
if (eventId == null)
|
|
2270
|
+
return of();
|
|
2271
|
+
const ref = this.modal.openModal(TemplateReviewDialog, 'dialog', {
|
|
2272
|
+
header: this.messages.t(`review.title.${mode}`),
|
|
2273
|
+
styleClass: '!w-[96vw] lg:!w-[52rem]',
|
|
2274
|
+
dismissible: true,
|
|
2275
|
+
inputValues: {
|
|
2276
|
+
mode,
|
|
2277
|
+
eventId,
|
|
2278
|
+
request: this.withSamples(content.request),
|
|
2279
|
+
recipients: content.recipients,
|
|
2280
|
+
condition: content.condition,
|
|
2281
|
+
requireLanguageReview: mode !== 'restoreDefault' && this.defaultOwned(),
|
|
2282
|
+
isEnabled: content.isEnabled ?? false,
|
|
2283
|
+
},
|
|
2284
|
+
});
|
|
2285
|
+
return ref.onClose.pipe(filter((result) => !!result), map((result) => result));
|
|
2286
|
+
}
|
|
2287
|
+
/** Readable sample values for every field the previewed content uses. */
|
|
2288
|
+
withSamples(request) {
|
|
2289
|
+
return withPreviewSamples(request, this.context()?.properties ?? [], this.lang());
|
|
2290
|
+
}
|
|
2291
|
+
confirmDiscard() {
|
|
2292
|
+
if (!this.dirty())
|
|
2293
|
+
return of(true);
|
|
2294
|
+
return new Observable((subscriber) => {
|
|
2295
|
+
this.confirmation.confirm({
|
|
2296
|
+
type: 'dialog',
|
|
2297
|
+
header: this.messages.t('unsaved.header'),
|
|
2298
|
+
message: this.messages.t('unsaved.message'),
|
|
2299
|
+
acceptLabel: this.messages.t('unsaved.discard'),
|
|
2300
|
+
rejectLabel: this.messages.t('unsaved.stay'),
|
|
2301
|
+
acceptButton: { severity: 'danger' },
|
|
2302
|
+
accept: () => {
|
|
2303
|
+
subscriber.next(true);
|
|
2304
|
+
subscriber.complete();
|
|
2305
|
+
},
|
|
2306
|
+
reject: () => {
|
|
2307
|
+
subscriber.next(false);
|
|
2308
|
+
subscriber.complete();
|
|
2309
|
+
},
|
|
2310
|
+
});
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
patchFromTemplate(template) {
|
|
2314
|
+
this.form.reset({
|
|
2315
|
+
name: template.name ?? '',
|
|
2316
|
+
subject: template.subject ?? '',
|
|
2317
|
+
emailTemplate: template.emailTemplate ?? '',
|
|
2318
|
+
smsTemplate: template.smsTemplate ?? '',
|
|
2319
|
+
appEn: template.appTemplate?.en ?? '',
|
|
2320
|
+
appAr: template.appTemplate?.ar ?? '',
|
|
2321
|
+
conditionFormula: template.conditionFormula ?? '',
|
|
2322
|
+
});
|
|
2323
|
+
this.formValue.set(this.form.getRawValue());
|
|
1227
2324
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
const value =
|
|
2325
|
+
/** Replacement semantics: only the template's own channel field is sent. */
|
|
2326
|
+
buildSavePayload(template) {
|
|
2327
|
+
const value = this.form.getRawValue();
|
|
2328
|
+
const condition = value.conditionFormula.trim();
|
|
1231
2329
|
return {
|
|
1232
|
-
|
|
1233
|
-
|
|
2330
|
+
expectedRevision: template.revision,
|
|
2331
|
+
name: value.name.trim(),
|
|
2332
|
+
subject: template.channel === 'Email' ? value.subject.trim() || null : null,
|
|
2333
|
+
emailTemplate: template.channel === 'Email' ? value.emailTemplate : null,
|
|
2334
|
+
smsTemplate: template.channel === 'Sms' ? value.smsTemplate : null,
|
|
2335
|
+
appTemplate: template.channel === 'App'
|
|
2336
|
+
? { en: value.appEn, ar: value.appAr }
|
|
2337
|
+
: null,
|
|
2338
|
+
conditionFormula: condition || null,
|
|
1234
2339
|
};
|
|
1235
2340
|
}
|
|
1236
2341
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplate, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1237
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NotificationTemplate, isStandalone: true, selector: "mt-notification-template", ngImport: i0, template: "<div class=\"flex flex-col gap-4 p-4\" [formGroup]=\"form\">\r\n <!-- Loading State -->\r\n @if (isLoadingEvents()) {\r\n <div class=\"flex flex-col gap-4\">\r\n <div class=\"w-1/2\">\r\n <p-skeleton width=\"4rem\" height=\"1rem\" styleClass=\"mb-2\" />\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <div class=\"flex items-center justify-between\">\r\n <div class=\"flex gap-2\">\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <p-skeleton width=\"8rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"100%\" height=\"8rem\" borderRadius=\"0.5rem\" />\r\n <div class=\"flex justify-end gap-2 pt-4\">\r\n <p-skeleton width=\"6rem\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"6rem\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n </div>\r\n } @else if (events().length === 0) {\r\n <!-- Empty State -->\r\n <mt-card>\r\n <div\r\n class=\"flex flex-col items-center justify-center py-12 text-slate-500\"\r\n >\r\n <mt-icon icon=\"alert.alert-circle\" class=\"mb-2 h-12 w-12\" />\r\n <p>{{ \"notification.noEventsFound\" | transloco }}</p>\r\n </div>\r\n </mt-card>\r\n } @else {\r\n <!-- Event Selector -->\r\n <div class=\"w-1/2\">\r\n <mt-select-field\r\n formControlName=\"event\"\r\n [options]=\"events()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'eventName'\"\r\n [label]=\"'notification.events' | transloco\"\r\n [placeholder]=\"'notification.selectEvent' | transloco\"\r\n [required]=\"true\"\r\n [filter]=\"false\"\r\n (onChange)=\"onEventChange($event)\"\r\n />\r\n </div>\r\n\r\n @if (selectedEvent()) {\r\n <!-- Template Tabs + Add notification -->\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <mt-tabs\r\n [active]=\"selectedTemplateId()\"\r\n [options]=\"templateTabs()\"\r\n (activeChange)=\"onTemplateTabChange($event)\"\r\n />\r\n <mt-button\r\n icon=\"general.plus\"\r\n [label]=\"'notification.addNotification' | transloco\"\r\n size=\"small\"\r\n [loading]=\"isAddingTemplate()\"\r\n (onClick)=\"onAddTemplateClick()\"\r\n />\r\n </div>\r\n\r\n <!-- Template Content -->\r\n @if (selectedTemplate(); as template) {\r\n <div class=\"flex flex-col gap-4\">\r\n <!-- Name -->\r\n <div class=\"w-full\">\r\n <mt-text-field\r\n formControlName=\"name\"\r\n [label]=\"'notification.name' | transloco\"\r\n [placeholder]=\"'notification.enterName' | transloco\"\r\n [required]=\"true\"\r\n />\r\n </div>\r\n <!-- Channel & Properties Picker -->\r\n <div class=\"flex flex-col gap-4\">\r\n <mt-select-field\r\n formControlName=\"channel\"\r\n [options]=\"channelOptions()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'key'\"\r\n [label]=\"'notification.channel.label' | transloco\"\r\n [placeholder]=\"'notification.selectChannel' | transloco\"\r\n [required]=\"true\"\r\n [filter]=\"false\"\r\n [disabled]=\"true\"\r\n />\r\n <mt-select-field\r\n [options]=\"propertyGroups()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'key'\"\r\n [group]=\"true\"\r\n [optionGroupLabel]=\"'groupName'\"\r\n [optionGroupChildren]=\"'properties'\"\r\n [label]=\"'notification.propertiesList' | transloco\"\r\n [placeholder]=\"'notification.selectProperty' | transloco\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n [filter]=\"true\"\r\n [filterBy]=\"'name'\"\r\n [loading]=\"isLoadingProperties()\"\r\n [formControl]=\"propertyPickerControl\"\r\n (onChange)=\"onAddProperty($event)\"\r\n />\r\n </div>\r\n <!-- Channel Content Editors -->\r\n @for (ch of channelConfigs; track ch.key) {\r\n @if (form.controls[\"channel\"].value === ch.key) {\r\n <!-- Bilingual Content Fields -->\r\n @if (isBilingual(ch) && ch.translatableKey) {\r\n <ng-container [formGroupName]=\"ch.translatableKey\">\r\n @if (ch.useRichEditor) {\r\n <mt-editor-field\r\n formControlName=\"en\"\r\n [label]=\"'notification.english' | transloco\"\r\n dir=\"ltr\"\r\n />\r\n <mt-editor-field\r\n formControlName=\"ar\"\r\n [label]=\"'notification.arabic' | transloco\"\r\n dir=\"rtl\"\r\n />\r\n } @else {\r\n <mt-textarea-field\r\n formControlName=\"en\"\r\n [label]=\"'notification.english' | transloco\"\r\n [rows]=\"'6'\"\r\n dir=\"ltr\"\r\n />\r\n <mt-textarea-field\r\n formControlName=\"ar\"\r\n [label]=\"'notification.arabic' | transloco\"\r\n [rows]=\"'6'\"\r\n dir=\"rtl\"\r\n />\r\n }\r\n </ng-container>\r\n } @else if (ch.templateKey) {\r\n <!-- Non-Bilingual Content Field -->\r\n @if (ch.useRichEditor) {\r\n <mt-editor-field\r\n [formControlName]=\"ch.templateKey\"\r\n [label]=\"'notification.notificationBody' | transloco\"\r\n />\r\n } @else {\r\n <mt-textarea-field\r\n [formControlName]=\"ch.templateKey\"\r\n [label]=\"'notification.notificationBody' | transloco\"\r\n [rows]=\"'6'\"\r\n />\r\n }\r\n }\r\n <!-- Bilingual Toggle -->\r\n @if (ch.bilingualOptional) {\r\n <mt-toggle-field\r\n [formControlName]=\"ch.key + 'Bilingual'\"\r\n [label]=\"'notification.bilingual' | transloco\"\r\n />\r\n }\r\n }\r\n }\r\n <!-- Receivers -->\r\n <div class=\"w-full\">\r\n <mt-select-field\r\n [options]=\"availableReceiverGroups()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'id'\"\r\n [group]=\"true\"\r\n [optionGroupLabel]=\"'groupName'\"\r\n [optionGroupChildren]=\"'items'\"\r\n [label]=\"'notification.receivers' | transloco\"\r\n [placeholder]=\"'notification.selectReceiver' | transloco\"\r\n [filter]=\"true\"\r\n [filterBy]=\"'name'\"\r\n [loading]=\"isLoadingReceivers()\"\r\n [formControl]=\"receiverPickerControl\"\r\n (onChange)=\"onAddReceiver($event)\"\r\n />\r\n </div>\r\n @if ((template.receivers ?? []).length > 0) {\r\n <div class=\"flex w-full flex-wrap gap-2\">\r\n @for (receiver of template.receivers; track receiver.id) {\r\n <div\r\n class=\"flex items-center gap-2 rounded-full border border-surface bg-surface-50 py-1 pe-2 ps-3 text-sm transition-colors hover:bg-surface-100\"\r\n >\r\n <mt-icon\r\n icon=\"user.user-01\"\r\n class=\"h-4 w-4 shrink-0 text-slate-500\"\r\n />\r\n <span class=\"font-medium\">{{ receiver.name?.display }}</span>\r\n @if (!receiver.isSystem) {\r\n <mt-button\r\n icon=\"general.trash-01\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n [text]=\"true\"\r\n [loading]=\"isReceiverDeleting(receiver.id)\"\r\n (onClick)=\"onDeleteReceiver(receiver)\"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n <!-- Actions -->\r\n <div class=\"flex justify-end gap-2 pt-4\">\r\n @if (!template.isSystem) {\r\n <mt-button\r\n icon=\"general.trash-01\"\r\n [label]=\"'notification.delete' | transloco\"\r\n severity=\"danger\"\r\n [loading]=\"isDeletingTemplate()\"\r\n [disabled]=\"isUpdatingTemplate()\"\r\n (onClick)=\"onDeleteTemplate()\"\r\n />\r\n }\r\n <mt-button\r\n icon=\"general.save-01\"\r\n [label]=\"'notification.save' | transloco\"\r\n [loading]=\"isUpdatingTemplate()\"\r\n [disabled]=\"isDeletingTemplate() || isFormInvalid()\"\r\n (onClick)=\"onSaveTemplate()\"\r\n />\r\n </div>\r\n </div>\r\n } @else {\r\n <!-- No template selected -->\r\n <div\r\n class=\"flex flex-col items-center justify-center py-12 text-slate-500\"\r\n >\r\n <mt-icon icon=\"alert.bell-01\" class=\"mb-2 h-12 w-12\" />\r\n <p>{{ \"notification.selectTemplatePrompt\" | transloco }}</p>\r\n </div>\r\n }\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}:host ::ng-deep mt-textarea-field>.flex.items-center,:host ::ng-deep mt-editor-field>.flex.items-center{direction:ltr}:host-context([dir=rtl]) ::ng-deep mt-textarea-field>.flex.items-center,:host-context([dir=rtl]) ::ng-deep mt-editor-field>.flex.items-center{direction:rtl}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: EditorField, selector: "mt-editor-field", inputs: ["label", "hint", "placeholder", "readonly", "theme", "height", "dir", "modules", "required"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }, { kind: "component", type: Tabs, selector: "mt-tabs", inputs: ["options", "optionLabel", "optionValue", "active", "mode", "moreLabel", "defaultIcon", "size", "fluid", "disabled", "searchThreshold"], outputs: ["activeChange", "onChange"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2342
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NotificationTemplate, isStandalone: true, selector: "mt-notification-template", host: { listeners: { "window:beforeunload": "onBeforeUnload($event)" } }, viewQueries: [{ propertyName: "emailEditor", first: true, predicate: ["emailEditor"], descendants: true, read: EmailVisualEditor, isSignal: true }, { propertyName: "appEnEditor", first: true, predicate: ["appEnEditor"], descendants: true, read: EmailVisualEditor, isSignal: true }, { propertyName: "appArEditor", first: true, predicate: ["appArEditor"], descendants: true, read: EmailVisualEditor, isSignal: true }, { propertyName: "smsSource", first: true, predicate: ["smsSource"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'notification'\">\n <div class=\"flex min-w-0 flex-col gap-4 p-4\">\n @if (facade.isLoadingEvents() && events().length === 0) {\n <div class=\"flex flex-col gap-4\" aria-live=\"polite\">\n <p-skeleton width=\"50%\" height=\"2.5rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"16rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else if (facade.eventsError() && events().length === 0) {\n <mt-empty-state\n illustration=\"none\"\n [title]=\"t('load-error')\"\n [actionLabel]=\"t('actions.retry')\"\n actionIcon=\"arrow.refresh-cw-01\"\n (actionClick)=\"retryLoad()\"\n />\n } @else if (events().length === 0) {\n <mt-empty-state illustration=\"none\" [title]=\"t('noEventsFound')\" />\n } @else {\n <!-- Event -->\n <div class=\"w-full md:w-1/2\">\n <mt-select-field\n [formControl]=\"eventControl\"\n [options]=\"events()\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [label]=\"t('events')\"\n [placeholder]=\"t('selectEvent')\"\n [filter]=\"events().length > 8\"\n filterBy=\"name\"\n [showClear]=\"false\"\n (onChange)=\"onEventChange($event)\"\n />\n </div>\n\n @if (selectedEvent(); as event) {\n <!-- Ready-made defaults -->\n @if (missingDefaults().length > 0 || removedDefaults().length > 0) {\n <div\n class=\"flex flex-col gap-2 rounded-md border border-surface bg-surface-50 px-4 py-3 text-sm\"\n >\n @if (missingDefaults().length > 0) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n <span class=\"flex items-center gap-2\">\n <mt-icon\n icon=\"general.zap\"\n class=\"size-4 shrink-0 text-primary\"\n />\n {{ t(\"defaults.missing\") }}\n </span>\n <mt-button\n size=\"small\"\n [label]=\"t('defaults.add')\"\n [loading]=\"facade.isLoading('initializeDefaults')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onInitializeDefaults()\"\n />\n </div>\n }\n @for (item of removedDefaults(); track item.slot.channel) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n <span>{{\n t(\"defaults.removed\", {\n channel: channelLabel(item.slot.channel),\n })\n }}</span>\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('defaults.restore')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onRestoreDefault(item.slot.channel)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- Templates of the event -->\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (templates().length > 0) {\n <mt-tabs\n class=\"min-w-0 flex-1\"\n mode=\"underline\"\n [active]=\"activeTab()\"\n [options]=\"templateTabs()\"\n (activeChange)=\"onTemplateTabChange($event)\"\n />\n } @else {\n <span class=\"text-sm text-muted-color\">{{\n t(\"no-templates\")\n }}</span>\n }\n <mt-button\n icon=\"general.plus\"\n [label]=\"t('addNotification')\"\n size=\"small\"\n [disabled]=\"isMutating() || !context()\"\n (onClick)=\"onAddTemplate()\"\n />\n </div>\n\n @if (facade.contextError() && !context()) {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\n role=\"alert\"\n >\n <span>{{ t(\"context-error\") }}</span>\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.retry')\"\n (onClick)=\"retryLoad()\"\n />\n </div>\n }\n\n @if (template(); as template) {\n @if (!context()) {\n <div class=\"flex flex-col gap-3\">\n <p-skeleton\n width=\"100%\"\n height=\"3.5rem\"\n borderRadius=\"0.375rem\"\n />\n <p-skeleton width=\"100%\" height=\"20rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else {\n <!-- Status and actions -->\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-surface bg-content px-4 py-3\"\n >\n <div class=\"flex min-w-0 flex-wrap items-center gap-2 text-sm\">\n <span\n class=\"inline-flex items-center gap-1 rounded-md border border-surface bg-surface-50 px-2 py-0.5\"\n >\n <mt-icon\n [icon]=\"channelIcons[template.channel]\"\n class=\"size-4\"\n />\n {{ channelLabel(template.channel) }}\n </span>\n @switch (status()) {\n @case (\"on\") {\n <span\n class=\"rounded-md border border-green-200 bg-green-50 px-2 py-0.5 font-medium text-green-700\"\n [mtTooltip]=\"t('status.on-hint')\"\n >{{ t(\"status.on\") }}</span\n >\n }\n @case (\"off\") {\n <span\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-0.5 font-medium text-muted-color\"\n [mtTooltip]=\"t('status.off-hint')\"\n >{{ t(\"status.off\") }}</span\n >\n }\n @case (\"draft\") {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 font-medium text-amber-700\"\n [mtTooltip]=\"t('status.draft-hint')\"\n >{{ t(\"status.draft\") }}</span\n >\n }\n }\n @if (unpublishedChanges()) {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-amber-700\"\n [mtTooltip]=\"t('status.unpublished-hint')\"\n >{{ t(\"status.unpublished\") }}</span\n >\n }\n @if (defaultOwned()) {\n <span\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-0.5 text-muted-color\"\n >{{ t(\"status.default\") }}</span\n >\n }\n @if (dirty()) {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-amber-700\"\n >{{ t(\"status.unsaved\") }}</span\n >\n }\n </div>\n\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n icon=\"time.clock-rewind\"\n severity=\"secondary\"\n variant=\"text\"\n [tooltip]=\"t('actions.history')\"\n [ariaLabel]=\"t('actions.history')\"\n [disabled]=\"template.publishedRevision === null\"\n (onClick)=\"onOpenHistory()\"\n />\n @if (template.channel === \"Email\") {\n <mt-button\n icon=\"communication.send-01\"\n severity=\"secondary\"\n variant=\"text\"\n [tooltip]=\"\n dirty() ? t('saveBeforeEmailTest') : t('testEmail')\n \"\n [ariaLabel]=\"t('testEmail')\"\n [disabled]=\"dirty() || isMutating()\"\n (onClick)=\"onTestEmail()\"\n />\n }\n @if (!template.isSystem) {\n <mt-button\n icon=\"general.trash-01\"\n severity=\"danger\"\n variant=\"text\"\n [tooltip]=\"t('actions.delete')\"\n [ariaLabel]=\"t('actions.delete')\"\n [loading]=\"facade.isLoading('deleteTemplate')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onDelete()\"\n />\n }\n @if (status() === \"on\") {\n <mt-button\n icon=\"general.toggle-01-left\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.turn-off')\"\n [loading]=\"facade.isLoading('setActivation')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onTurnOff()\"\n />\n } @else {\n <mt-button\n icon=\"general.toggle-01-right\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.turn-on')\"\n [tooltip]=\"\n template.publishedRevision === null\n ? t('actions.turn-on-blocked')\n : ''\n \"\n [loading]=\"facade.isLoading('setActivation')\"\n [disabled]=\"!canTurnOn()\"\n (onClick)=\"onTurnOn()\"\n />\n }\n <mt-button\n icon=\"general.check\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.publish')\"\n [tooltip]=\"dirty() ? t('actions.save-first') : ''\"\n [loading]=\"facade.isLoading('publishTemplate')\"\n [disabled]=\"!canPublish()\"\n (onClick)=\"onPublish()\"\n />\n @if (dirty()) {\n <mt-button\n severity=\"secondary\"\n variant=\"text\"\n [label]=\"t('actions.discard')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onDiscard()\"\n />\n }\n <mt-button\n icon=\"general.save-01\"\n [label]=\"t('save')\"\n [loading]=\"facade.isLoading('saveTemplate')\"\n [disabled]=\"!canSave()\"\n (onClick)=\"onSave()\"\n />\n </div>\n </div>\n\n @if (conflict()) {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700\"\n role=\"alert\"\n >\n <span>{{ t(\"conflict.message\") }}</span>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('conflict.keep-mine')\"\n (onClick)=\"resolveConflict(true)\"\n />\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n [label]=\"t('conflict.load-latest')\"\n (onClick)=\"resolveConflict(false)\"\n />\n </div>\n </div>\n }\n\n @if (writeDiagnostics().length > 0) {\n <ul\n class=\"list-disc rounded-md border border-red-200 bg-red-50 py-3 pe-4 ps-8 text-sm text-red-700\"\n role=\"alert\"\n >\n @for (diagnostic of writeDiagnostics(); track $index) {\n <li>{{ describeDiagnostic(diagnostic) }}</li>\n }\n </ul>\n }\n\n <div\n class=\"grid min-w-0 gap-4\"\n [class]=\"wide() ? 'grid-cols-2' : 'grid-cols-1'\"\n [formGroup]=\"form\"\n >\n <!-- Content -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.content\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-4 p-4\">\n <mt-text-field\n formControlName=\"name\"\n [label]=\"t('name')\"\n [placeholder]=\"t('enterName')\"\n [required]=\"true\"\n />\n @if (template.channel === \"Email\") {\n <mt-text-field\n formControlName=\"subject\"\n [label]=\"t('subject')\"\n [hint]=\"t('subject-hint')\"\n />\n }\n <mt-select-field\n [formControl]=\"placeholderControl\"\n [options]=\"placeholderGroups()\"\n optionLabel=\"label\"\n optionValue=\"key\"\n [group]=\"true\"\n optionGroupLabel=\"groupName\"\n optionGroupChildren=\"properties\"\n [label]=\"t('placeholders.label')\"\n [hint]=\"t('placeholders.hint')\"\n [placeholder]=\"t('placeholders.placeholder')\"\n [hasPlaceholderPrefix]=\"false\"\n [filter]=\"true\"\n filterBy=\"label\"\n (onChange)=\"onInsertPlaceholder($event)\"\n />\n @switch (template.channel) {\n @case (\"Email\") {\n <mt-email-visual-editor\n #emailEditor\n formControlName=\"emailTemplate\"\n [label]=\"t('email-body')\"\n [hint]=\"t('email-body-hint')\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"emailImageOrigins()\"\n height=\"26rem\"\n />\n }\n @case (\"Sms\") {\n <div #smsSource (focusin)=\"setActiveField('smsTemplate')\">\n <mt-textarea-field\n dir=\"auto\"\n formControlName=\"smsTemplate\"\n [label]=\"t('sms-body')\"\n [hint]=\"t('sms-body-hint')\"\n rows=\"6\"\n />\n </div>\n }\n @case (\"App\") {\n <mt-email-visual-editor\n #appEnEditor\n formControlName=\"appEn\"\n [label]=\"t('english')\"\n direction=\"ltr\"\n [starter]=\"false\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"appImageOrigins()\"\n height=\"12rem\"\n (focused)=\"setActiveField('appEn')\"\n />\n <mt-email-visual-editor\n #appArEditor\n formControlName=\"appAr\"\n [label]=\"t('arabic')\"\n direction=\"rtl\"\n [starter]=\"false\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"appImageOrigins()\"\n height=\"12rem\"\n (focused)=\"setActiveField('appAr')\"\n />\n }\n }\n </div>\n </section>\n\n <!-- Live preview -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.preview\") }}\n </header>\n <div class=\"flex flex-1 flex-col p-4\">\n <mt-notification-content-preview\n [preview]=\"preview.result()\"\n [status]=\"preview.status()\"\n [errorCode]=\"preview.errorCode()\"\n [stale]=\"previewStale()\"\n [fill]=\"true\"\n [showSubject]=\"false\"\n (retry)=\"preview.retry()\"\n />\n </div>\n </section>\n\n <!-- Recipients -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.recipients\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-3 p-4\">\n <mt-select-field\n [formControl]=\"recipientControl\"\n [options]=\"recipientGroups()\"\n optionLabel=\"name\"\n optionValue=\"key\"\n [group]=\"true\"\n optionGroupLabel=\"groupName\"\n optionGroupChildren=\"items\"\n [label]=\"t('recipients.add')\"\n [hint]=\"t('recipients.hint')\"\n [placeholder]=\"t('recipients.placeholder')\"\n [filter]=\"true\"\n filterBy=\"name\"\n [loading]=\"facade.isLoading('addReceivers')\"\n (onChange)=\"onAddRecipient($event)\"\n />\n @if (template.receivers.length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (receiver of template.receivers; track receiver.id) {\n <li\n class=\"flex items-center gap-2 rounded-md border border-surface bg-surface-50 py-1 pe-1 ps-3 text-sm\"\n >\n <span class=\"font-medium\">{{\n receiverName(receiver)\n }}</span>\n <span class=\"text-xs text-muted-color\">{{\n t(\"recipient-type.\" + receiver.type)\n }}</span>\n @if (!receiver.isSystem) {\n <mt-button\n icon=\"general.x-close\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"t('recipients.remove')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onRemoveRecipient(receiver)\"\n />\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"text-sm text-amber-700\">\n {{ t(\"recipients.empty\") }}\n </p>\n }\n </div>\n </section>\n\n <!-- Send only when -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.condition\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-2 p-4\">\n <p class=\"text-sm text-muted-color\">\n {{ t(\"condition.hint\") }}\n </p>\n <mt-formula-builder\n formControlName=\"conditionFormula\"\n valueMode=\"expression\"\n [isProcessBuilder]=\"true\"\n [propertiesByPath]=\"conditionPropertiesByPath()\"\n [knownProperties]=\"conditionKeys()\"\n [placeholder]=\"t('condition.placeholder')\"\n />\n </div>\n </section>\n </div>\n }\n } @else if (templates().length > 0) {\n <p class=\"py-8 text-center text-sm text-muted-color\">\n {{ t(\"selectTemplatePrompt\") }}\n </p>\n }\n }\n }\n </div>\n</ng-container>\n", styles: [":host{display:block;width:100%;height:100%}:host ::ng-deep mt-textarea-field>.flex.items-center,:host ::ng-deep mt-editor-field>.flex.items-center{direction:ltr}:host-context([dir=rtl]) ::ng-deep mt-textarea-field>.flex.items-center,:host-context([dir=rtl]) ::ng-deep mt-editor-field>.flex.items-center{direction:rtl}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: Tabs, selector: "mt-tabs", inputs: ["options", "optionLabel", "optionValue", "active", "mode", "moreLabel", "defaultIcon", "size", "fluid", "disabled", "searchThreshold"], outputs: ["activeChange", "onChange"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: FormulaBuilder, selector: "mt-formula-builder", inputs: ["propertiesByPath", "levelSchemaId", "moduleId", "contextKey", "contextEntityTypeKey", "templateId", "placeholder", "hideToolbar", "hideStatusBar", "invalidWhenEmpty", "toolbarTabs", "functionCategories", "operators", "codeOnly", "builderOnly", "valueMode", "isProcessBuilder", "knownProperties"], outputs: ["validationChange", "tokensChange"] }, { kind: "component", type: EmailVisualEditor, selector: "mt-email-visual-editor", inputs: ["label", "hint", "placeholders", "layout", "height", "direction", "starter", "approvedImageOrigins"], outputs: ["focused"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: NotificationContentPreview, selector: "mt-notification-content-preview", inputs: ["preview", "status", "errorCode", "stale", "height", "fill", "showSubject"], outputs: ["retry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1238
2343
|
}
|
|
1239
2344
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplate, decorators: [{
|
|
1240
2345
|
type: Component,
|
|
1241
|
-
args: [{ selector: 'mt-notification-template',
|
|
1242
|
-
CommonModule,
|
|
2346
|
+
args: [{ selector: 'mt-notification-template', imports: [
|
|
1243
2347
|
ReactiveFormsModule,
|
|
1244
|
-
|
|
1245
|
-
Card,
|
|
2348
|
+
TranslocoDirective,
|
|
1246
2349
|
Button,
|
|
2350
|
+
EmptyState,
|
|
1247
2351
|
SelectField,
|
|
2352
|
+
Tabs,
|
|
1248
2353
|
TextField,
|
|
1249
2354
|
TextareaField,
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
2355
|
+
Tooltip,
|
|
2356
|
+
FormulaBuilder,
|
|
2357
|
+
EmailVisualEditor,
|
|
1253
2358
|
Icon,
|
|
1254
2359
|
Skeleton,
|
|
1255
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-4 p-4\" [formGroup]=\"form\">\r\n <!-- Loading State -->\r\n @if (isLoadingEvents()) {\r\n <div class=\"flex flex-col gap-4\">\r\n <div class=\"w-1/2\">\r\n <p-skeleton width=\"4rem\" height=\"1rem\" styleClass=\"mb-2\" />\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <div class=\"flex items-center justify-between\">\r\n <div class=\"flex gap-2\">\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"5rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <p-skeleton width=\"8rem\" height=\"2.25rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"100%\" height=\"8rem\" borderRadius=\"0.5rem\" />\r\n <div class=\"flex justify-end gap-2 pt-4\">\r\n <p-skeleton width=\"6rem\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n <p-skeleton width=\"6rem\" height=\"2.5rem\" borderRadius=\"0.5rem\" />\r\n </div>\r\n </div>\r\n } @else if (events().length === 0) {\r\n <!-- Empty State -->\r\n <mt-card>\r\n <div\r\n class=\"flex flex-col items-center justify-center py-12 text-slate-500\"\r\n >\r\n <mt-icon icon=\"alert.alert-circle\" class=\"mb-2 h-12 w-12\" />\r\n <p>{{ \"notification.noEventsFound\" | transloco }}</p>\r\n </div>\r\n </mt-card>\r\n } @else {\r\n <!-- Event Selector -->\r\n <div class=\"w-1/2\">\r\n <mt-select-field\r\n formControlName=\"event\"\r\n [options]=\"events()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'eventName'\"\r\n [label]=\"'notification.events' | transloco\"\r\n [placeholder]=\"'notification.selectEvent' | transloco\"\r\n [required]=\"true\"\r\n [filter]=\"false\"\r\n (onChange)=\"onEventChange($event)\"\r\n />\r\n </div>\r\n\r\n @if (selectedEvent()) {\r\n <!-- Template Tabs + Add notification -->\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <mt-tabs\r\n [active]=\"selectedTemplateId()\"\r\n [options]=\"templateTabs()\"\r\n (activeChange)=\"onTemplateTabChange($event)\"\r\n />\r\n <mt-button\r\n icon=\"general.plus\"\r\n [label]=\"'notification.addNotification' | transloco\"\r\n size=\"small\"\r\n [loading]=\"isAddingTemplate()\"\r\n (onClick)=\"onAddTemplateClick()\"\r\n />\r\n </div>\r\n\r\n <!-- Template Content -->\r\n @if (selectedTemplate(); as template) {\r\n <div class=\"flex flex-col gap-4\">\r\n <!-- Name -->\r\n <div class=\"w-full\">\r\n <mt-text-field\r\n formControlName=\"name\"\r\n [label]=\"'notification.name' | transloco\"\r\n [placeholder]=\"'notification.enterName' | transloco\"\r\n [required]=\"true\"\r\n />\r\n </div>\r\n <!-- Channel & Properties Picker -->\r\n <div class=\"flex flex-col gap-4\">\r\n <mt-select-field\r\n formControlName=\"channel\"\r\n [options]=\"channelOptions()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'key'\"\r\n [label]=\"'notification.channel.label' | transloco\"\r\n [placeholder]=\"'notification.selectChannel' | transloco\"\r\n [required]=\"true\"\r\n [filter]=\"false\"\r\n [disabled]=\"true\"\r\n />\r\n <mt-select-field\r\n [options]=\"propertyGroups()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'key'\"\r\n [group]=\"true\"\r\n [optionGroupLabel]=\"'groupName'\"\r\n [optionGroupChildren]=\"'properties'\"\r\n [label]=\"'notification.propertiesList' | transloco\"\r\n [placeholder]=\"'notification.selectProperty' | transloco\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n [filter]=\"true\"\r\n [filterBy]=\"'name'\"\r\n [loading]=\"isLoadingProperties()\"\r\n [formControl]=\"propertyPickerControl\"\r\n (onChange)=\"onAddProperty($event)\"\r\n />\r\n </div>\r\n <!-- Channel Content Editors -->\r\n @for (ch of channelConfigs; track ch.key) {\r\n @if (form.controls[\"channel\"].value === ch.key) {\r\n <!-- Bilingual Content Fields -->\r\n @if (isBilingual(ch) && ch.translatableKey) {\r\n <ng-container [formGroupName]=\"ch.translatableKey\">\r\n @if (ch.useRichEditor) {\r\n <mt-editor-field\r\n formControlName=\"en\"\r\n [label]=\"'notification.english' | transloco\"\r\n dir=\"ltr\"\r\n />\r\n <mt-editor-field\r\n formControlName=\"ar\"\r\n [label]=\"'notification.arabic' | transloco\"\r\n dir=\"rtl\"\r\n />\r\n } @else {\r\n <mt-textarea-field\r\n formControlName=\"en\"\r\n [label]=\"'notification.english' | transloco\"\r\n [rows]=\"'6'\"\r\n dir=\"ltr\"\r\n />\r\n <mt-textarea-field\r\n formControlName=\"ar\"\r\n [label]=\"'notification.arabic' | transloco\"\r\n [rows]=\"'6'\"\r\n dir=\"rtl\"\r\n />\r\n }\r\n </ng-container>\r\n } @else if (ch.templateKey) {\r\n <!-- Non-Bilingual Content Field -->\r\n @if (ch.useRichEditor) {\r\n <mt-editor-field\r\n [formControlName]=\"ch.templateKey\"\r\n [label]=\"'notification.notificationBody' | transloco\"\r\n />\r\n } @else {\r\n <mt-textarea-field\r\n [formControlName]=\"ch.templateKey\"\r\n [label]=\"'notification.notificationBody' | transloco\"\r\n [rows]=\"'6'\"\r\n />\r\n }\r\n }\r\n <!-- Bilingual Toggle -->\r\n @if (ch.bilingualOptional) {\r\n <mt-toggle-field\r\n [formControlName]=\"ch.key + 'Bilingual'\"\r\n [label]=\"'notification.bilingual' | transloco\"\r\n />\r\n }\r\n }\r\n }\r\n <!-- Receivers -->\r\n <div class=\"w-full\">\r\n <mt-select-field\r\n [options]=\"availableReceiverGroups()\"\r\n [optionLabel]=\"'name'\"\r\n [optionValue]=\"'id'\"\r\n [group]=\"true\"\r\n [optionGroupLabel]=\"'groupName'\"\r\n [optionGroupChildren]=\"'items'\"\r\n [label]=\"'notification.receivers' | transloco\"\r\n [placeholder]=\"'notification.selectReceiver' | transloco\"\r\n [filter]=\"true\"\r\n [filterBy]=\"'name'\"\r\n [loading]=\"isLoadingReceivers()\"\r\n [formControl]=\"receiverPickerControl\"\r\n (onChange)=\"onAddReceiver($event)\"\r\n />\r\n </div>\r\n @if ((template.receivers ?? []).length > 0) {\r\n <div class=\"flex w-full flex-wrap gap-2\">\r\n @for (receiver of template.receivers; track receiver.id) {\r\n <div\r\n class=\"flex items-center gap-2 rounded-full border border-surface bg-surface-50 py-1 pe-2 ps-3 text-sm transition-colors hover:bg-surface-100\"\r\n >\r\n <mt-icon\r\n icon=\"user.user-01\"\r\n class=\"h-4 w-4 shrink-0 text-slate-500\"\r\n />\r\n <span class=\"font-medium\">{{ receiver.name?.display }}</span>\r\n @if (!receiver.isSystem) {\r\n <mt-button\r\n icon=\"general.trash-01\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n [text]=\"true\"\r\n [loading]=\"isReceiverDeleting(receiver.id)\"\r\n (onClick)=\"onDeleteReceiver(receiver)\"\r\n />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n <!-- Actions -->\r\n <div class=\"flex justify-end gap-2 pt-4\">\r\n @if (!template.isSystem) {\r\n <mt-button\r\n icon=\"general.trash-01\"\r\n [label]=\"'notification.delete' | transloco\"\r\n severity=\"danger\"\r\n [loading]=\"isDeletingTemplate()\"\r\n [disabled]=\"isUpdatingTemplate()\"\r\n (onClick)=\"onDeleteTemplate()\"\r\n />\r\n }\r\n <mt-button\r\n icon=\"general.save-01\"\r\n [label]=\"'notification.save' | transloco\"\r\n [loading]=\"isUpdatingTemplate()\"\r\n [disabled]=\"isDeletingTemplate() || isFormInvalid()\"\r\n (onClick)=\"onSaveTemplate()\"\r\n />\r\n </div>\r\n </div>\r\n } @else {\r\n <!-- No template selected -->\r\n <div\r\n class=\"flex flex-col items-center justify-center py-12 text-slate-500\"\r\n >\r\n <mt-icon icon=\"alert.bell-01\" class=\"mb-2 h-12 w-12\" />\r\n <p>{{ \"notification.selectTemplatePrompt\" | transloco }}</p>\r\n </div>\r\n }\r\n }\r\n }\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}:host ::ng-deep mt-textarea-field>.flex.items-center,:host ::ng-deep mt-editor-field>.flex.items-center{direction:ltr}:host-context([dir=rtl]) ::ng-deep mt-textarea-field>.flex.items-center,:host-context([dir=rtl]) ::ng-deep mt-editor-field>.flex.items-center{direction:rtl}\n"] }]
|
|
1256
|
-
}], ctorParameters: () => [] });
|
|
2360
|
+
NotificationContentPreview,
|
|
2361
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'notification'\">\n <div class=\"flex min-w-0 flex-col gap-4 p-4\">\n @if (facade.isLoadingEvents() && events().length === 0) {\n <div class=\"flex flex-col gap-4\" aria-live=\"polite\">\n <p-skeleton width=\"50%\" height=\"2.5rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"2.5rem\" borderRadius=\"0.375rem\" />\n <p-skeleton width=\"100%\" height=\"16rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else if (facade.eventsError() && events().length === 0) {\n <mt-empty-state\n illustration=\"none\"\n [title]=\"t('load-error')\"\n [actionLabel]=\"t('actions.retry')\"\n actionIcon=\"arrow.refresh-cw-01\"\n (actionClick)=\"retryLoad()\"\n />\n } @else if (events().length === 0) {\n <mt-empty-state illustration=\"none\" [title]=\"t('noEventsFound')\" />\n } @else {\n <!-- Event -->\n <div class=\"w-full md:w-1/2\">\n <mt-select-field\n [formControl]=\"eventControl\"\n [options]=\"events()\"\n optionLabel=\"name\"\n optionValue=\"id\"\n [label]=\"t('events')\"\n [placeholder]=\"t('selectEvent')\"\n [filter]=\"events().length > 8\"\n filterBy=\"name\"\n [showClear]=\"false\"\n (onChange)=\"onEventChange($event)\"\n />\n </div>\n\n @if (selectedEvent(); as event) {\n <!-- Ready-made defaults -->\n @if (missingDefaults().length > 0 || removedDefaults().length > 0) {\n <div\n class=\"flex flex-col gap-2 rounded-md border border-surface bg-surface-50 px-4 py-3 text-sm\"\n >\n @if (missingDefaults().length > 0) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n <span class=\"flex items-center gap-2\">\n <mt-icon\n icon=\"general.zap\"\n class=\"size-4 shrink-0 text-primary\"\n />\n {{ t(\"defaults.missing\") }}\n </span>\n <mt-button\n size=\"small\"\n [label]=\"t('defaults.add')\"\n [loading]=\"facade.isLoading('initializeDefaults')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onInitializeDefaults()\"\n />\n </div>\n }\n @for (item of removedDefaults(); track item.slot.channel) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n <span>{{\n t(\"defaults.removed\", {\n channel: channelLabel(item.slot.channel),\n })\n }}</span>\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('defaults.restore')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onRestoreDefault(item.slot.channel)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- Templates of the event -->\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (templates().length > 0) {\n <mt-tabs\n class=\"min-w-0 flex-1\"\n mode=\"underline\"\n [active]=\"activeTab()\"\n [options]=\"templateTabs()\"\n (activeChange)=\"onTemplateTabChange($event)\"\n />\n } @else {\n <span class=\"text-sm text-muted-color\">{{\n t(\"no-templates\")\n }}</span>\n }\n <mt-button\n icon=\"general.plus\"\n [label]=\"t('addNotification')\"\n size=\"small\"\n [disabled]=\"isMutating() || !context()\"\n (onClick)=\"onAddTemplate()\"\n />\n </div>\n\n @if (facade.contextError() && !context()) {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\"\n role=\"alert\"\n >\n <span>{{ t(\"context-error\") }}</span>\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.retry')\"\n (onClick)=\"retryLoad()\"\n />\n </div>\n }\n\n @if (template(); as template) {\n @if (!context()) {\n <div class=\"flex flex-col gap-3\">\n <p-skeleton\n width=\"100%\"\n height=\"3.5rem\"\n borderRadius=\"0.375rem\"\n />\n <p-skeleton width=\"100%\" height=\"20rem\" borderRadius=\"0.375rem\" />\n </div>\n } @else {\n <!-- Status and actions -->\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-surface bg-content px-4 py-3\"\n >\n <div class=\"flex min-w-0 flex-wrap items-center gap-2 text-sm\">\n <span\n class=\"inline-flex items-center gap-1 rounded-md border border-surface bg-surface-50 px-2 py-0.5\"\n >\n <mt-icon\n [icon]=\"channelIcons[template.channel]\"\n class=\"size-4\"\n />\n {{ channelLabel(template.channel) }}\n </span>\n @switch (status()) {\n @case (\"on\") {\n <span\n class=\"rounded-md border border-green-200 bg-green-50 px-2 py-0.5 font-medium text-green-700\"\n [mtTooltip]=\"t('status.on-hint')\"\n >{{ t(\"status.on\") }}</span\n >\n }\n @case (\"off\") {\n <span\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-0.5 font-medium text-muted-color\"\n [mtTooltip]=\"t('status.off-hint')\"\n >{{ t(\"status.off\") }}</span\n >\n }\n @case (\"draft\") {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 font-medium text-amber-700\"\n [mtTooltip]=\"t('status.draft-hint')\"\n >{{ t(\"status.draft\") }}</span\n >\n }\n }\n @if (unpublishedChanges()) {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-amber-700\"\n [mtTooltip]=\"t('status.unpublished-hint')\"\n >{{ t(\"status.unpublished\") }}</span\n >\n }\n @if (defaultOwned()) {\n <span\n class=\"rounded-md border border-surface bg-surface-50 px-2 py-0.5 text-muted-color\"\n >{{ t(\"status.default\") }}</span\n >\n }\n @if (dirty()) {\n <span\n class=\"rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-amber-700\"\n >{{ t(\"status.unsaved\") }}</span\n >\n }\n </div>\n\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n icon=\"time.clock-rewind\"\n severity=\"secondary\"\n variant=\"text\"\n [tooltip]=\"t('actions.history')\"\n [ariaLabel]=\"t('actions.history')\"\n [disabled]=\"template.publishedRevision === null\"\n (onClick)=\"onOpenHistory()\"\n />\n @if (template.channel === \"Email\") {\n <mt-button\n icon=\"communication.send-01\"\n severity=\"secondary\"\n variant=\"text\"\n [tooltip]=\"\n dirty() ? t('saveBeforeEmailTest') : t('testEmail')\n \"\n [ariaLabel]=\"t('testEmail')\"\n [disabled]=\"dirty() || isMutating()\"\n (onClick)=\"onTestEmail()\"\n />\n }\n @if (!template.isSystem) {\n <mt-button\n icon=\"general.trash-01\"\n severity=\"danger\"\n variant=\"text\"\n [tooltip]=\"t('actions.delete')\"\n [ariaLabel]=\"t('actions.delete')\"\n [loading]=\"facade.isLoading('deleteTemplate')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onDelete()\"\n />\n }\n @if (status() === \"on\") {\n <mt-button\n icon=\"general.toggle-01-left\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.turn-off')\"\n [loading]=\"facade.isLoading('setActivation')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onTurnOff()\"\n />\n } @else {\n <mt-button\n icon=\"general.toggle-01-right\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.turn-on')\"\n [tooltip]=\"\n template.publishedRevision === null\n ? t('actions.turn-on-blocked')\n : ''\n \"\n [loading]=\"facade.isLoading('setActivation')\"\n [disabled]=\"!canTurnOn()\"\n (onClick)=\"onTurnOn()\"\n />\n }\n <mt-button\n icon=\"general.check\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('actions.publish')\"\n [tooltip]=\"dirty() ? t('actions.save-first') : ''\"\n [loading]=\"facade.isLoading('publishTemplate')\"\n [disabled]=\"!canPublish()\"\n (onClick)=\"onPublish()\"\n />\n @if (dirty()) {\n <mt-button\n severity=\"secondary\"\n variant=\"text\"\n [label]=\"t('actions.discard')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onDiscard()\"\n />\n }\n <mt-button\n icon=\"general.save-01\"\n [label]=\"t('save')\"\n [loading]=\"facade.isLoading('saveTemplate')\"\n [disabled]=\"!canSave()\"\n (onClick)=\"onSave()\"\n />\n </div>\n </div>\n\n @if (conflict()) {\n <div\n class=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700\"\n role=\"alert\"\n >\n <span>{{ t(\"conflict.message\") }}</span>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n [label]=\"t('conflict.keep-mine')\"\n (onClick)=\"resolveConflict(true)\"\n />\n <mt-button\n size=\"small\"\n severity=\"secondary\"\n [label]=\"t('conflict.load-latest')\"\n (onClick)=\"resolveConflict(false)\"\n />\n </div>\n </div>\n }\n\n @if (writeDiagnostics().length > 0) {\n <ul\n class=\"list-disc rounded-md border border-red-200 bg-red-50 py-3 pe-4 ps-8 text-sm text-red-700\"\n role=\"alert\"\n >\n @for (diagnostic of writeDiagnostics(); track $index) {\n <li>{{ describeDiagnostic(diagnostic) }}</li>\n }\n </ul>\n }\n\n <div\n class=\"grid min-w-0 gap-4\"\n [class]=\"wide() ? 'grid-cols-2' : 'grid-cols-1'\"\n [formGroup]=\"form\"\n >\n <!-- Content -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.content\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-4 p-4\">\n <mt-text-field\n formControlName=\"name\"\n [label]=\"t('name')\"\n [placeholder]=\"t('enterName')\"\n [required]=\"true\"\n />\n @if (template.channel === \"Email\") {\n <mt-text-field\n formControlName=\"subject\"\n [label]=\"t('subject')\"\n [hint]=\"t('subject-hint')\"\n />\n }\n <mt-select-field\n [formControl]=\"placeholderControl\"\n [options]=\"placeholderGroups()\"\n optionLabel=\"label\"\n optionValue=\"key\"\n [group]=\"true\"\n optionGroupLabel=\"groupName\"\n optionGroupChildren=\"properties\"\n [label]=\"t('placeholders.label')\"\n [hint]=\"t('placeholders.hint')\"\n [placeholder]=\"t('placeholders.placeholder')\"\n [hasPlaceholderPrefix]=\"false\"\n [filter]=\"true\"\n filterBy=\"label\"\n (onChange)=\"onInsertPlaceholder($event)\"\n />\n @switch (template.channel) {\n @case (\"Email\") {\n <mt-email-visual-editor\n #emailEditor\n formControlName=\"emailTemplate\"\n [label]=\"t('email-body')\"\n [hint]=\"t('email-body-hint')\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"emailImageOrigins()\"\n height=\"26rem\"\n />\n }\n @case (\"Sms\") {\n <div #smsSource (focusin)=\"setActiveField('smsTemplate')\">\n <mt-textarea-field\n dir=\"auto\"\n formControlName=\"smsTemplate\"\n [label]=\"t('sms-body')\"\n [hint]=\"t('sms-body-hint')\"\n rows=\"6\"\n />\n </div>\n }\n @case (\"App\") {\n <mt-email-visual-editor\n #appEnEditor\n formControlName=\"appEn\"\n [label]=\"t('english')\"\n direction=\"ltr\"\n [starter]=\"false\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"appImageOrigins()\"\n height=\"12rem\"\n (focused)=\"setActiveField('appEn')\"\n />\n <mt-email-visual-editor\n #appArEditor\n formControlName=\"appAr\"\n [label]=\"t('arabic')\"\n direction=\"rtl\"\n [starter]=\"false\"\n [placeholders]=\"placeholderOptions()\"\n [approvedImageOrigins]=\"appImageOrigins()\"\n height=\"12rem\"\n (focused)=\"setActiveField('appAr')\"\n />\n }\n }\n </div>\n </section>\n\n <!-- Live preview -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.preview\") }}\n </header>\n <div class=\"flex flex-1 flex-col p-4\">\n <mt-notification-content-preview\n [preview]=\"preview.result()\"\n [status]=\"preview.status()\"\n [errorCode]=\"preview.errorCode()\"\n [stale]=\"previewStale()\"\n [fill]=\"true\"\n [showSubject]=\"false\"\n (retry)=\"preview.retry()\"\n />\n </div>\n </section>\n\n <!-- Recipients -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.recipients\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-3 p-4\">\n <mt-select-field\n [formControl]=\"recipientControl\"\n [options]=\"recipientGroups()\"\n optionLabel=\"name\"\n optionValue=\"key\"\n [group]=\"true\"\n optionGroupLabel=\"groupName\"\n optionGroupChildren=\"items\"\n [label]=\"t('recipients.add')\"\n [hint]=\"t('recipients.hint')\"\n [placeholder]=\"t('recipients.placeholder')\"\n [filter]=\"true\"\n filterBy=\"name\"\n [loading]=\"facade.isLoading('addReceivers')\"\n (onChange)=\"onAddRecipient($event)\"\n />\n @if (template.receivers.length > 0) {\n <ul class=\"flex flex-wrap gap-2\">\n @for (receiver of template.receivers; track receiver.id) {\n <li\n class=\"flex items-center gap-2 rounded-md border border-surface bg-surface-50 py-1 pe-1 ps-3 text-sm\"\n >\n <span class=\"font-medium\">{{\n receiverName(receiver)\n }}</span>\n <span class=\"text-xs text-muted-color\">{{\n t(\"recipient-type.\" + receiver.type)\n }}</span>\n @if (!receiver.isSystem) {\n <mt-button\n icon=\"general.x-close\"\n severity=\"secondary\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"t('recipients.remove')\"\n [disabled]=\"isMutating()\"\n (onClick)=\"onRemoveRecipient(receiver)\"\n />\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"text-sm text-amber-700\">\n {{ t(\"recipients.empty\") }}\n </p>\n }\n </div>\n </section>\n\n <!-- Send only when -->\n <section\n class=\"flex min-w-0 flex-col overflow-hidden rounded-md border border-surface bg-content\"\n >\n <header\n class=\"border-b border-surface bg-surface-50 px-4 py-2.5 text-sm font-semibold\"\n >\n {{ t(\"sections.condition\") }}\n </header>\n <div class=\"flex min-w-0 flex-col gap-2 p-4\">\n <p class=\"text-sm text-muted-color\">\n {{ t(\"condition.hint\") }}\n </p>\n <mt-formula-builder\n formControlName=\"conditionFormula\"\n valueMode=\"expression\"\n [isProcessBuilder]=\"true\"\n [propertiesByPath]=\"conditionPropertiesByPath()\"\n [knownProperties]=\"conditionKeys()\"\n [placeholder]=\"t('condition.placeholder')\"\n />\n </div>\n </section>\n </div>\n }\n } @else if (templates().length > 0) {\n <p class=\"py-8 text-center text-sm text-muted-color\">\n {{ t(\"selectTemplatePrompt\") }}\n </p>\n }\n }\n }\n </div>\n</ng-container>\n", styles: [":host{display:block;width:100%;height:100%}:host ::ng-deep mt-textarea-field>.flex.items-center,:host ::ng-deep mt-editor-field>.flex.items-center{direction:ltr}:host-context([dir=rtl]) ::ng-deep mt-textarea-field>.flex.items-center,:host-context([dir=rtl]) ::ng-deep mt-editor-field>.flex.items-center{direction:rtl}\n"] }]
|
|
2362
|
+
}], ctorParameters: () => [], propDecorators: { emailEditor: [{ type: i0.ViewChild, args: ['emailEditor', { ...{
|
|
2363
|
+
read: EmailVisualEditor,
|
|
2364
|
+
}, isSignal: true }] }], appEnEditor: [{ type: i0.ViewChild, args: ['appEnEditor', { ...{
|
|
2365
|
+
read: EmailVisualEditor,
|
|
2366
|
+
}, isSignal: true }] }], appArEditor: [{ type: i0.ViewChild, args: ['appArEditor', { ...{
|
|
2367
|
+
read: EmailVisualEditor,
|
|
2368
|
+
}, isSignal: true }] }], smsSource: [{ type: i0.ViewChild, args: ['smsSource', { isSignal: true }] }], onBeforeUnload: [{
|
|
2369
|
+
type: HostListener,
|
|
2370
|
+
args: ['window:beforeunload', ['$event']]
|
|
2371
|
+
}] } });
|
|
2372
|
+
function defaultField(channel) {
|
|
2373
|
+
switch (channel) {
|
|
2374
|
+
case 'Email':
|
|
2375
|
+
return 'emailTemplate';
|
|
2376
|
+
case 'Sms':
|
|
2377
|
+
return 'smsTemplate';
|
|
2378
|
+
case 'App':
|
|
2379
|
+
return 'appEn';
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function publicationReview(publication, nameOf) {
|
|
2383
|
+
return {
|
|
2384
|
+
request: publicationPreviewRequest(publication.content),
|
|
2385
|
+
recipients: publication.content.receivers.map((receiver) => ({
|
|
2386
|
+
name: nameOf(receiver),
|
|
2387
|
+
type: receiver.type,
|
|
2388
|
+
})),
|
|
2389
|
+
condition: publication.content.conditionFormula,
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
1257
2392
|
|
|
1258
2393
|
const APP_STATES = [NotificationState];
|
|
1259
2394
|
|
|
@@ -1261,5 +2396,5 @@ const APP_STATES = [NotificationState];
|
|
|
1261
2396
|
* Generated bundle index. Do not edit.
|
|
1262
2397
|
*/
|
|
1263
2398
|
|
|
1264
|
-
export { APP_STATES, AddReceivers,
|
|
2399
|
+
export { APP_STATES, AddReceivers, AddTemplateDialog, CreateTemplate, DEFAULTS_PUBLISHER, DeleteReceiver, DeleteTemplate, GetAuthoringContext, GetDefaultRecipes, GetEvents, GetPublications, InitializeDefaults, NOTIFICATION_CHANNELS, NOTIFICATION_MUTATION_KEYS, NotificationActionKey, NotificationContentPreview, NotificationFacade, NotificationState, NotificationTemplate, NotificationTemplateApi, PublishTemplate, ResetNotificationState, RestoreDefault, RestorePublication, SaveTemplate, SelectEvent, SelectTemplate, SetModuleInfo, SetTemplateActivation, TemplateHistoryDrawer, TemplateReviewDialog, currentPublication, flattenRecipientCatalog, hasUnpublishedChanges, isDefaultOwned, isRevisionConflict, missingDefaultChannels, parseAppContent, previewSamples, publicationPreviewRequest, readNotificationFailure, recipientKey, templatePreviewRequest, templateStatus, withPreviewSamples };
|
|
1265
2400
|
//# sourceMappingURL=masterteam-notification.mjs.map
|