@masterteam/notification 0.0.28 → 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 -47
- package/assets/i18n/en.json +236 -47
- package/assets/notification.css +1 -1
- package/fesm2022/masterteam-notification.mjs +1973 -872
- package/fesm2022/masterteam-notification.mjs.map +1 -1
- package/package.json +6 -5
- package/types/masterteam-notification.d.ts +789 -307
|
@@ -1,114 +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 {
|
|
23
|
-
import { ModalRef } from '@masterteam/components/dialog';
|
|
24
|
-
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';
|
|
25
23
|
import { Action, Selector, State, Store, select } from '@ngxs/store';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
data = input(null, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
32
|
-
templateNameControl = new FormControl('', [Validators.required]);
|
|
33
|
-
onSave() {
|
|
34
|
-
if (this.templateNameControl.valid) {
|
|
35
|
-
this.ref.close({ name: this.templateNameControl.value?.trim() });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
onCancel() {
|
|
39
|
-
this.ref.close();
|
|
40
|
-
}
|
|
41
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
42
|
-
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" }] });
|
|
43
|
-
}
|
|
44
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AddTemplateDialog, decorators: [{
|
|
45
|
-
type: Component,
|
|
46
|
-
args: [{ selector: 'mt-add-template-dialog', standalone: true, imports: [
|
|
47
|
-
CommonModule,
|
|
48
|
-
ReactiveFormsModule,
|
|
49
|
-
TranslocoModule,
|
|
50
|
-
TextField,
|
|
51
|
-
Button,
|
|
52
|
-
], 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" }]
|
|
53
|
-
}], 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';
|
|
54
29
|
|
|
55
30
|
// ============================================================================
|
|
56
|
-
// Action Keys
|
|
31
|
+
// Action Keys
|
|
57
32
|
// ============================================================================
|
|
58
33
|
var NotificationActionKey;
|
|
59
34
|
(function (NotificationActionKey) {
|
|
60
35
|
NotificationActionKey["GetEvents"] = "getEvents";
|
|
61
|
-
NotificationActionKey["
|
|
62
|
-
NotificationActionKey["
|
|
36
|
+
NotificationActionKey["GetContext"] = "getContext";
|
|
37
|
+
NotificationActionKey["GetDefaults"] = "getDefaults";
|
|
38
|
+
NotificationActionKey["GetPublications"] = "getPublications";
|
|
39
|
+
NotificationActionKey["CreateTemplate"] = "createTemplate";
|
|
40
|
+
NotificationActionKey["SaveTemplate"] = "saveTemplate";
|
|
63
41
|
NotificationActionKey["AddReceivers"] = "addReceivers";
|
|
64
42
|
NotificationActionKey["DeleteReceiver"] = "deleteReceiver";
|
|
65
|
-
NotificationActionKey["AddTemplate"] = "addTemplate";
|
|
66
|
-
NotificationActionKey["UpdateTemplate"] = "updateTemplate";
|
|
67
43
|
NotificationActionKey["DeleteTemplate"] = "deleteTemplate";
|
|
44
|
+
NotificationActionKey["PublishTemplate"] = "publishTemplate";
|
|
45
|
+
NotificationActionKey["SetActivation"] = "setActivation";
|
|
46
|
+
NotificationActionKey["RestorePublication"] = "restorePublication";
|
|
47
|
+
NotificationActionKey["InitializeDefaults"] = "initializeDefaults";
|
|
48
|
+
NotificationActionKey["RestoreDefault"] = "restoreDefault";
|
|
68
49
|
})(NotificationActionKey || (NotificationActionKey = {}));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
key: 'app',
|
|
88
|
-
labelKey: 'notification.channel.app',
|
|
89
|
-
supportsBilingual: true,
|
|
90
|
-
bilingualOptional: false,
|
|
91
|
-
useRichEditor: true,
|
|
92
|
-
translatableKey: 'appTemplate',
|
|
93
|
-
},
|
|
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',
|
|
94
67
|
];
|
|
95
|
-
/** Helper to get channel config by key */
|
|
96
|
-
function getChannelConfig(key) {
|
|
97
|
-
return CHANNEL_CONFIGS.find((c) => c.key === key) ?? CHANNEL_CONFIGS[0];
|
|
98
|
-
}
|
|
99
|
-
/** Build form control name for a channel field */
|
|
100
|
-
function getFormControlName(ch, type) {
|
|
101
|
-
if (type === 'single')
|
|
102
|
-
return ch.templateKey ?? null;
|
|
103
|
-
if (!ch.translatableKey)
|
|
104
|
-
return null;
|
|
105
|
-
return type === 'en'
|
|
106
|
-
? `${ch.translatableKey}.en`
|
|
107
|
-
: `${ch.translatableKey}.ar`;
|
|
108
|
-
}
|
|
109
68
|
|
|
110
69
|
// ============================================================================
|
|
111
|
-
//
|
|
70
|
+
// Scope
|
|
112
71
|
// ============================================================================
|
|
113
72
|
class SetModuleInfo {
|
|
114
73
|
moduleType;
|
|
@@ -125,56 +84,45 @@ class SetModuleInfo {
|
|
|
125
84
|
this.parentPath = parentPath;
|
|
126
85
|
}
|
|
127
86
|
}
|
|
87
|
+
class ResetNotificationState {
|
|
88
|
+
static type = '[Notification] Reset State';
|
|
89
|
+
}
|
|
128
90
|
// ============================================================================
|
|
129
|
-
//
|
|
91
|
+
// Loading
|
|
130
92
|
// ============================================================================
|
|
93
|
+
/** Loads (or re-reads) the scope's events, keeping the current selection when it still exists. */
|
|
131
94
|
class GetEvents {
|
|
132
95
|
static type = '[Notification] Get Events';
|
|
133
96
|
}
|
|
134
|
-
class
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
static type = '[Notification] Get Properties';
|
|
140
|
-
constructor(isMultiLang) {
|
|
141
|
-
this.isMultiLang = isMultiLang;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// ============================================================================
|
|
145
|
-
// Action Mode (Workflow)
|
|
146
|
-
// ============================================================================
|
|
147
|
-
class SetActionMode {
|
|
148
|
-
actionId;
|
|
149
|
-
static type = '[Notification] Set Action Mode';
|
|
150
|
-
constructor(actionId) {
|
|
151
|
-
this.actionId = actionId;
|
|
97
|
+
class GetAuthoringContext {
|
|
98
|
+
eventId;
|
|
99
|
+
static type = '[Notification] Get Authoring Context';
|
|
100
|
+
constructor(eventId) {
|
|
101
|
+
this.eventId = eventId;
|
|
152
102
|
}
|
|
153
103
|
}
|
|
154
|
-
class
|
|
155
|
-
|
|
156
|
-
static type = '[Notification] Get
|
|
157
|
-
constructor(
|
|
158
|
-
this.
|
|
104
|
+
class GetDefaultRecipes {
|
|
105
|
+
eventId;
|
|
106
|
+
static type = '[Notification] Get Default Recipes';
|
|
107
|
+
constructor(eventId) {
|
|
108
|
+
this.eventId = eventId;
|
|
159
109
|
}
|
|
160
110
|
}
|
|
161
|
-
class
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.actionId = actionId;
|
|
167
|
-
this.isMultiLang = isMultiLang;
|
|
111
|
+
class GetPublications {
|
|
112
|
+
templateId;
|
|
113
|
+
static type = '[Notification] Get Publications';
|
|
114
|
+
constructor(templateId) {
|
|
115
|
+
this.templateId = templateId;
|
|
168
116
|
}
|
|
169
117
|
}
|
|
170
118
|
// ============================================================================
|
|
171
|
-
// Selection
|
|
119
|
+
// Selection
|
|
172
120
|
// ============================================================================
|
|
173
121
|
class SelectEvent {
|
|
174
|
-
|
|
122
|
+
eventId;
|
|
175
123
|
static type = '[Notification] Select Event';
|
|
176
|
-
constructor(
|
|
177
|
-
this.
|
|
124
|
+
constructor(eventId) {
|
|
125
|
+
this.eventId = eventId;
|
|
178
126
|
}
|
|
179
127
|
}
|
|
180
128
|
class SelectTemplate {
|
|
@@ -185,58 +133,426 @@ class SelectTemplate {
|
|
|
185
133
|
}
|
|
186
134
|
}
|
|
187
135
|
// ============================================================================
|
|
188
|
-
//
|
|
136
|
+
// Template writes
|
|
189
137
|
// ============================================================================
|
|
190
|
-
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 {
|
|
191
148
|
templateId;
|
|
192
149
|
payload;
|
|
193
|
-
static type = '[Notification]
|
|
150
|
+
static type = '[Notification] Save Template';
|
|
194
151
|
constructor(templateId, payload) {
|
|
195
152
|
this.templateId = templateId;
|
|
196
153
|
this.payload = payload;
|
|
197
154
|
}
|
|
198
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
|
+
}
|
|
199
176
|
class DeleteReceiver {
|
|
200
177
|
receiverId;
|
|
178
|
+
expectedRevision;
|
|
201
179
|
static type = '[Notification] Delete Receiver';
|
|
202
|
-
constructor(receiverId) {
|
|
180
|
+
constructor(receiverId, expectedRevision) {
|
|
203
181
|
this.receiverId = receiverId;
|
|
182
|
+
this.expectedRevision = expectedRevision;
|
|
204
183
|
}
|
|
205
184
|
}
|
|
206
185
|
// ============================================================================
|
|
207
|
-
//
|
|
186
|
+
// Lifecycle
|
|
208
187
|
// ============================================================================
|
|
209
|
-
class
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
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;
|
|
216
197
|
}
|
|
217
198
|
}
|
|
218
|
-
class
|
|
199
|
+
class SetTemplateActivation {
|
|
219
200
|
templateId;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
201
|
+
expectedRevision;
|
|
202
|
+
enabled;
|
|
203
|
+
reviews;
|
|
204
|
+
static type = '[Notification] Set Template Activation';
|
|
205
|
+
constructor(templateId, expectedRevision, enabled, reviews) {
|
|
223
206
|
this.templateId = templateId;
|
|
224
|
-
this.
|
|
207
|
+
this.expectedRevision = expectedRevision;
|
|
208
|
+
this.enabled = enabled;
|
|
209
|
+
this.reviews = reviews;
|
|
225
210
|
}
|
|
226
211
|
}
|
|
227
|
-
class
|
|
212
|
+
class RestorePublication {
|
|
228
213
|
templateId;
|
|
229
|
-
|
|
230
|
-
|
|
214
|
+
expectedRevision;
|
|
215
|
+
publicationRevision;
|
|
216
|
+
reviews;
|
|
217
|
+
static type = '[Notification] Restore Publication';
|
|
218
|
+
constructor(templateId, expectedRevision, publicationRevision, reviews) {
|
|
231
219
|
this.templateId = templateId;
|
|
220
|
+
this.expectedRevision = expectedRevision;
|
|
221
|
+
this.publicationRevision = publicationRevision;
|
|
222
|
+
this.reviews = reviews;
|
|
232
223
|
}
|
|
233
224
|
}
|
|
234
225
|
// ============================================================================
|
|
235
|
-
//
|
|
226
|
+
// Defaults
|
|
236
227
|
// ============================================================================
|
|
237
|
-
class
|
|
238
|
-
|
|
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');
|
|
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;
|
|
239
553
|
}
|
|
554
|
+
/** The system principal that publishes generated App defaults. */
|
|
555
|
+
const DEFAULTS_PUBLISHER = 'system:notification-defaults';
|
|
240
556
|
|
|
241
557
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
242
558
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -244,59 +560,51 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
244
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;
|
|
245
561
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
246
562
|
};
|
|
247
|
-
// Default State
|
|
248
563
|
const DEFAULT_STATE = {
|
|
249
|
-
// Module configuration
|
|
250
564
|
moduleType: null,
|
|
251
565
|
moduleId: null,
|
|
252
566
|
parentModuleType: null,
|
|
253
567
|
parentModuleId: null,
|
|
254
568
|
parentPath: '',
|
|
255
|
-
// Action mode
|
|
256
|
-
isActionMode: false,
|
|
257
|
-
selectedActionId: null,
|
|
258
|
-
// Data collections
|
|
259
569
|
events: [],
|
|
260
|
-
templates: [],
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
selectedEventName: null,
|
|
570
|
+
templates: [],
|
|
571
|
+
context: null,
|
|
572
|
+
defaults: null,
|
|
573
|
+
publications: [],
|
|
574
|
+
publicationsTemplateId: null,
|
|
575
|
+
selectedEventId: null,
|
|
267
576
|
selectedTemplateId: null,
|
|
268
|
-
// Loading state (from LoadingStateShape)
|
|
269
577
|
loadingActive: [],
|
|
270
578
|
errors: {},
|
|
271
579
|
};
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
static getState(state) {
|
|
278
|
-
return state;
|
|
279
|
-
}
|
|
580
|
+
let NotificationState = class NotificationState {
|
|
581
|
+
api = inject(NotificationTemplateApi);
|
|
582
|
+
// ==========================================================================
|
|
583
|
+
// Slice selectors
|
|
584
|
+
// ==========================================================================
|
|
280
585
|
static getEvents(state) {
|
|
281
586
|
return state.events;
|
|
282
587
|
}
|
|
283
588
|
static getTemplates(state) {
|
|
284
589
|
return state.templates;
|
|
285
590
|
}
|
|
286
|
-
static
|
|
287
|
-
return state.
|
|
591
|
+
static getSelectedEventId(state) {
|
|
592
|
+
return state.selectedEventId;
|
|
288
593
|
}
|
|
289
|
-
static
|
|
290
|
-
return state.
|
|
594
|
+
static getSelectedTemplateId(state) {
|
|
595
|
+
return state.selectedTemplateId;
|
|
291
596
|
}
|
|
292
|
-
static
|
|
293
|
-
return state.
|
|
597
|
+
static getContext(state) {
|
|
598
|
+
return state.context;
|
|
294
599
|
}
|
|
295
|
-
static
|
|
296
|
-
return state.
|
|
600
|
+
static getDefaults(state) {
|
|
601
|
+
return state.defaults;
|
|
297
602
|
}
|
|
298
|
-
static
|
|
299
|
-
return
|
|
603
|
+
static getPublicationsSlice(state) {
|
|
604
|
+
return {
|
|
605
|
+
templateId: state.publicationsTemplateId,
|
|
606
|
+
items: state.publications,
|
|
607
|
+
};
|
|
300
608
|
}
|
|
301
609
|
static getLoadingActive(state) {
|
|
302
610
|
return state.loadingActive;
|
|
@@ -304,119 +612,26 @@ let NotificationState = class NotificationState extends CrudStateBase {
|
|
|
304
612
|
static getErrors(state) {
|
|
305
613
|
return state.errors;
|
|
306
614
|
}
|
|
615
|
+
// ==========================================================================
|
|
307
616
|
// Derived selectors
|
|
308
|
-
|
|
309
|
-
|
|
617
|
+
// ==========================================================================
|
|
618
|
+
static getSelectedEvent(events, selectedEventId) {
|
|
619
|
+
return events.find((event) => event.id === selectedEventId) ?? null;
|
|
310
620
|
}
|
|
311
|
-
static getEventTemplates(templates,
|
|
312
|
-
if (
|
|
621
|
+
static getEventTemplates(templates, selectedEventId) {
|
|
622
|
+
if (selectedEventId == null)
|
|
313
623
|
return [];
|
|
314
|
-
return templates.filter((
|
|
624
|
+
return templates.filter((template) => template.eventId === selectedEventId);
|
|
315
625
|
}
|
|
316
626
|
static getSelectedTemplate(templates, selectedTemplateId) {
|
|
317
|
-
return templates.find((
|
|
318
|
-
}
|
|
319
|
-
static getAvailableReceivers(receivers, selectedTemplate) {
|
|
320
|
-
if (!selectedTemplate)
|
|
321
|
-
return receivers;
|
|
322
|
-
const templateReceivers = selectedTemplate.receivers ?? [];
|
|
323
|
-
const assignedIds = new Set(templateReceivers.map((r) => r.identifier));
|
|
324
|
-
return receivers.filter((r) => !assignedIds.has(String(r.id)));
|
|
325
|
-
}
|
|
326
|
-
static getAvailableReceiverGroups(receiverGroups, selectedTemplate) {
|
|
327
|
-
if (!selectedTemplate)
|
|
328
|
-
return receiverGroups;
|
|
329
|
-
const assignedIds = new Set((selectedTemplate.receivers ?? []).map((r) => r.identifier));
|
|
330
|
-
return receiverGroups
|
|
331
|
-
.map((group) => ({
|
|
332
|
-
...group,
|
|
333
|
-
items: group.items.filter((item) => !assignedIds.has(String(item.id))),
|
|
334
|
-
}))
|
|
335
|
-
.filter((group) => group.items.length > 0);
|
|
336
|
-
}
|
|
337
|
-
// Helper Methods
|
|
338
|
-
/** API returns events with nested templates - we flatten for state */
|
|
339
|
-
flattenEventsResponse(eventsWithTemplates) {
|
|
340
|
-
const events = [];
|
|
341
|
-
const templates = [];
|
|
342
|
-
for (const event of eventsWithTemplates) {
|
|
343
|
-
const { templates: eventTemplates, ...eventData } = event;
|
|
344
|
-
events.push(eventData);
|
|
345
|
-
templates.push(...eventTemplates.map((t) => ({ ...t, eventId: event.id })));
|
|
346
|
-
}
|
|
347
|
-
return { events, templates };
|
|
348
|
-
}
|
|
349
|
-
normalizeReceiverGroups(groups) {
|
|
350
|
-
return groups
|
|
351
|
-
.map((group) => ({
|
|
352
|
-
groupName: group.groupName,
|
|
353
|
-
items: this.flattenReceiverItems(group.items),
|
|
354
|
-
}))
|
|
355
|
-
.filter((group) => group.items.length > 0);
|
|
356
|
-
}
|
|
357
|
-
flattenReceiverItems(items, parentLabel) {
|
|
358
|
-
if (!Array.isArray(items)) {
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
361
|
-
return items.flatMap((item) => {
|
|
362
|
-
const subGroupItems = item.items;
|
|
363
|
-
if (Array.isArray(subGroupItems)) {
|
|
364
|
-
const subGroupName = item.subGroupName;
|
|
365
|
-
const nextLabel = subGroupName
|
|
366
|
-
? parentLabel
|
|
367
|
-
? `${parentLabel} - ${subGroupName}`
|
|
368
|
-
: subGroupName
|
|
369
|
-
: parentLabel;
|
|
370
|
-
return this.flattenReceiverItems(subGroupItems, nextLabel);
|
|
371
|
-
}
|
|
372
|
-
const receiver = item;
|
|
373
|
-
if (receiver.id == null || !receiver.name) {
|
|
374
|
-
return [];
|
|
375
|
-
}
|
|
376
|
-
return [
|
|
377
|
-
{
|
|
378
|
-
...receiver,
|
|
379
|
-
name: parentLabel
|
|
380
|
-
? `${parentLabel} - ${receiver.name}`
|
|
381
|
-
: receiver.name,
|
|
382
|
-
},
|
|
383
|
-
];
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
hasOwnField(value, field) {
|
|
387
|
-
return Object.prototype.hasOwnProperty.call(value, field);
|
|
627
|
+
return (templates.find((template) => template.id === selectedTemplateId) ?? null);
|
|
388
628
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
...existingTemplate,
|
|
392
|
-
name: updatedTemplate.name ?? existingTemplate.name,
|
|
393
|
-
};
|
|
394
|
-
if (this.hasOwnField(updatedTemplate, 'emailTemplate')) {
|
|
395
|
-
mergedTemplate.emailTemplate = updatedTemplate.emailTemplate ?? '';
|
|
396
|
-
}
|
|
397
|
-
if (this.hasOwnField(updatedTemplate, 'emailContentTranslatable') &&
|
|
398
|
-
updatedTemplate.emailContentTranslatable) {
|
|
399
|
-
mergedTemplate.emailContentTranslatable = {
|
|
400
|
-
...existingTemplate.emailContentTranslatable,
|
|
401
|
-
...updatedTemplate.emailContentTranslatable,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
if (this.hasOwnField(updatedTemplate, 'smsTemplate')) {
|
|
405
|
-
mergedTemplate.smsTemplate = updatedTemplate.smsTemplate ?? '';
|
|
406
|
-
}
|
|
407
|
-
if (this.hasOwnField(updatedTemplate, 'appTemplate') &&
|
|
408
|
-
updatedTemplate.appTemplate) {
|
|
409
|
-
mergedTemplate.appTemplate = {
|
|
410
|
-
...existingTemplate.appTemplate,
|
|
411
|
-
...updatedTemplate.appTemplate,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
if (this.hasOwnField(updatedTemplate, 'conditionFormula')) {
|
|
415
|
-
mergedTemplate.conditionFormula = updatedTemplate.conditionFormula;
|
|
416
|
-
}
|
|
417
|
-
return mergedTemplate;
|
|
629
|
+
static getPublications(slice, selectedTemplateId) {
|
|
630
|
+
return slice.templateId === selectedTemplateId ? slice.items : [];
|
|
418
631
|
}
|
|
419
|
-
//
|
|
632
|
+
// ==========================================================================
|
|
633
|
+
// Scope
|
|
634
|
+
// ==========================================================================
|
|
420
635
|
setModuleInfo(ctx, action) {
|
|
421
636
|
let parentPath = '';
|
|
422
637
|
if (action.parentModuleType && action.parentModuleId) {
|
|
@@ -431,218 +646,273 @@ let NotificationState = class NotificationState extends CrudStateBase {
|
|
|
431
646
|
parentModuleType: action.parentModuleType ?? null,
|
|
432
647
|
parentModuleId: action.parentModuleId ?? null,
|
|
433
648
|
parentPath,
|
|
434
|
-
isActionMode: action.moduleType === 'workflow',
|
|
435
649
|
});
|
|
436
650
|
}
|
|
437
651
|
resetState(ctx) {
|
|
438
652
|
ctx.setState(DEFAULT_STATE);
|
|
439
653
|
}
|
|
440
|
-
//
|
|
654
|
+
// ==========================================================================
|
|
655
|
+
// Loading
|
|
656
|
+
// ==========================================================================
|
|
441
657
|
getEvents(ctx) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
658
|
+
const scope = scopePathOf(ctx.getState());
|
|
659
|
+
if (!scope)
|
|
660
|
+
return;
|
|
661
|
+
const previousEventId = ctx.getState().selectedEventId;
|
|
662
|
+
return handleApiRequest({
|
|
663
|
+
ctx,
|
|
445
664
|
key: NotificationActionKey.GetEvents,
|
|
446
|
-
request$:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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 };
|
|
457
679
|
},
|
|
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
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
getAuthoringContext(ctx, action) {
|
|
699
|
+
return handleApiRequest({
|
|
700
|
+
ctx,
|
|
701
|
+
key: NotificationActionKey.GetContext,
|
|
702
|
+
request$: this.api.getContext(action.eventId),
|
|
703
|
+
onSuccess: (context, state) => state.selectedEventId === action.eventId ? { context } : undefined,
|
|
458
704
|
});
|
|
459
705
|
}
|
|
460
|
-
|
|
461
|
-
const { parentPath, moduleType, moduleId } = ctx.getState();
|
|
462
|
-
const req$ = this.http.get(`${this.baseUrl}${parentPath}/${moduleType}/${moduleId}/receivers`);
|
|
706
|
+
getDefaultRecipes(ctx, action) {
|
|
463
707
|
return handleApiRequest({
|
|
464
708
|
ctx,
|
|
465
|
-
key: NotificationActionKey.
|
|
466
|
-
request$:
|
|
467
|
-
onSuccess: (
|
|
468
|
-
const receiverGroups = this.normalizeReceiverGroups(response.data ?? []);
|
|
469
|
-
return {
|
|
470
|
-
receiverGroups,
|
|
471
|
-
receivers: receiverGroups.flatMap((group) => group.items),
|
|
472
|
-
};
|
|
473
|
-
},
|
|
709
|
+
key: NotificationActionKey.GetDefaults,
|
|
710
|
+
request$: this.api.getDefaults(action.eventId),
|
|
711
|
+
onSuccess: (defaults, state) => state.selectedEventId === action.eventId ? { defaults } : undefined,
|
|
474
712
|
});
|
|
475
713
|
}
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
726
|
return handleApiRequest({
|
|
481
727
|
ctx,
|
|
482
|
-
key: NotificationActionKey.
|
|
483
|
-
request$:
|
|
484
|
-
onSuccess: (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
icon: 'custom.list-cards',
|
|
488
|
-
})) ?? [],
|
|
489
|
-
}),
|
|
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,
|
|
490
733
|
});
|
|
491
734
|
}
|
|
492
|
-
//
|
|
493
|
-
|
|
735
|
+
// ==========================================================================
|
|
736
|
+
// Selection
|
|
737
|
+
// ==========================================================================
|
|
738
|
+
selectEvent(ctx, action) {
|
|
739
|
+
const state = ctx.getState();
|
|
740
|
+
if (state.selectedEventId === action.eventId)
|
|
741
|
+
return;
|
|
742
|
+
const firstTemplate = state.templates.find((template) => template.eventId === action.eventId);
|
|
494
743
|
ctx.patchState({
|
|
495
|
-
|
|
744
|
+
selectedEventId: action.eventId,
|
|
745
|
+
selectedTemplateId: firstTemplate?.id ?? null,
|
|
746
|
+
context: null,
|
|
747
|
+
defaults: null,
|
|
748
|
+
publications: [],
|
|
749
|
+
publicationsTemplateId: null,
|
|
496
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);
|
|
497
758
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const receiverGroups = this.normalizeReceiverGroups(response.data ?? []);
|
|
506
|
-
return {
|
|
507
|
-
receiverGroups,
|
|
508
|
-
receivers: receiverGroups.flatMap((group) => group.items),
|
|
509
|
-
};
|
|
510
|
-
},
|
|
759
|
+
selectTemplate(ctx, action) {
|
|
760
|
+
if (ctx.getState().selectedTemplateId === action.templateId)
|
|
761
|
+
return;
|
|
762
|
+
ctx.patchState({
|
|
763
|
+
selectedTemplateId: action.templateId,
|
|
764
|
+
publications: [],
|
|
765
|
+
publicationsTemplateId: null,
|
|
511
766
|
});
|
|
767
|
+
return ctx.dispatch(new GetPublications(action.templateId));
|
|
512
768
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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) {
|
|
516
774
|
return handleApiRequest({
|
|
517
775
|
ctx,
|
|
518
|
-
key: NotificationActionKey.
|
|
519
|
-
request$:
|
|
520
|
-
onSuccess: (
|
|
521
|
-
|
|
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
|
+
: {}),
|
|
522
787
|
}),
|
|
523
788
|
});
|
|
524
789
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
790
|
+
saveTemplate(ctx, action) {
|
|
791
|
+
return handleApiRequest({
|
|
792
|
+
ctx,
|
|
793
|
+
key: NotificationActionKey.SaveTemplate,
|
|
794
|
+
request$: this.api.saveTemplate(action.templateId, action.payload),
|
|
795
|
+
onSuccess: (template, state) => ({
|
|
796
|
+
templates: upsertTemplate(state.templates, template),
|
|
797
|
+
}),
|
|
533
798
|
});
|
|
534
799
|
}
|
|
535
|
-
selectTemplate(ctx, action) {
|
|
536
|
-
ctx.patchState({ selectedTemplateId: action.templateId });
|
|
537
|
-
}
|
|
538
|
-
// Receiver CRUD
|
|
539
800
|
addReceivers(ctx, action) {
|
|
540
|
-
const req$ = this.http.post(`${this.baseUrl}/templates/${action.templateId}`, action.payload);
|
|
541
801
|
return handleApiRequest({
|
|
542
802
|
ctx,
|
|
543
803
|
key: NotificationActionKey.AddReceivers,
|
|
544
|
-
request$:
|
|
545
|
-
onSuccess: (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
receivers: [
|
|
549
|
-
...(state.templates.find((t) => t.id === action.templateId)
|
|
550
|
-
?.receivers ?? []),
|
|
551
|
-
...newReceivers,
|
|
552
|
-
],
|
|
553
|
-
}, 'id');
|
|
554
|
-
return { templates };
|
|
555
|
-
},
|
|
804
|
+
request$: this.api.addReceivers(action.templateId, action.expectedRevision, action.receivers),
|
|
805
|
+
onSuccess: (template, state) => ({
|
|
806
|
+
templates: upsertTemplate(state.templates, template),
|
|
807
|
+
}),
|
|
556
808
|
});
|
|
557
809
|
}
|
|
558
810
|
deleteReceiver(ctx, action) {
|
|
559
|
-
const state = ctx.getState();
|
|
560
|
-
const template = this.getSelectedTemplate(state);
|
|
561
|
-
if (!template) {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const req$ = this.http.delete(`${this.baseUrl}/receivers/${action.receiverId}`);
|
|
565
811
|
return handleApiRequest({
|
|
566
812
|
ctx,
|
|
567
813
|
key: NotificationActionKey.DeleteReceiver,
|
|
568
|
-
request$:
|
|
569
|
-
onSuccess: (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}, 'id');
|
|
573
|
-
return { templates };
|
|
574
|
-
},
|
|
814
|
+
request$: this.api.deleteReceiver(action.receiverId, action.expectedRevision),
|
|
815
|
+
onSuccess: (template, state) => ({
|
|
816
|
+
templates: upsertTemplate(state.templates, template),
|
|
817
|
+
}),
|
|
575
818
|
});
|
|
576
819
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
820
|
+
deleteTemplate(ctx, action) {
|
|
821
|
+
const eventId = ctx
|
|
822
|
+
.getState()
|
|
823
|
+
.templates.find((template) => template.id === action.templateId)?.eventId;
|
|
580
824
|
return handleApiRequest({
|
|
581
825
|
ctx,
|
|
582
|
-
key: NotificationActionKey.
|
|
583
|
-
request$:
|
|
584
|
-
onSuccess: (
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
return {};
|
|
826
|
+
key: NotificationActionKey.DeleteTemplate,
|
|
827
|
+
request$: this.api.deleteTemplate(action.templateId, action.expectedRevision),
|
|
828
|
+
onSuccess: (_, state) => {
|
|
829
|
+
const templates = state.templates.filter((template) => template.id !== action.templateId);
|
|
830
|
+
const next = templates.find((template) => template.eventId === state.selectedEventId);
|
|
588
831
|
return {
|
|
589
|
-
templates
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
832
|
+
templates,
|
|
833
|
+
selectedTemplateId: state.selectedTemplateId === action.templateId
|
|
834
|
+
? (next?.id ?? null)
|
|
835
|
+
: state.selectedTemplateId,
|
|
836
|
+
publications: [],
|
|
837
|
+
publicationsTemplateId: null,
|
|
594
838
|
};
|
|
595
839
|
},
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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),
|
|
602
859
|
}),
|
|
603
|
-
});
|
|
860
|
+
}).pipe(tap(() => ctx.dispatch(new GetPublications(action.templateId))));
|
|
861
|
+
}
|
|
862
|
+
setTemplateActivation(ctx, action) {
|
|
604
863
|
return handleApiRequest({
|
|
605
864
|
ctx,
|
|
606
|
-
key: NotificationActionKey.
|
|
607
|
-
request$:
|
|
608
|
-
onSuccess: (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return {};
|
|
612
|
-
// Preserve existing fields that API may not return or shouldn't change
|
|
613
|
-
const existingTemplate = state.templates.find((t) => t.id === action.templateId);
|
|
614
|
-
if (!existingTemplate)
|
|
615
|
-
return {};
|
|
616
|
-
// Only update name and content fields, preserve channel/eventId/receivers
|
|
617
|
-
const templates = this.adapter.upsertOne(state.templates, this.mergeUpdatedTemplate(existingTemplate, updatedTemplate), 'id');
|
|
618
|
-
return { templates };
|
|
619
|
-
},
|
|
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
|
+
}),
|
|
620
870
|
});
|
|
621
871
|
}
|
|
622
|
-
|
|
623
|
-
const req$ = this.http.delete(`${this.baseUrl}/templates/${action.templateId}`);
|
|
872
|
+
restorePublication(ctx, action) {
|
|
624
873
|
return handleApiRequest({
|
|
625
874
|
ctx,
|
|
626
|
-
key: NotificationActionKey.
|
|
627
|
-
request$:
|
|
628
|
-
onSuccess: (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const eventTemplates = templates.filter((t) => t.eventId === currentEvent?.id);
|
|
633
|
-
const systemTemplate = eventTemplates.find((t) => t.isSystem);
|
|
634
|
-
return {
|
|
635
|
-
templates,
|
|
636
|
-
selectedTemplateId: systemTemplate?.id ?? eventTemplates[0]?.id ?? null,
|
|
637
|
-
};
|
|
638
|
-
},
|
|
639
|
-
});
|
|
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))));
|
|
640
881
|
}
|
|
641
|
-
//
|
|
642
|
-
|
|
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))));
|
|
644
914
|
}
|
|
645
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState, deps:
|
|
915
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
646
916
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState });
|
|
647
917
|
};
|
|
648
918
|
__decorate([
|
|
@@ -652,47 +922,53 @@ __decorate([
|
|
|
652
922
|
Action(ResetNotificationState)
|
|
653
923
|
], NotificationState.prototype, "resetState", null);
|
|
654
924
|
__decorate([
|
|
655
|
-
Action(GetEvents)
|
|
925
|
+
Action(GetEvents, { cancelUncompleted: true })
|
|
656
926
|
], NotificationState.prototype, "getEvents", null);
|
|
657
927
|
__decorate([
|
|
658
|
-
Action(
|
|
659
|
-
], NotificationState.prototype, "
|
|
928
|
+
Action(GetAuthoringContext, { cancelUncompleted: true })
|
|
929
|
+
], NotificationState.prototype, "getAuthoringContext", null);
|
|
660
930
|
__decorate([
|
|
661
|
-
Action(
|
|
662
|
-
], NotificationState.prototype, "
|
|
931
|
+
Action(GetDefaultRecipes, { cancelUncompleted: true })
|
|
932
|
+
], NotificationState.prototype, "getDefaultRecipes", null);
|
|
663
933
|
__decorate([
|
|
664
|
-
Action(
|
|
665
|
-
], NotificationState.prototype, "
|
|
666
|
-
__decorate([
|
|
667
|
-
Action(GetActionReceivers)
|
|
668
|
-
], NotificationState.prototype, "getActionReceivers", null);
|
|
669
|
-
__decorate([
|
|
670
|
-
Action(GetActionProperties)
|
|
671
|
-
], NotificationState.prototype, "getActionProperties", null);
|
|
934
|
+
Action(GetPublications, { cancelUncompleted: true })
|
|
935
|
+
], NotificationState.prototype, "getPublications", null);
|
|
672
936
|
__decorate([
|
|
673
937
|
Action(SelectEvent)
|
|
674
938
|
], NotificationState.prototype, "selectEvent", null);
|
|
675
939
|
__decorate([
|
|
676
940
|
Action(SelectTemplate)
|
|
677
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);
|
|
678
948
|
__decorate([
|
|
679
949
|
Action(AddReceivers)
|
|
680
950
|
], NotificationState.prototype, "addReceivers", null);
|
|
681
951
|
__decorate([
|
|
682
952
|
Action(DeleteReceiver)
|
|
683
953
|
], NotificationState.prototype, "deleteReceiver", null);
|
|
684
|
-
__decorate([
|
|
685
|
-
Action(AddTemplate)
|
|
686
|
-
], NotificationState.prototype, "addTemplate", null);
|
|
687
|
-
__decorate([
|
|
688
|
-
Action(UpdateTemplate)
|
|
689
|
-
], NotificationState.prototype, "updateTemplate", null);
|
|
690
954
|
__decorate([
|
|
691
955
|
Action(DeleteTemplate)
|
|
692
956
|
], NotificationState.prototype, "deleteTemplate", null);
|
|
693
957
|
__decorate([
|
|
694
|
-
|
|
695
|
-
], 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);
|
|
696
972
|
__decorate([
|
|
697
973
|
Selector()
|
|
698
974
|
], NotificationState, "getEvents", null);
|
|
@@ -701,19 +977,19 @@ __decorate([
|
|
|
701
977
|
], NotificationState, "getTemplates", null);
|
|
702
978
|
__decorate([
|
|
703
979
|
Selector()
|
|
704
|
-
], NotificationState, "
|
|
980
|
+
], NotificationState, "getSelectedEventId", null);
|
|
705
981
|
__decorate([
|
|
706
982
|
Selector()
|
|
707
|
-
], NotificationState, "
|
|
983
|
+
], NotificationState, "getSelectedTemplateId", null);
|
|
708
984
|
__decorate([
|
|
709
985
|
Selector()
|
|
710
|
-
], NotificationState, "
|
|
986
|
+
], NotificationState, "getContext", null);
|
|
711
987
|
__decorate([
|
|
712
988
|
Selector()
|
|
713
|
-
], NotificationState, "
|
|
989
|
+
], NotificationState, "getDefaults", null);
|
|
714
990
|
__decorate([
|
|
715
991
|
Selector()
|
|
716
|
-
], NotificationState, "
|
|
992
|
+
], NotificationState, "getPublicationsSlice", null);
|
|
717
993
|
__decorate([
|
|
718
994
|
Selector()
|
|
719
995
|
], NotificationState, "getLoadingActive", null);
|
|
@@ -721,15 +997,12 @@ __decorate([
|
|
|
721
997
|
Selector()
|
|
722
998
|
], NotificationState, "getErrors", null);
|
|
723
999
|
__decorate([
|
|
724
|
-
Selector([
|
|
725
|
-
NotificationState.getEvents,
|
|
726
|
-
NotificationState.getSelectedEventName,
|
|
727
|
-
])
|
|
1000
|
+
Selector([NotificationState.getEvents, NotificationState.getSelectedEventId])
|
|
728
1001
|
], NotificationState, "getSelectedEvent", null);
|
|
729
1002
|
__decorate([
|
|
730
1003
|
Selector([
|
|
731
1004
|
NotificationState.getTemplates,
|
|
732
|
-
NotificationState.
|
|
1005
|
+
NotificationState.getSelectedEventId,
|
|
733
1006
|
])
|
|
734
1007
|
], NotificationState, "getEventTemplates", null);
|
|
735
1008
|
__decorate([
|
|
@@ -740,16 +1013,10 @@ __decorate([
|
|
|
740
1013
|
], NotificationState, "getSelectedTemplate", null);
|
|
741
1014
|
__decorate([
|
|
742
1015
|
Selector([
|
|
743
|
-
NotificationState.
|
|
744
|
-
NotificationState.
|
|
745
|
-
])
|
|
746
|
-
], NotificationState, "getAvailableReceivers", null);
|
|
747
|
-
__decorate([
|
|
748
|
-
Selector([
|
|
749
|
-
NotificationState.getReceiverGroups,
|
|
750
|
-
NotificationState.getSelectedTemplate,
|
|
1016
|
+
NotificationState.getPublicationsSlice,
|
|
1017
|
+
NotificationState.getSelectedTemplateId,
|
|
751
1018
|
])
|
|
752
|
-
], NotificationState, "
|
|
1019
|
+
], NotificationState, "getPublications", null);
|
|
753
1020
|
NotificationState = __decorate([
|
|
754
1021
|
State({
|
|
755
1022
|
name: 'notification',
|
|
@@ -758,48 +1025,71 @@ NotificationState = __decorate([
|
|
|
758
1025
|
], NotificationState);
|
|
759
1026
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationState, decorators: [{
|
|
760
1027
|
type: Injectable
|
|
761
|
-
}], 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
|
+
}
|
|
762
1055
|
|
|
763
1056
|
class NotificationFacade {
|
|
764
1057
|
store = inject(Store);
|
|
765
|
-
|
|
766
|
-
//
|
|
767
|
-
//
|
|
1058
|
+
api = inject(NotificationTemplateApi);
|
|
1059
|
+
// ==========================================================================
|
|
1060
|
+
// Data
|
|
1061
|
+
// ==========================================================================
|
|
768
1062
|
events = select(NotificationState.getEvents);
|
|
1063
|
+
/** Templates of the selected event. */
|
|
769
1064
|
templates = select(NotificationState.getEventTemplates);
|
|
770
|
-
|
|
771
|
-
receiverGroups = select(NotificationState.getReceiverGroups);
|
|
772
|
-
propertyGroups = select(NotificationState.getPropertyGroups);
|
|
773
|
-
// Selection selectors
|
|
774
|
-
selectedEventName = select(NotificationState.getSelectedEventName);
|
|
1065
|
+
selectedEventId = select(NotificationState.getSelectedEventId);
|
|
775
1066
|
selectedTemplateId = select(NotificationState.getSelectedTemplateId);
|
|
776
1067
|
selectedEvent = select(NotificationState.getSelectedEvent);
|
|
777
1068
|
selectedTemplate = select(NotificationState.getSelectedTemplate);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
//
|
|
1069
|
+
context = select(NotificationState.getContext);
|
|
1070
|
+
defaults = select(NotificationState.getDefaults);
|
|
1071
|
+
publications = select(NotificationState.getPublications);
|
|
1072
|
+
// ==========================================================================
|
|
1073
|
+
// Loading / errors
|
|
1074
|
+
// ==========================================================================
|
|
783
1075
|
loadingActive = select(NotificationState.getLoadingActive);
|
|
784
1076
|
errors = select(NotificationState.getErrors);
|
|
785
|
-
// ============================================================================
|
|
786
|
-
// Loading Signals - Computed from slice (minimal reactivity)
|
|
787
|
-
// ============================================================================
|
|
788
1077
|
isLoadingEvents = computed(() => this.loadingActive().includes(NotificationActionKey.GetEvents), ...(ngDevMode ? [{ debugName: "isLoadingEvents" }] : /* istanbul ignore next */ []));
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
isUpdatingTemplate = computed(() => this.loadingActive().includes(NotificationActionKey.UpdateTemplate), ...(ngDevMode ? [{ debugName: "isUpdatingTemplate" }] : /* istanbul ignore next */ []));
|
|
795
|
-
isDeletingTemplate = computed(() => this.loadingActive().includes(NotificationActionKey.DeleteTemplate), ...(ngDevMode ? [{ debugName: "isDeletingTemplate" }] : /* istanbul ignore next */ []));
|
|
796
|
-
// ============================================================================
|
|
797
|
-
// Error Signals - Computed from slice (minimal reactivity)
|
|
798
|
-
// ============================================================================
|
|
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 */ []));
|
|
799
1083
|
eventsError = computed(() => this.errors()[NotificationActionKey.GetEvents] ?? null, ...(ngDevMode ? [{ debugName: "eventsError" }] : /* istanbul ignore next */ []));
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
// ==========================================================================
|
|
803
1093
|
/**
|
|
804
1094
|
* Scopes this (shared, singleton) state to a module/event context.
|
|
805
1095
|
*
|
|
@@ -817,68 +1107,70 @@ class NotificationFacade {
|
|
|
817
1107
|
resetState() {
|
|
818
1108
|
return this.store.dispatch(new ResetNotificationState());
|
|
819
1109
|
}
|
|
820
|
-
|
|
1110
|
+
/** Loads the scope's events; the selected event's catalog and defaults follow. */
|
|
821
1111
|
loadEvents() {
|
|
822
1112
|
return this.store.dispatch(new GetEvents());
|
|
823
1113
|
}
|
|
824
|
-
|
|
825
|
-
|
|
1114
|
+
/** Kept for existing hosts — both load the scope's events and what follows from them. */
|
|
1115
|
+
loadBaseData() {
|
|
1116
|
+
return this.loadEvents();
|
|
826
1117
|
}
|
|
827
|
-
|
|
828
|
-
return this.
|
|
1118
|
+
loadAllData() {
|
|
1119
|
+
return this.loadEvents();
|
|
829
1120
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
return this.store.dispatch(new GetProperties(isMultiLang));
|
|
1121
|
+
// ==========================================================================
|
|
1122
|
+
// Selection
|
|
1123
|
+
// ==========================================================================
|
|
1124
|
+
selectEvent(eventId) {
|
|
1125
|
+
return this.store.dispatch(new SelectEvent(eventId));
|
|
836
1126
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
return this.store.dispatch(new SetActionMode(actionId));
|
|
1127
|
+
selectTemplate(templateId) {
|
|
1128
|
+
return this.store.dispatch(new SelectTemplate(templateId));
|
|
840
1129
|
}
|
|
841
|
-
|
|
842
|
-
return this.store.dispatch(new
|
|
1130
|
+
reloadDefaults(eventId) {
|
|
1131
|
+
return this.store.dispatch(new GetDefaultRecipes(eventId));
|
|
843
1132
|
}
|
|
844
|
-
|
|
845
|
-
|
|
1133
|
+
// ==========================================================================
|
|
1134
|
+
// Writes
|
|
1135
|
+
// ==========================================================================
|
|
1136
|
+
createTemplate(eventId, payload) {
|
|
1137
|
+
return this.store.dispatch(new CreateTemplate(eventId, payload));
|
|
846
1138
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
return this.store.dispatch(new SelectEvent(eventName));
|
|
1139
|
+
saveTemplate(templateId, payload) {
|
|
1140
|
+
return this.store.dispatch(new SaveTemplate(templateId, payload));
|
|
850
1141
|
}
|
|
851
|
-
|
|
852
|
-
return this.store.dispatch(new
|
|
1142
|
+
deleteTemplate(templateId, expectedRevision) {
|
|
1143
|
+
return this.store.dispatch(new DeleteTemplate(templateId, expectedRevision));
|
|
853
1144
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
return this.store.dispatch(new AddReceivers(templateId, payload));
|
|
1145
|
+
addReceivers(templateId, expectedRevision, receivers) {
|
|
1146
|
+
return this.store.dispatch(new AddReceivers(templateId, expectedRevision, receivers));
|
|
857
1147
|
}
|
|
858
|
-
deleteReceiver(receiverId) {
|
|
859
|
-
return this.store.dispatch(new DeleteReceiver(receiverId));
|
|
1148
|
+
deleteReceiver(receiverId, expectedRevision) {
|
|
1149
|
+
return this.store.dispatch(new DeleteReceiver(receiverId, expectedRevision));
|
|
860
1150
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return this.store.dispatch(new AddTemplate(eventId, payload));
|
|
1151
|
+
publish(templateId, expectedRevision, reviews) {
|
|
1152
|
+
return this.store.dispatch(new PublishTemplate(templateId, expectedRevision, reviews));
|
|
864
1153
|
}
|
|
865
|
-
|
|
866
|
-
return this.store.dispatch(new
|
|
1154
|
+
turnOn(templateId, expectedRevision, reviews) {
|
|
1155
|
+
return this.store.dispatch(new SetTemplateActivation(templateId, expectedRevision, true, reviews));
|
|
867
1156
|
}
|
|
868
|
-
|
|
869
|
-
return this.store.dispatch(new
|
|
1157
|
+
turnOff(templateId, expectedRevision) {
|
|
1158
|
+
return this.store.dispatch(new SetTemplateActivation(templateId, expectedRevision, false, null));
|
|
870
1159
|
}
|
|
871
|
-
|
|
872
|
-
this.
|
|
873
|
-
this.loadReceivers();
|
|
1160
|
+
restorePublication(templateId, expectedRevision, publicationRevision, reviews) {
|
|
1161
|
+
return this.store.dispatch(new RestorePublication(templateId, expectedRevision, publicationRevision, reviews));
|
|
874
1162
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
this.
|
|
880
|
-
|
|
881
|
-
|
|
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);
|
|
882
1174
|
}
|
|
883
1175
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
884
1176
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationFacade, providedIn: 'root' });
|
|
@@ -888,405 +1180,1214 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
888
1180
|
args: [{ providedIn: 'root' }]
|
|
889
1181
|
}] });
|
|
890
1182
|
|
|
891
|
-
|
|
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);
|
|
892
1433
|
facade = inject(NotificationFacade);
|
|
1434
|
+
destroyRef = inject(DestroyRef);
|
|
893
1435
|
transloco = inject(TranslocoService);
|
|
894
|
-
|
|
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 {
|
|
895
1507
|
modalService = inject(ModalService);
|
|
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);
|
|
896
1563
|
toast = inject(ToastService);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1564
|
+
destroyRef = inject(DestroyRef);
|
|
1565
|
+
lang = toSignal(this.transloco.langChanges$, {
|
|
1566
|
+
initialValue: this.transloco.getActiveLang(),
|
|
1567
|
+
});
|
|
1568
|
+
// ==========================================================================
|
|
1569
|
+
// Store
|
|
1570
|
+
// ==========================================================================
|
|
900
1571
|
events = this.facade.events;
|
|
901
1572
|
templates = this.facade.templates;
|
|
902
|
-
receivers = this.facade.receivers;
|
|
903
|
-
propertyGroups = this.facade.propertyGroups;
|
|
904
|
-
// Selection Signals
|
|
905
|
-
selectedEventName = this.facade.selectedEventName;
|
|
906
|
-
selectedTemplateId = this.facade.selectedTemplateId;
|
|
907
1573
|
selectedEvent = this.facade.selectedEvent;
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
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 }),
|
|
939
1604
|
});
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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)
|
|
974
1644
|
return false;
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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],
|
|
988
1678
|
})), ...(ngDevMode ? [{ debugName: "templateTabs" }] : /* istanbul ignore next */ []));
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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 */ []));
|
|
1000
1759
|
constructor() {
|
|
1760
|
+
this.form.valueChanges
|
|
1761
|
+
.pipe(takeUntilDestroyed())
|
|
1762
|
+
.subscribe(() => this.formValue.set(this.form.getRawValue()));
|
|
1001
1763
|
effect(() => {
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
if (this.
|
|
1005
|
-
this.
|
|
1006
|
-
emitEvent: false,
|
|
1007
|
-
});
|
|
1764
|
+
const eventId = this.selectedEventId();
|
|
1765
|
+
untracked(() => {
|
|
1766
|
+
if (this.eventControl.value !== eventId) {
|
|
1767
|
+
this.eventControl.setValue(eventId, { emitEvent: false });
|
|
1008
1768
|
}
|
|
1009
|
-
}
|
|
1010
|
-
else {
|
|
1011
|
-
this.form.controls['event'].setValue(null, { emitEvent: false });
|
|
1012
|
-
}
|
|
1769
|
+
});
|
|
1013
1770
|
});
|
|
1771
|
+
// A newly selected template replaces the editor; a write on the same
|
|
1772
|
+
// template (recipients, publish, activation) never touches unsaved edits.
|
|
1014
1773
|
effect(() => {
|
|
1015
|
-
this.selectedTemplateId();
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
this.
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
+
});
|
|
1023
1792
|
});
|
|
1024
1793
|
effect(() => {
|
|
1025
|
-
const
|
|
1026
|
-
|
|
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) {
|
|
1027
1803
|
return;
|
|
1028
1804
|
}
|
|
1029
|
-
|
|
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
|
+
});
|
|
1030
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
|
+
}
|
|
1031
1827
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
+
});
|
|
1038
1853
|
}
|
|
1039
1854
|
onTemplateTabChange(templateId) {
|
|
1040
|
-
this.
|
|
1041
|
-
|
|
1042
|
-
// Channel is fixed per template - set on creation only
|
|
1043
|
-
onAddProperty(propertyKey) {
|
|
1044
|
-
if (!propertyKey)
|
|
1855
|
+
const current = this.selectedTemplateId();
|
|
1856
|
+
if (templateId === current)
|
|
1045
1857
|
return;
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
enCtrl?.setValue(`${enCtrl.value ?? ''} ${tag}`);
|
|
1054
|
-
arCtrl?.setValue(`${arCtrl.value ?? ''} ${tag}`);
|
|
1858
|
+
this.activeTab.set(templateId);
|
|
1859
|
+
this.confirmDiscard().subscribe((discard) => {
|
|
1860
|
+
if (discard) {
|
|
1861
|
+
this.facade.selectTemplate(templateId);
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
this.activeTab.set(current);
|
|
1055
1865
|
}
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
retryLoad() {
|
|
1869
|
+
const eventId = this.selectedEventId();
|
|
1870
|
+
if (this.facade.eventsError() || eventId == null) {
|
|
1871
|
+
this.facade.loadEvents();
|
|
1872
|
+
return;
|
|
1056
1873
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
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);
|
|
1899
|
+
return;
|
|
1059
1900
|
}
|
|
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.
|
|
1060
1911
|
setTimeout(() => {
|
|
1061
|
-
|
|
1062
|
-
|
|
1912
|
+
textarea?.focus();
|
|
1913
|
+
textarea?.setSelectionRange(caret, caret);
|
|
1914
|
+
});
|
|
1063
1915
|
}
|
|
1064
|
-
|
|
1065
|
-
const template = this.
|
|
1066
|
-
if (!
|
|
1067
|
-
|
|
1916
|
+
onSave() {
|
|
1917
|
+
const template = this.template();
|
|
1918
|
+
if (!template || !this.canSave()) {
|
|
1919
|
+
this.form.markAllAsTouched();
|
|
1068
1920
|
return;
|
|
1069
1921
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1922
|
+
this.facade
|
|
1923
|
+
.saveTemplate(template.id, this.buildSavePayload(template))
|
|
1924
|
+
.subscribe({
|
|
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),
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
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);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
// ==========================================================================
|
|
1944
|
+
// Recipients
|
|
1945
|
+
// ==========================================================================
|
|
1946
|
+
onAddRecipient(key) {
|
|
1947
|
+
const template = this.template();
|
|
1948
|
+
setTimeout(() => this.recipientControl.reset());
|
|
1949
|
+
if (!key || !template || this.isMutating())
|
|
1072
1950
|
return;
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1951
|
+
const option = this.recipientGroups()
|
|
1952
|
+
.flatMap((group) => group.items)
|
|
1953
|
+
.find((item) => item.key === key);
|
|
1954
|
+
if (!option)
|
|
1077
1955
|
return;
|
|
1078
|
-
}
|
|
1079
|
-
this.addingReceiverIds.update((ids) => [...ids, receiverId]);
|
|
1080
1956
|
this.facade
|
|
1081
|
-
.addReceivers(template.id,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
id: selectedReceiver.id,
|
|
1085
|
-
type: selectedReceiver.type,
|
|
1086
|
-
},
|
|
1087
|
-
],
|
|
1088
|
-
})
|
|
1957
|
+
.addReceivers(template.id, template.revision, [
|
|
1958
|
+
{ type: option.type, id: option.id },
|
|
1959
|
+
])
|
|
1089
1960
|
.subscribe({
|
|
1090
|
-
|
|
1091
|
-
error: () => this.
|
|
1961
|
+
next: () => this.afterWrite(null),
|
|
1962
|
+
error: (error) => this.onWriteError(error),
|
|
1092
1963
|
});
|
|
1093
|
-
setTimeout(() => this.receiverPickerControl.reset(), 0);
|
|
1094
1964
|
}
|
|
1095
|
-
|
|
1096
|
-
this.
|
|
1965
|
+
onRemoveRecipient(receiver) {
|
|
1966
|
+
const template = this.template();
|
|
1967
|
+
if (!template || receiver.isSystem || this.isMutating())
|
|
1968
|
+
return;
|
|
1969
|
+
this.confirmation.confirmDelete({
|
|
1097
1970
|
type: 'dialog',
|
|
1098
|
-
|
|
1099
|
-
this.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
+
});
|
|
1979
|
+
}
|
|
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())
|
|
2033
|
+
return;
|
|
2034
|
+
const revision = template.revision;
|
|
2035
|
+
this.confirmation.confirm({
|
|
2036
|
+
type: 'dialog',
|
|
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'),
|
|
2041
|
+
acceptButton: { severity: 'danger' },
|
|
2042
|
+
accept: () => this.facade.turnOff(template.id, revision).subscribe({
|
|
2043
|
+
next: () => this.afterWrite('turned-off'),
|
|
2044
|
+
error: (error) => this.onWriteError(error),
|
|
2045
|
+
}),
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
onOpenHistory() {
|
|
2049
|
+
const eventId = this.selectedEventId();
|
|
2050
|
+
if (eventId == null)
|
|
2051
|
+
return;
|
|
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,
|
|
1108
2062
|
},
|
|
1109
2063
|
});
|
|
2064
|
+
ref.onClose.subscribe((result) => {
|
|
2065
|
+
if (result?.publication)
|
|
2066
|
+
this.restoreVersion(result.publication);
|
|
2067
|
+
});
|
|
1110
2068
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
+
}));
|
|
1113
2088
|
}
|
|
1114
|
-
|
|
1115
|
-
const template = this.
|
|
1116
|
-
if (!template || this.
|
|
1117
|
-
this.form.markAllAsTouched();
|
|
2089
|
+
onDelete() {
|
|
2090
|
+
const template = this.template();
|
|
2091
|
+
if (!template || template.isSystem || this.isMutating())
|
|
1118
2092
|
return;
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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,
|
|
2123
|
+
name: result.name,
|
|
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),
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
1124
2136
|
});
|
|
1125
2137
|
}
|
|
1126
2138
|
onTestEmail() {
|
|
1127
2139
|
const event = this.selectedEvent();
|
|
1128
|
-
const template = this.
|
|
1129
|
-
if (!event || !template ||
|
|
2140
|
+
const template = this.template();
|
|
2141
|
+
if (!event || !template || template.channel !== 'Email')
|
|
1130
2142
|
return;
|
|
1131
|
-
if (this.
|
|
1132
|
-
this.toast.warn(this.
|
|
2143
|
+
if (this.dirty()) {
|
|
2144
|
+
this.toast.warn(this.messages.t('saveBeforeEmailTest'));
|
|
1133
2145
|
return;
|
|
1134
2146
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return;
|
|
1139
|
-
this.modalService.openModal(EmailUserTestDialog, 'dialog', {
|
|
1140
|
-
header: this.transloco.translate('notification.testEmail'),
|
|
2147
|
+
// Sends the saved draft — never the editor's unsaved values.
|
|
2148
|
+
this.modal.openModal(EmailUserTestDialog, 'dialog', {
|
|
2149
|
+
header: this.messages.t('testEmail'),
|
|
1141
2150
|
styleClass: '!w-[96vw] sm:!w-[38rem]',
|
|
1142
2151
|
inputValues: {
|
|
1143
2152
|
kind: 'template',
|
|
1144
|
-
eventId,
|
|
1145
|
-
templateId,
|
|
1146
|
-
sourceLabel: `${event.name} — ${template.name}`,
|
|
1147
|
-
|
|
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()),
|
|
1148
2163
|
},
|
|
1149
2164
|
});
|
|
1150
2165
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
2166
|
+
// ==========================================================================
|
|
2167
|
+
// Defaults
|
|
2168
|
+
// ==========================================================================
|
|
2169
|
+
onInitializeDefaults() {
|
|
2170
|
+
const eventId = this.selectedEventId();
|
|
2171
|
+
if (eventId == null || this.isMutating())
|
|
1154
2172
|
return;
|
|
1155
|
-
this.
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
accept: () => this.facade.deleteTemplate(template.id),
|
|
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),
|
|
1161
2178
|
});
|
|
1162
2179
|
}
|
|
1163
|
-
|
|
1164
|
-
const
|
|
1165
|
-
if (
|
|
2180
|
+
onRestoreDefault(channel) {
|
|
2181
|
+
const eventId = this.selectedEventId();
|
|
2182
|
+
if (eventId == null || this.isMutating())
|
|
1166
2183
|
return;
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
+
}));
|
|
1178
2208
|
});
|
|
1179
2209
|
}
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
2210
|
+
// ==========================================================================
|
|
2211
|
+
// Conflicts and errors
|
|
2212
|
+
// ==========================================================================
|
|
1183
2213
|
/**
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
* effect (or the user's own pick) put in that control. An event with no
|
|
1188
|
-
* templates yet — a brand-new one, or one whose last template was just
|
|
1189
|
-
* deleted — resolves `selectedTemplate` to null, and this effect runs after
|
|
1190
|
-
* the event effect in the same flush: resetting the whole group there would
|
|
1191
|
-
* blank the event picker the user just used, while `selectedEvent()` stays
|
|
1192
|
-
* truthy and keeps the tabs/add-template row visible. Leaving the control
|
|
1193
|
-
* alone also keeps the two effects ordering-independent — on a full
|
|
1194
|
-
* `ResetNotificationState` the event effect has already nulled it, and that
|
|
1195
|
-
* null is carried through.
|
|
1196
|
-
*
|
|
1197
|
-
* `channel` is restored through {@link channelControl} with events enabled
|
|
1198
|
-
* because `channelSignal` is a `toSignal` of its `valueChanges` and is the
|
|
1199
|
-
* only consumer of them on this form — a silent reset would leave
|
|
1200
|
-
* `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.
|
|
1201
2217
|
*/
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
appendToControl(key, value) {
|
|
1210
|
-
const ctrl = this.form.controls[key];
|
|
1211
|
-
ctrl?.setValue(`${ctrl.value ?? ''} ${value}`);
|
|
1212
|
-
}
|
|
1213
|
-
patchForm(template) {
|
|
1214
|
-
const channel = template.channel || 'email';
|
|
1215
|
-
this.form.patchValue({
|
|
1216
|
-
name: template.name,
|
|
1217
|
-
channel: channel,
|
|
1218
|
-
});
|
|
1219
|
-
// Patch all channel controls dynamically
|
|
1220
|
-
const t = template;
|
|
1221
|
-
for (const ch of CHANNEL_CONFIGS) {
|
|
1222
|
-
if (ch.templateKey) {
|
|
1223
|
-
this.form
|
|
1224
|
-
.get(ch.templateKey)
|
|
1225
|
-
?.setValue(t[ch.templateKey] ?? '');
|
|
1226
|
-
}
|
|
1227
|
-
if (ch.translatableKey) {
|
|
1228
|
-
const trans = t[ch.translatableKey] ?? {};
|
|
1229
|
-
this.form.get(ch.translatableKey)?.patchValue({
|
|
1230
|
-
en: trans.en ?? '',
|
|
1231
|
-
ar: trans.ar ?? '',
|
|
1232
|
-
});
|
|
2218
|
+
resolveConflict(keepMine) {
|
|
2219
|
+
this.facade.loadEvents().subscribe(() => {
|
|
2220
|
+
if (!keepMine) {
|
|
2221
|
+
const template = this.template();
|
|
2222
|
+
if (template)
|
|
2223
|
+
this.patchFromTemplate(template);
|
|
1233
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;
|
|
1234
2241
|
}
|
|
2242
|
+
if (toastKey)
|
|
2243
|
+
this.toast.success(this.messages.t(`toast.${toastKey}`));
|
|
1235
2244
|
}
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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();
|
|
1251
2262
|
}
|
|
1252
|
-
return payload;
|
|
1253
2263
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
+
});
|
|
1257
2312
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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());
|
|
2324
|
+
}
|
|
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();
|
|
1261
2329
|
return {
|
|
1262
|
-
|
|
1263
|
-
|
|
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,
|
|
1264
2339
|
};
|
|
1265
2340
|
}
|
|
1266
2341
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplate, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1267
|
-
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 (form.controls[\"channel\"].value === \"email\") {\r\n <mt-button\r\n icon=\"communication.send-01\"\r\n [label]=\"'notification.testEmail' | transloco\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [disabled]=\"\r\n form.dirty || isUpdatingTemplate() || isDeletingTemplate()\r\n \"\r\n (onClick)=\"onTestEmail()\"\r\n />\r\n }\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 });
|
|
1268
2343
|
}
|
|
1269
2344
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NotificationTemplate, decorators: [{
|
|
1270
2345
|
type: Component,
|
|
1271
|
-
args: [{ selector: 'mt-notification-template',
|
|
1272
|
-
CommonModule,
|
|
2346
|
+
args: [{ selector: 'mt-notification-template', imports: [
|
|
1273
2347
|
ReactiveFormsModule,
|
|
1274
|
-
|
|
1275
|
-
Card,
|
|
2348
|
+
TranslocoDirective,
|
|
1276
2349
|
Button,
|
|
2350
|
+
EmptyState,
|
|
1277
2351
|
SelectField,
|
|
2352
|
+
Tabs,
|
|
1278
2353
|
TextField,
|
|
1279
2354
|
TextareaField,
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2355
|
+
Tooltip,
|
|
2356
|
+
FormulaBuilder,
|
|
2357
|
+
EmailVisualEditor,
|
|
1283
2358
|
Icon,
|
|
1284
2359
|
Skeleton,
|
|
1285
|
-
], 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 (form.controls[\"channel\"].value === \"email\") {\r\n <mt-button\r\n icon=\"communication.send-01\"\r\n [label]=\"'notification.testEmail' | transloco\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [disabled]=\"\r\n form.dirty || isUpdatingTemplate() || isDeletingTemplate()\r\n \"\r\n (onClick)=\"onTestEmail()\"\r\n />\r\n }\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"] }]
|
|
1286
|
-
}], ctorParameters: () => [] });
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
+
};
|
|
1290
2391
|
}
|
|
1291
2392
|
|
|
1292
2393
|
const APP_STATES = [NotificationState];
|
|
@@ -1295,5 +2396,5 @@ const APP_STATES = [NotificationState];
|
|
|
1295
2396
|
* Generated bundle index. Do not edit.
|
|
1296
2397
|
*/
|
|
1297
2398
|
|
|
1298
|
-
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 };
|
|
1299
2400
|
//# sourceMappingURL=masterteam-notification.mjs.map
|