@masterteam/system-settings 0.0.12 → 0.0.13

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.
@@ -0,0 +1,2501 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, signal, computed, input, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
3
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { ActivatedRoute, Router } from '@angular/router';
5
+ import { TranslocoService, TranslocoDirective } from '@jsverse/transloco';
6
+ import { EmptyState } from '@masterteam/components/empty-state';
7
+ import { Page } from '@masterteam/components/page';
8
+ import { ToastService } from '@masterteam/components/toast';
9
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
10
+ import { mergeMap, of, throwError, catchError, map, switchMap, forkJoin, tap, finalize, take, timer } from 'rxjs';
11
+ import { EMAIL_DELIVERY_OPTIONS, EMAIL_DELIVERY_ATTEMPT_STATUSES, EMAIL_DELIVERY_PERMISSIONS } from './masterteam-system-settings.mjs';
12
+ import { Icon } from '@masterteam/icons';
13
+ import * as i1 from '@angular/forms';
14
+ import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
15
+ import { Button } from '@masterteam/components/button';
16
+ import { ModalService } from '@masterteam/components/modal';
17
+ import { SelectField } from '@masterteam/components/select-field';
18
+ import { Table } from '@masterteam/components/table';
19
+ import { EntityStatus } from '@masterteam/components/entities';
20
+ import { DatePipe, DOCUMENT } from '@angular/common';
21
+ import { Card } from '@masterteam/components/card';
22
+ import { ModalRef } from '@masterteam/components/dialog';
23
+ import { TextareaField } from '@masterteam/components/textarea-field';
24
+ import { ToggleField } from '@masterteam/components/toggle-field';
25
+ import { ConfirmationService } from '@masterteam/components/confirmation';
26
+ import { TextareaFieldConfig, TextFieldConfig, ToggleFieldConfig, ValidatorConfig, SelectFieldConfig, RadioButtonFieldConfig, NumberFieldConfig } from '@masterteam/components';
27
+ import { DynamicForm } from '@masterteam/forms/dynamic-form';
28
+ import { Menu } from '@masterteam/components/menu';
29
+
30
+ class EmailDeliveryApi {
31
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
32
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi });
33
+ }
34
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, decorators: [{
35
+ type: Injectable
36
+ }] });
37
+
38
+ class EmailDeliveryHttpApi extends EmailDeliveryApi {
39
+ http = inject(HttpClient);
40
+ options = inject(EMAIL_DELIVERY_OPTIONS);
41
+ getStatus() {
42
+ return this.http.get(this.url('status'));
43
+ }
44
+ getProviders() {
45
+ return this.http.get(this.url('providers'));
46
+ }
47
+ getErrorCatalog() {
48
+ return this.http.get(this.url('error-catalog'));
49
+ }
50
+ getConnections() {
51
+ return this.http.get(this.url('connections'));
52
+ }
53
+ getConnection(id) {
54
+ return this.http.get(this.url(`connections/${encodeURIComponent(id)}`));
55
+ }
56
+ createConnection(request) {
57
+ return this.http.post(this.url('connections'), request);
58
+ }
59
+ updateConnection(id, request) {
60
+ return this.http.put(this.url(`connections/${encodeURIComponent(id)}`), request);
61
+ }
62
+ deleteConnection(id, concurrencyToken) {
63
+ return this.http.delete(this.url(`connections/${encodeURIComponent(id)}`), { params: { concurrencyToken } });
64
+ }
65
+ bindConnection(purpose, connectionId) {
66
+ return this.http.put(this.url(`bindings/${encodeURIComponent(purpose)}`), { connectionId });
67
+ }
68
+ unbindConnection(purpose) {
69
+ return this.http.delete(this.url(`bindings/${encodeURIComponent(purpose)}`));
70
+ }
71
+ validateConnection(id) {
72
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/validate`), null);
73
+ }
74
+ sendConnectionTest(id, request) {
75
+ return this.http
76
+ .post(this.url(`connections/${encodeURIComponent(id)}/test`), request)
77
+ .pipe(mergeMap((response) => isEmailSendResult(response)
78
+ ? of(response)
79
+ : throwError(() => response)), catchError((error) => {
80
+ const body = error instanceof HttpErrorResponse ? error.error : error;
81
+ return isEmailSendResult(body) ? of(body) : throwError(() => error);
82
+ }));
83
+ }
84
+ getConnectionTest(connectionId, testRequestId) {
85
+ return this.http.get(this.url(`connections/${encodeURIComponent(connectionId)}/test-requests/${encodeURIComponent(testRequestId)}`));
86
+ }
87
+ startDelegatedAuthorization(id) {
88
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/delegated/start`), null);
89
+ }
90
+ startAdminConsent(id) {
91
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/admin-consent/start`), null);
92
+ }
93
+ revokeAuthorization(id) {
94
+ return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/revoke`), null);
95
+ }
96
+ getAttempts(status, cursor, take) {
97
+ return this.http.get(this.url('attempts'), {
98
+ params: withoutNulls({ status, cursor, take }),
99
+ });
100
+ }
101
+ replayAttempt(id, request) {
102
+ return this.http.post(this.url(`attempts/${encodeURIComponent(id)}/replay`), request);
103
+ }
104
+ getTestRequest(testRequestId) {
105
+ return this.http.get(this.url(`test-requests/${encodeURIComponent(testRequestId)}`));
106
+ }
107
+ sendTemplateTest(eventId, templateId, request) {
108
+ return this.http
109
+ .post(this.notificationUrl(`events/${eventId}/templates/${templateId}/test-email`), request)
110
+ .pipe(map((response) => response.data));
111
+ }
112
+ sendAlertTest(alertId, request) {
113
+ return this.http
114
+ .post(this.notificationUrl(`alerts/${alertId}/test`), request)
115
+ .pipe(map((response) => response.data));
116
+ }
117
+ getFinalizationQuarantine(cursor, take) {
118
+ return this.http.get(this.url('finalization-quarantine'), { params: withoutNulls({ cursor, take }) });
119
+ }
120
+ retryFinalizationQuarantine(itemId, reason) {
121
+ return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/retry`), { reason });
122
+ }
123
+ acknowledgeFinalizationQuarantine(itemId, reason) {
124
+ return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/acknowledge`), { reason });
125
+ }
126
+ getHostEvidence(cursor, take) {
127
+ return this.http.get(this.url('orphan-evidence'), {
128
+ params: withoutNulls({ cursor, take }),
129
+ });
130
+ }
131
+ exportHostEvidence(evidenceOccurrenceId) {
132
+ return this.http
133
+ .get(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/export`), {
134
+ observe: 'response',
135
+ responseType: 'blob',
136
+ })
137
+ .pipe(map((response) => toEvidenceDownload(response)));
138
+ }
139
+ repairHostEvidence(evidenceOccurrenceId, request) {
140
+ return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/repair`), request);
141
+ }
142
+ acknowledgeHostEvidence(evidenceOccurrenceId, request) {
143
+ return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/acknowledge`), request);
144
+ }
145
+ rebuildEvidenceIndex(take) {
146
+ return this.http.post(this.url('orphan-evidence/rebuild-index'), null, { params: { take } });
147
+ }
148
+ url(path) {
149
+ return joinPath(this.options.apiBasePath, path);
150
+ }
151
+ notificationUrl(path) {
152
+ return joinPath(this.options.notificationApiBasePath, path);
153
+ }
154
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
155
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, providedIn: 'root' });
156
+ }
157
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, decorators: [{
158
+ type: Injectable,
159
+ args: [{ providedIn: 'root' }]
160
+ }] });
161
+ function isEmailSendResult(value) {
162
+ return (typeof value === 'object' &&
163
+ value !== null &&
164
+ typeof value.outcome === 'string' &&
165
+ typeof value.code === 'string');
166
+ }
167
+ function withoutNulls(values) {
168
+ return Object.fromEntries(Object.entries(values).filter((entry) => entry[1] !== null));
169
+ }
170
+ function joinPath(base, path) {
171
+ return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
172
+ }
173
+ function toEvidenceDownload(response) {
174
+ const contentDisposition = response.headers.get('content-disposition') ?? '';
175
+ const fileNameMatch = /filename\*?=(?:UTF-8''|\")?([^\";]+)/i.exec(contentDisposition);
176
+ return {
177
+ blob: response.body ?? new Blob(),
178
+ fileName: fileNameMatch
179
+ ? decodeURIComponent(fileNameMatch[1].replace(/\"$/, ''))
180
+ : 'email-delivery-evidence.json',
181
+ contentType: response.headers.get('content-type') ?? 'application/octet-stream',
182
+ };
183
+ }
184
+
185
+ const PENDING_AUTHORIZATION_STORAGE_KEY = 'mt.email-delivery.pending-authorization-connection';
186
+ class EmailDeliveryFacade {
187
+ options = inject(EMAIL_DELIVERY_OPTIONS);
188
+ api = inject(EmailDeliveryHttpApi);
189
+ transloco = inject(TranslocoService);
190
+ toast = inject(ToastService);
191
+ initialized = false;
192
+ status = signal(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
193
+ providers = signal([], ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
194
+ errorCatalog = signal([], ...(ngDevMode ? [{ debugName: "errorCatalog" }] : /* istanbul ignore next */ []));
195
+ connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : /* istanbul ignore next */ []));
196
+ selectedConnection = signal(null, ...(ngDevMode ? [{ debugName: "selectedConnection" }] : /* istanbul ignore next */ []));
197
+ attempts = signal([], ...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
198
+ attemptsNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "attemptsNextCursor" }] : /* istanbul ignore next */ []));
199
+ attemptStatusFilter = signal(null, ...(ngDevMode ? [{ debugName: "attemptStatusFilter" }] : /* istanbul ignore next */ []));
200
+ quarantine = signal([], ...(ngDevMode ? [{ debugName: "quarantine" }] : /* istanbul ignore next */ []));
201
+ quarantineNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "quarantineNextCursor" }] : /* istanbul ignore next */ []));
202
+ hostEvidence = signal([], ...(ngDevMode ? [{ debugName: "hostEvidence" }] : /* istanbul ignore next */ []));
203
+ hostEvidenceNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "hostEvidenceNextCursor" }] : /* istanbul ignore next */ []));
204
+ hostEvidenceScanBudgetExhausted = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceScanBudgetExhausted" }] : /* istanbul ignore next */ []));
205
+ statusLoading = signal(false, ...(ngDevMode ? [{ debugName: "statusLoading" }] : /* istanbul ignore next */ []));
206
+ catalogLoading = signal(false, ...(ngDevMode ? [{ debugName: "catalogLoading" }] : /* istanbul ignore next */ []));
207
+ connectionsLoading = signal(false, ...(ngDevMode ? [{ debugName: "connectionsLoading" }] : /* istanbul ignore next */ []));
208
+ detailsLoading = signal(false, ...(ngDevMode ? [{ debugName: "detailsLoading" }] : /* istanbul ignore next */ []));
209
+ attemptsLoading = signal(false, ...(ngDevMode ? [{ debugName: "attemptsLoading" }] : /* istanbul ignore next */ []));
210
+ quarantineLoading = signal(false, ...(ngDevMode ? [{ debugName: "quarantineLoading" }] : /* istanbul ignore next */ []));
211
+ hostEvidenceLoading = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceLoading" }] : /* istanbul ignore next */ []));
212
+ mutationLoading = signal(null, ...(ngDevMode ? [{ debugName: "mutationLoading" }] : /* istanbul ignore next */ []));
213
+ lastConnectionTest = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTest" }] : /* istanbul ignore next */ []));
214
+ lastConnectionTestRequestId = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTestRequestId" }] : /* istanbul ignore next */ []));
215
+ activeConnection = computed(() => this.connections().find((connection) => connection.bindings.includes('notifications')), ...(ngDevMode ? [{ debugName: "activeConnection" }] : /* istanbul ignore next */ []));
216
+ managementAvailable = computed(() => this.status()?.managementEnabled === true, ...(ngDevMode ? [{ debugName: "managementAvailable" }] : /* istanbul ignore next */ []));
217
+ initialize() {
218
+ if (this.initialized)
219
+ return;
220
+ this.initialized = true;
221
+ this.loadStatus()
222
+ .pipe(switchMap(() => forkJoin({
223
+ catalog: this.loadCatalog(),
224
+ connections: this.loadConnections(),
225
+ })))
226
+ .subscribe({ error: (error) => this.showError(error) });
227
+ }
228
+ hasPermission(permission) {
229
+ return this.options.permissionKeys.includes(permission);
230
+ }
231
+ loadStatus() {
232
+ this.statusLoading.set(true);
233
+ return this.api.getStatus().pipe(tap((status) => this.status.set(status)), finalize(() => this.statusLoading.set(false)));
234
+ }
235
+ loadCatalog() {
236
+ this.catalogLoading.set(true);
237
+ return forkJoin({
238
+ providers: this.api.getProviders(),
239
+ errors: this.api.getErrorCatalog(),
240
+ }).pipe(tap(({ providers, errors }) => {
241
+ this.providers.set(providers);
242
+ this.errorCatalog.set(errors);
243
+ }), finalize(() => this.catalogLoading.set(false)));
244
+ }
245
+ loadConnections() {
246
+ this.connectionsLoading.set(true);
247
+ return this.api.getConnections().pipe(tap((connections) => this.connections.set(connections)), finalize(() => this.connectionsLoading.set(false)));
248
+ }
249
+ loadConnection(id) {
250
+ this.detailsLoading.set(true);
251
+ return this.api.getConnection(id).pipe(tap((details) => this.selectedConnection.set(details)), finalize(() => this.detailsLoading.set(false)));
252
+ }
253
+ createConnection(request) {
254
+ this.mutationLoading.set('create');
255
+ return this.api.createConnection(request).pipe(switchMap((details) => this.refreshConnection(details.connection.id)), tap(() => this.showSuccess('connection-created')), finalize(() => this.mutationLoading.set(null)));
256
+ }
257
+ updateConnection(id, request) {
258
+ this.mutationLoading.set('update');
259
+ return this.api.updateConnection(id, request).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-updated')), finalize(() => this.mutationLoading.set(null)));
260
+ }
261
+ deleteConnection(id, concurrencyToken) {
262
+ this.mutationLoading.set('delete');
263
+ return this.api.deleteConnection(id, concurrencyToken).pipe(switchMap(() => forkJoin({
264
+ connections: this.loadConnections(),
265
+ status: this.loadStatus(),
266
+ })), tap(() => {
267
+ this.selectedConnection.set(null);
268
+ this.showSuccess('connection-deleted');
269
+ }), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
270
+ }
271
+ validateConnection(id) {
272
+ this.mutationLoading.set('validate');
273
+ return this.api.validateConnection(id).pipe(switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), tap((result) => result.isValid
274
+ ? this.toast.success(this.describeCode(result.code))
275
+ : this.toast.warn(this.describeCode(result.code))), finalize(() => this.mutationLoading.set(null)));
276
+ }
277
+ sendConnectionTest(id, request) {
278
+ this.mutationLoading.set('test');
279
+ this.lastConnectionTestRequestId.set(request.testRequestId);
280
+ return this.api.sendConnectionTest(id, request).pipe(tap((result) => this.lastConnectionTest.set(result)), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), finalize(() => this.mutationLoading.set(null)));
281
+ }
282
+ retrieveConnectionTest(connectionId, testRequestId) {
283
+ this.mutationLoading.set('retrieve-test');
284
+ return this.api.getConnectionTest(connectionId, testRequestId).pipe(tap((stored) => this.lastConnectionTest.set(stored.result)), finalize(() => this.mutationLoading.set(null)));
285
+ }
286
+ bindConnection(id, purpose = 'notifications') {
287
+ this.mutationLoading.set('bind');
288
+ return this.api.bindConnection(purpose, id).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-bound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
289
+ }
290
+ unbindConnection(id, purpose = 'notifications') {
291
+ this.mutationLoading.set('unbind');
292
+ return this.api.unbindConnection(purpose).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-unbound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
293
+ }
294
+ startAuthorization(id, kind) {
295
+ this.mutationLoading.set(kind);
296
+ const request = kind === 'delegated'
297
+ ? this.api.startDelegatedAuthorization(id)
298
+ : this.api.startAdminConsent(id);
299
+ return request.pipe(tap(() => this.rememberAuthorizationConnection(id)), finalize(() => this.mutationLoading.set(null)));
300
+ }
301
+ revokeAuthorization(id) {
302
+ this.mutationLoading.set('revoke');
303
+ return this.api.revokeAuthorization(id).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('authorization-revoked')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
304
+ }
305
+ loadAttempts(reset = true, status = this.attemptStatusFilter()) {
306
+ this.attemptsLoading.set(true);
307
+ const cursor = reset ? null : this.attemptsNextCursor();
308
+ if (reset)
309
+ this.attemptStatusFilter.set(status);
310
+ return this.api.getAttempts(status, cursor, 50).pipe(tap((page) => {
311
+ this.attempts.set(reset ? page.items : mergeById(this.attempts(), page.items, 'id'));
312
+ this.attemptsNextCursor.set(page.nextCursor);
313
+ }), finalize(() => this.attemptsLoading.set(false)));
314
+ }
315
+ replayAttempt(attempt, request) {
316
+ this.mutationLoading.set(`replay-${attempt.id}`);
317
+ return this.api.replayAttempt(attempt.id, request).pipe(tap((result) => {
318
+ this.attempts.update((items) => replaceById(items, result.attempt, 'id'));
319
+ this.showSuccess('attempt-replay-scheduled');
320
+ }), finalize(() => this.mutationLoading.set(null)));
321
+ }
322
+ loadQuarantine(reset = true) {
323
+ this.quarantineLoading.set(true);
324
+ const cursor = reset ? null : this.quarantineNextCursor();
325
+ return this.api.getFinalizationQuarantine(cursor, 100).pipe(tap((page) => {
326
+ this.quarantine.set(reset
327
+ ? page.items
328
+ : mergeById(this.quarantine(), page.items, 'itemId'));
329
+ this.quarantineNextCursor.set(page.nextCursor);
330
+ }), finalize(() => this.quarantineLoading.set(false)));
331
+ }
332
+ decideQuarantine(item, action, reason) {
333
+ this.mutationLoading.set(`${action}-${item.itemId}`);
334
+ const request = action === 'retry'
335
+ ? this.api.retryFinalizationQuarantine(item.itemId, reason)
336
+ : this.api.acknowledgeFinalizationQuarantine(item.itemId, reason);
337
+ return request.pipe(switchMap((updated) => this.loadQuarantine(true).pipe(map(() => updated))), tap(() => {
338
+ this.showSuccess(action === 'retry'
339
+ ? 'quarantine-retry-submitted'
340
+ : 'quarantine-acknowledged');
341
+ }), finalize(() => this.mutationLoading.set(null)));
342
+ }
343
+ loadHostEvidence(reset = true) {
344
+ this.hostEvidenceLoading.set(true);
345
+ const cursor = reset ? null : this.hostEvidenceNextCursor();
346
+ return this.api.getHostEvidence(cursor, 100).pipe(tap((page) => {
347
+ this.hostEvidence.set(reset
348
+ ? page.items
349
+ : mergeHostEvidence(this.hostEvidence(), page.items));
350
+ this.hostEvidenceNextCursor.set(page.nextCursor);
351
+ this.hostEvidenceScanBudgetExhausted.set(page.scanBudgetExhausted);
352
+ }), finalize(() => this.hostEvidenceLoading.set(false)));
353
+ }
354
+ exportHostEvidence(evidenceOccurrenceId) {
355
+ this.mutationLoading.set(`export-${evidenceOccurrenceId}`);
356
+ return this.api
357
+ .exportHostEvidence(evidenceOccurrenceId)
358
+ .pipe(finalize(() => this.mutationLoading.set(null)));
359
+ }
360
+ decideHostEvidence(item, action, reason) {
361
+ if (!item.evidenceOccurrenceId || !item.decisionToken) {
362
+ return throwError(() => new Error('Incomplete evidence identity'));
363
+ }
364
+ this.mutationLoading.set(`${action}-${item.evidenceOccurrenceId}`);
365
+ const request = {
366
+ reason,
367
+ decisionToken: item.decisionToken,
368
+ };
369
+ const operation = action === 'repair'
370
+ ? this.api.repairHostEvidence(item.evidenceOccurrenceId, request)
371
+ : this.api.acknowledgeHostEvidence(item.evidenceOccurrenceId, request);
372
+ return operation.pipe(switchMap((updated) => this.loadHostEvidence(true).pipe(map(() => updated))), tap(() => {
373
+ this.showSuccess(action === 'repair' ? 'evidence-repaired' : 'evidence-acknowledged');
374
+ }), finalize(() => this.mutationLoading.set(null)));
375
+ }
376
+ rebuildEvidenceIndex(take) {
377
+ this.mutationLoading.set('rebuild-evidence');
378
+ return this.api.rebuildEvidenceIndex(take).pipe(switchMap((result) => this.loadHostEvidence(true).pipe(map(() => result))), tap(() => this.showSuccess('evidence-index-rebuilt')), finalize(() => this.mutationLoading.set(null)));
379
+ }
380
+ providerByKey(key) {
381
+ return this.providers().find((provider) => provider.key === key) ?? null;
382
+ }
383
+ describeCode(code) {
384
+ if (!code)
385
+ return this.transloco.translate('email-delivery.unknown-code');
386
+ const key = `email-delivery.codes.${code}`;
387
+ const translated = this.transloco.translate(key);
388
+ if (translated !== key)
389
+ return translated;
390
+ const contract = this.errorCatalog().find((entry) => entry.code === code);
391
+ if (!contract || contract.frontendAction === 'none')
392
+ return code;
393
+ const actionKey = `email-delivery.error-actions.${contract.frontendAction}`;
394
+ const action = this.transloco.translate(actionKey);
395
+ return action === actionKey ? code : `${action} (${code})`;
396
+ }
397
+ describeError(error) {
398
+ const problem = extractProblem(error);
399
+ const base = this.describeCode(problem?.code);
400
+ return problem?.correlationId
401
+ ? `${base} (${this.transloco.translate('email-delivery.correlation-id')}: ${problem.correlationId})`
402
+ : base;
403
+ }
404
+ errorAction(error) {
405
+ const problem = extractProblem(error);
406
+ if (!problem)
407
+ return null;
408
+ return (problem.action ||
409
+ this.errorCatalog().find((entry) => entry.code === problem.code)
410
+ ?.frontendAction ||
411
+ null);
412
+ }
413
+ showError(error) {
414
+ this.toast.error(this.describeError(error));
415
+ }
416
+ takePendingAuthorizationConnection() {
417
+ if (typeof sessionStorage === 'undefined')
418
+ return null;
419
+ const id = sessionStorage.getItem(PENDING_AUTHORIZATION_STORAGE_KEY);
420
+ sessionStorage.removeItem(PENDING_AUTHORIZATION_STORAGE_KEY);
421
+ return id;
422
+ }
423
+ refreshConnection(id) {
424
+ return forkJoin({
425
+ details: this.api.getConnection(id),
426
+ connections: this.api.getConnections(),
427
+ status: this.api.getStatus(),
428
+ }).pipe(tap(({ details, connections, status }) => {
429
+ this.selectedConnection.set(details);
430
+ this.connections.set(connections);
431
+ this.status.set(status);
432
+ }), map(({ details }) => details));
433
+ }
434
+ rememberAuthorizationConnection(id) {
435
+ if (typeof sessionStorage === 'undefined')
436
+ return;
437
+ sessionStorage.setItem(PENDING_AUTHORIZATION_STORAGE_KEY, id);
438
+ }
439
+ showSuccess(key) {
440
+ this.toast.success(this.transloco.translate(`email-delivery.${key}`));
441
+ }
442
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
443
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, providedIn: 'root' });
444
+ }
445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, decorators: [{
446
+ type: Injectable,
447
+ args: [{ providedIn: 'root' }]
448
+ }] });
449
+ function extractProblem(error) {
450
+ if (error instanceof HttpErrorResponse) {
451
+ const value = error.error;
452
+ return value && typeof value.code === 'string'
453
+ ? value
454
+ : null;
455
+ }
456
+ if (typeof error === 'object' &&
457
+ error !== null &&
458
+ typeof error.code === 'string') {
459
+ return error;
460
+ }
461
+ return null;
462
+ }
463
+ function mergeById(current, incoming, key) {
464
+ const values = new Map(current.map((item) => [item[key], item]));
465
+ incoming.forEach((item) => values.set(item[key], item));
466
+ return Array.from(values.values());
467
+ }
468
+ function replaceById(current, replacement, key) {
469
+ return current.map((item) => item[key] === replacement[key] ? replacement : item);
470
+ }
471
+ function mergeHostEvidence(current, incoming) {
472
+ const identity = (item) => item.evidenceOccurrenceId ?? item.itemId;
473
+ const values = new Map(current.map((item) => [identity(item), item]));
474
+ incoming.forEach((item) => values.set(identity(item), item));
475
+ return Array.from(values.values());
476
+ }
477
+
478
+ const STATUS_DEFINITIONS = {
479
+ connection: {
480
+ Draft: status('#64748B', 'connection.draft'),
481
+ PendingAuthorization: status('#D97706', 'connection.pending-authorization'),
482
+ PendingValidation: status('#0284C7', 'connection.pending-validation'),
483
+ Ready: status('#059669', 'connection.ready'),
484
+ ReauthorizationRequired: status('#EA580C', 'connection.reauthorization-required'),
485
+ ConditionalAccessRequired: status('#EA580C', 'connection.conditional-access-required'),
486
+ Disabled: status('#64748B', 'connection.disabled'),
487
+ Invalid: status('#DC2626', 'connection.invalid'),
488
+ },
489
+ attempt: {
490
+ Reserved: status('#64748B', 'attempt.reserved'),
491
+ ProviderInvocationStarted: status('#0284C7', 'attempt.provider-invocation-started'),
492
+ ProviderInvocationDefinitivelyNotAccepted: status('#DC2626', 'attempt.provider-not-accepted'),
493
+ ProviderResultReconciliation: status('#7C3AED', 'attempt.reconciling'),
494
+ ProviderResultPending: status('#0284C7', 'attempt.pending'),
495
+ Blocked: status('#DC2626', 'attempt.blocked'),
496
+ RetryScheduled: status('#D97706', 'attempt.retry-scheduled'),
497
+ Accepted: status('#059669', 'attempt.accepted'),
498
+ OutcomeUnknown: status('#EA580C', 'attempt.outcome-unknown'),
499
+ FailedPermanent: status('#DC2626', 'attempt.failed-permanent'),
500
+ DeadLettered: status('#BE123C', 'attempt.dead-lettered'),
501
+ },
502
+ support: {
503
+ active: status('#D97706', 'support.active'),
504
+ 'repair-pending': status('#0284C7', 'support.repair-pending'),
505
+ repaired: status('#059669', 'support.repaired'),
506
+ 'acknowledgement-pending': status('#0284C7', 'support.acknowledgement-pending'),
507
+ acknowledged: status('#64748B', 'support.acknowledged'),
508
+ Owned: status('#059669', 'support.owned'),
509
+ owned: status('#059669', 'support.owned'),
510
+ Missing: status('#D97706', 'support.missing'),
511
+ missing: status('#D97706', 'support.missing'),
512
+ Conflict: status('#DC2626', 'support.conflict'),
513
+ conflict: status('#DC2626', 'support.conflict'),
514
+ IdentityIncomplete: status('#EA580C', 'support.identity-incomplete'),
515
+ identityincomplete: status('#EA580C', 'support.identity-incomplete'),
516
+ 'retryable-after-upgrade': status('#0284C7', 'support.retryable-after-upgrade'),
517
+ 'repair-required': status('#EA580C', 'support.repair-required'),
518
+ 'acknowledge-only': status('#64748B', 'support.acknowledge-only'),
519
+ configured: status('#059669', 'support.configured'),
520
+ 'not-configured': status('#64748B', 'support.not-configured'),
521
+ },
522
+ };
523
+ class EmailDeliveryStatus {
524
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
525
+ kind = input('support', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
526
+ transloco = inject(TranslocoService);
527
+ activeLanguage = toSignal(this.transloco.langChanges$, {
528
+ initialValue: this.transloco.getActiveLang(),
529
+ });
530
+ statusData = computed(() => {
531
+ this.activeLanguage();
532
+ return {
533
+ name: '',
534
+ viewType: 'Status',
535
+ value: getEmailDeliveryStatusValue(this.kind(), this.value(), (key) => this.transloco.translate(key)),
536
+ configuration: { hideName: true },
537
+ };
538
+ }, ...(ngDevMode ? [{ debugName: "statusData" }] : /* istanbul ignore next */ []));
539
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, deps: [], target: i0.ɵɵFactoryTarget.Component });
540
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: EmailDeliveryStatus, isStandalone: true, selector: "mt-email-delivery-status", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: `<mt-entity-status [data]="statusData()" />`, isInline: true, dependencies: [{ kind: "component", type: EntityStatus, selector: "mt-entity-status", inputs: ["data", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
541
+ }
542
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, decorators: [{
543
+ type: Component,
544
+ args: [{
545
+ selector: 'mt-email-delivery-status',
546
+ imports: [EntityStatus],
547
+ template: `<mt-entity-status [data]="statusData()" />`,
548
+ host: { class: 'inline-flex' },
549
+ changeDetection: ChangeDetectionStrategy.OnPush,
550
+ }]
551
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: false }] }] } });
552
+ function getEmailDeliveryStatusMap(kind, translate) {
553
+ return Object.fromEntries(Object.entries(STATUS_DEFINITIONS[kind]).map(([key, definition]) => [
554
+ key,
555
+ {
556
+ color: definition.color,
557
+ display: translate(`email-delivery.status.${definition.translationKey}`),
558
+ },
559
+ ]));
560
+ }
561
+ function getEmailDeliveryStatusValue(kind, value, translate) {
562
+ const definition = STATUS_DEFINITIONS[kind][value];
563
+ return {
564
+ key: value,
565
+ display: definition
566
+ ? translate(`email-delivery.status.${definition.translationKey}`)
567
+ : value,
568
+ color: definition?.color ?? '#64748B',
569
+ };
570
+ }
571
+ function status(color, translationKey) {
572
+ return { color, translationKey };
573
+ }
574
+
575
+ class EmailOperatorReasonDialog {
576
+ itemName = input.required(...(ngDevMode ? [{ debugName: "itemName" }] : /* istanbul ignore next */ []));
577
+ consequence = input.required(...(ngDevMode ? [{ debugName: "consequence" }] : /* istanbul ignore next */ []));
578
+ showExceptionalOverride = input(false, ...(ngDevMode ? [{ debugName: "showExceptionalOverride" }] : /* istanbul ignore next */ []));
579
+ confirmLabel = input(null, ...(ngDevMode ? [{ debugName: "confirmLabel" }] : /* istanbul ignore next */ []));
580
+ ref = inject(ModalRef);
581
+ modal = inject(ModalService);
582
+ reason = new FormControl('', {
583
+ nonNullable: true,
584
+ validators: [Validators.required, Validators.maxLength(500)],
585
+ });
586
+ exceptionalOverride = new FormControl(false, {
587
+ nonNullable: true,
588
+ });
589
+ cancel() {
590
+ this.ref.close();
591
+ }
592
+ confirm() {
593
+ this.reason.markAsTouched();
594
+ this.exceptionalOverride.markAsTouched();
595
+ if (!this.canConfirm())
596
+ return;
597
+ this.ref.close({
598
+ reason: this.reason.getRawValue().trim(),
599
+ exceptionalOverride: this.exceptionalOverride.getRawValue(),
600
+ });
601
+ }
602
+ canConfirm() {
603
+ return (this.reason.valid &&
604
+ (!this.showExceptionalOverride() ||
605
+ this.exceptionalOverride.getRawValue()));
606
+ }
607
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
608
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailOperatorReasonDialog, isStandalone: true, selector: "mt-email-operator-reason-dialog", inputs: { itemName: { classPropertyName: "itemName", publicName: "itemName", isSignal: true, isRequired: true, transformFunction: null }, consequence: { classPropertyName: "consequence", publicName: "consequence", isSignal: true, isRequired: true, transformFunction: null }, showExceptionalOverride: { classPropertyName: "showExceptionalOverride", publicName: "showExceptionalOverride", isSignal: true, isRequired: false, transformFunction: null }, confirmLabel: { classPropertyName: "confirmLabel", publicName: "confirmLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n [hint]=\"t('exceptional-override-hint')\"\r\n />\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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: 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: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
609
+ }
610
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, decorators: [{
611
+ type: Component,
612
+ args: [{ selector: 'mt-email-operator-reason-dialog', imports: [
613
+ ReactiveFormsModule,
614
+ TranslocoDirective,
615
+ Button,
616
+ TextareaField,
617
+ ToggleField,
618
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n [hint]=\"t('exceptional-override-hint')\"\r\n />\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
619
+ }], propDecorators: { itemName: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemName", required: true }] }], consequence: [{ type: i0.Input, args: [{ isSignal: true, alias: "consequence", required: true }] }], showExceptionalOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExceptionalOverride", required: false }] }], confirmLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmLabel", required: false }] }] } });
620
+
621
+ class EmailAttemptDetails {
622
+ attempt = input.required(...(ngDevMode ? [{ debugName: "attempt" }] : /* istanbul ignore next */ []));
623
+ ref = inject(ModalRef);
624
+ transloco = inject(TranslocoService);
625
+ modal = inject(ModalService);
626
+ facade = inject(EmailDeliveryFacade);
627
+ canReplay() {
628
+ return [
629
+ 'Blocked',
630
+ 'RetryScheduled',
631
+ 'Accepted',
632
+ 'FailedPermanent',
633
+ 'DeadLettered',
634
+ 'OutcomeUnknown',
635
+ ].includes(this.attempt().status);
636
+ }
637
+ providerLabel(providerKey) {
638
+ return (this.facade.providerByKey(providerKey)?.displayName ?? providerKey ?? '—');
639
+ }
640
+ replay() {
641
+ const attempt = this.attempt();
642
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
643
+ header: this.t('replay-attempt'),
644
+ styleClass: '!w-[36rem] max-w-[96vw]',
645
+ dismissible: true,
646
+ inputValues: {
647
+ itemName: attempt.id,
648
+ consequence: isExceptionalReplay(attempt.status)
649
+ ? this.t('replay-exceptional-warning')
650
+ : this.t('replay-warning'),
651
+ showExceptionalOverride: isExceptionalReplay(attempt.status),
652
+ confirmLabel: this.t('replay-attempt'),
653
+ },
654
+ });
655
+ dialog.onClose
656
+ .pipe(take(1))
657
+ .subscribe((decision) => {
658
+ if (!decision)
659
+ return;
660
+ this.facade
661
+ .replayAttempt(attempt, {
662
+ concurrencyToken: attempt.concurrencyToken,
663
+ exceptionalOverride: decision.exceptionalOverride,
664
+ reason: decision.reason,
665
+ })
666
+ .subscribe({
667
+ next: (result) => this.ref.close(result.attempt),
668
+ error: (error) => this.facade.showError(error),
669
+ });
670
+ });
671
+ }
672
+ close() {
673
+ this.ref.close();
674
+ }
675
+ t(key) {
676
+ return this.transloco.translate(`email-delivery.${key}`);
677
+ }
678
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, deps: [], target: i0.ɵɵFactoryTarget.Component });
679
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailAttemptDetails, isStandalone: true, selector: "mt-email-attempt-details", inputs: { attempt: { classPropertyName: "attempt", publicName: "attempt", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-lg border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n </dl>\r\n </mt-card>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\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: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
680
+ }
681
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, decorators: [{
682
+ type: Component,
683
+ args: [{ selector: 'mt-email-attempt-details', imports: [DatePipe, TranslocoDirective, Button, Card, EmailDeliveryStatus], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-lg border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n </dl>\r\n </mt-card>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
684
+ }], propDecorators: { attempt: [{ type: i0.Input, args: [{ isSignal: true, alias: "attempt", required: true }] }] } });
685
+ function isExceptionalReplay(status) {
686
+ return status === 'Accepted' || status === 'OutcomeUnknown';
687
+ }
688
+
689
+ class EmailDeliveryAttempts {
690
+ modal = inject(ModalService);
691
+ transloco = inject(TranslocoService);
692
+ facade = inject(EmailDeliveryFacade);
693
+ status = new FormControl(null);
694
+ activeLanguage = toSignal(this.transloco.langChanges$, {
695
+ initialValue: this.transloco.getActiveLang(),
696
+ });
697
+ statusOptions = computed(() => {
698
+ this.activeLanguage();
699
+ const statusMap = getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco));
700
+ return [
701
+ { label: this.t('all-statuses'), value: null },
702
+ ...EMAIL_DELIVERY_ATTEMPT_STATUSES.map((value) => ({
703
+ label: statusMap[value]?.display ?? value,
704
+ value,
705
+ })),
706
+ ];
707
+ }, ...(ngDevMode ? [{ debugName: "statusOptions" }] : /* istanbul ignore next */ []));
708
+ columns = computed(() => {
709
+ this.activeLanguage();
710
+ return [
711
+ {
712
+ key: 'status',
713
+ label: this.t('attempt.status'),
714
+ type: 'status',
715
+ statusMap: getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco)),
716
+ },
717
+ { key: 'providerKey', label: this.t('attempt.provider') },
718
+ { key: 'recipientFingerprint', label: this.t('recipient-fingerprint') },
719
+ {
720
+ key: 'createdAtUtc',
721
+ label: this.t('attempt.created'),
722
+ type: 'dateTime',
723
+ },
724
+ { key: 'attemptCount', label: this.t('attempt.count'), type: 'number' },
725
+ ];
726
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
727
+ actions = computed(() => {
728
+ this.activeLanguage();
729
+ return [
730
+ {
731
+ icon: 'general.eye',
732
+ label: this.t('view-details'),
733
+ action: (row) => this.openDetails(row),
734
+ },
735
+ ];
736
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
737
+ ngOnInit() {
738
+ this.load(true);
739
+ }
740
+ applyFilter() {
741
+ this.load(true);
742
+ }
743
+ loadMore() {
744
+ this.load(false);
745
+ }
746
+ openDetails(attempt) {
747
+ const ref = this.modal.openModal(EmailAttemptDetails, 'drawer', {
748
+ header: this.t('attempt-details'),
749
+ styleClass: '!absolute !shadow-none !w-full !max-w-full',
750
+ position: 'end',
751
+ appendTo: 'page-content',
752
+ dismissableMask: true,
753
+ dismissible: true,
754
+ inputValues: { attempt },
755
+ });
756
+ ref.onClose.pipe(take(1)).subscribe((updated) => {
757
+ if (updated)
758
+ this.load(true);
759
+ });
760
+ }
761
+ load(reset) {
762
+ this.facade.loadAttempts(reset, this.status.getRawValue()).subscribe({
763
+ error: (error) => this.facade.showError(error),
764
+ });
765
+ }
766
+ t(key) {
767
+ return this.transloco.translate(`email-delivery.${key}`);
768
+ }
769
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, deps: [], target: i0.ɵɵFactoryTarget.Component });
770
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryAttempts, isStandalone: true, selector: "mt-email-delivery-attempts", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-labelledby=\"attempts-heading\">\r\n <div>\r\n <h2 id=\"attempts-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"activity-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"activity-description\") }}\r\n </p>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-end\">\r\n <mt-select-field\r\n class=\"w-full sm:w-72\"\r\n [formControl]=\"status\"\r\n [label]=\"t('attempt.status-filter')\"\r\n [options]=\"statusOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n />\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('apply-filter')\"\r\n (onClick)=\"applyFilter()\"\r\n />\r\n </div>\r\n\r\n @if (!facade.attemptsLoading() && facade.attempts().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-attempts-title')\"\r\n [description]=\"t('no-attempts-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"facade.attempts()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ 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: "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: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
771
+ }
772
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, decorators: [{
773
+ type: Component,
774
+ args: [{ selector: 'mt-email-delivery-attempts', imports: [
775
+ ReactiveFormsModule,
776
+ TranslocoDirective,
777
+ Button,
778
+ EmptyState,
779
+ SelectField,
780
+ Table,
781
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-labelledby=\"attempts-heading\">\r\n <div>\r\n <h2 id=\"attempts-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"activity-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"activity-description\") }}\r\n </p>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-end\">\r\n <mt-select-field\r\n class=\"w-full sm:w-72\"\r\n [formControl]=\"status\"\r\n [label]=\"t('attempt.status-filter')\"\r\n [options]=\"statusOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n />\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('apply-filter')\"\r\n (onClick)=\"applyFilter()\"\r\n />\r\n </div>\r\n\r\n @if (!facade.attemptsLoading() && facade.attempts().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-attempts-title')\"\r\n [description]=\"t('no-attempts-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"facade.attempts()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
782
+ }] });
783
+
784
+ class EmailDeliveryHostEvidence {
785
+ document = inject(DOCUMENT);
786
+ modal = inject(ModalService);
787
+ confirmation = inject(ConfirmationService);
788
+ transloco = inject(TranslocoService);
789
+ facade = inject(EmailDeliveryFacade);
790
+ activeLanguage = toSignal(this.transloco.langChanges$, {
791
+ initialValue: this.transloco.getActiveLang(),
792
+ });
793
+ rows = computed(() => this.facade.hostEvidence().map((item) => ({
794
+ ...item,
795
+ evidenceRowId: item.evidenceOccurrenceId ?? item.itemId,
796
+ evidenceKindLabel: humanizeIdentifier$1(item.evidenceKind),
797
+ evidenceOccurrenceIdentity: identifierEntity$1(item.evidenceOccurrenceId),
798
+ errorLabel: this.facade.describeCode(item.errorCode),
799
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
800
+ columns = computed(() => {
801
+ this.activeLanguage();
802
+ const statusMap = getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco));
803
+ return [
804
+ { key: 'evidenceKindLabel', label: this.t('evidence-kind') },
805
+ {
806
+ key: 'resolutionState',
807
+ label: this.t('resolution-state'),
808
+ type: 'status',
809
+ statusMap,
810
+ },
811
+ {
812
+ key: 'ownershipState',
813
+ label: this.t('ownership-state'),
814
+ type: 'status',
815
+ statusMap,
816
+ },
817
+ {
818
+ key: 'detectedAtUtc',
819
+ label: this.t('detected'),
820
+ type: 'dateTime',
821
+ },
822
+ { key: 'errorLabel', label: this.t('issue') },
823
+ {
824
+ key: 'evidenceOccurrenceIdentity',
825
+ label: this.t('evidence-occurrence-id'),
826
+ type: 'entity',
827
+ width: '20rem',
828
+ shrink: true,
829
+ },
830
+ ];
831
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
832
+ actions = computed(() => {
833
+ this.activeLanguage();
834
+ return [
835
+ {
836
+ icon: 'general.download-01',
837
+ label: this.t('export'),
838
+ hidden: (row) => {
839
+ const item = row;
840
+ return !item.canExport || !item.evidenceOccurrenceId;
841
+ },
842
+ action: (row) => this.export(row),
843
+ },
844
+ {
845
+ icon: 'general.tool-02',
846
+ label: this.t('repair'),
847
+ hidden: (row) => !this.facade.managementAvailable() ||
848
+ !row.canRepair ||
849
+ !row.evidenceOccurrenceId ||
850
+ !row.decisionToken,
851
+ action: (row) => this.decide(row, 'repair'),
852
+ },
853
+ {
854
+ icon: 'general.check-circle',
855
+ label: this.t('acknowledge'),
856
+ color: 'danger',
857
+ hidden: (row) => !this.facade.managementAvailable() ||
858
+ !row.evidenceOccurrenceId ||
859
+ !row.decisionToken,
860
+ action: (row) => this.decide(row, 'acknowledge'),
861
+ },
862
+ ];
863
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
864
+ ngOnInit() {
865
+ if (this.facade.hostEvidence().length === 0)
866
+ this.load(true);
867
+ }
868
+ export(item) {
869
+ if (!item.evidenceOccurrenceId)
870
+ return;
871
+ this.facade.exportHostEvidence(item.evidenceOccurrenceId).subscribe({
872
+ next: (download) => {
873
+ const url = URL.createObjectURL(download.blob);
874
+ const anchor = this.document.createElement('a');
875
+ anchor.href = url;
876
+ anchor.download = download.fileName;
877
+ anchor.rel = 'noopener';
878
+ anchor.click();
879
+ URL.revokeObjectURL(url);
880
+ },
881
+ error: (error) => this.facade.showError(error),
882
+ });
883
+ }
884
+ decide(item, action) {
885
+ if (!item.evidenceOccurrenceId || !item.decisionToken)
886
+ return;
887
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
888
+ header: this.t(action === 'repair' ? 'repair-evidence' : 'acknowledge-evidence'),
889
+ styleClass: '!w-[36rem] max-w-[96vw]',
890
+ dismissible: true,
891
+ inputValues: {
892
+ itemName: item.evidenceOccurrenceId,
893
+ consequence: this.t(action === 'repair'
894
+ ? 'evidence-repair-warning'
895
+ : 'evidence-acknowledge-warning'),
896
+ confirmLabel: this.t(action),
897
+ },
898
+ });
899
+ dialog.onClose
900
+ .pipe(take(1))
901
+ .subscribe((decision) => {
902
+ if (!decision)
903
+ return;
904
+ this.facade
905
+ .decideHostEvidence(item, action, decision.reason)
906
+ .subscribe({
907
+ error: (error) => {
908
+ this.facade.showError(error);
909
+ this.load(true);
910
+ },
911
+ });
912
+ });
913
+ }
914
+ confirmRebuild() {
915
+ this.confirmation.confirm({
916
+ type: 'dialog',
917
+ header: this.t('rebuild-evidence-index'),
918
+ message: this.t('rebuild-evidence-warning'),
919
+ accept: () => this.facade.rebuildEvidenceIndex(200).subscribe({
920
+ error: (error) => this.facade.showError(error),
921
+ }),
922
+ });
923
+ }
924
+ loadMore() {
925
+ this.load(false);
926
+ }
927
+ load(reset) {
928
+ this.facade.loadHostEvidence(reset).subscribe({
929
+ error: (error) => this.facade.showError(error),
930
+ });
931
+ }
932
+ t(key) {
933
+ return this.transloco.translate(`email-delivery.${key}`);
934
+ }
935
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, deps: [], target: i0.ɵɵFactoryTarget.Component });
936
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryHostEvidence, isStandalone: true, selector: "mt-email-delivery-host-evidence", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"host-evidence-heading\"\r\n >\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n @if (!facade.hostEvidenceLoading() && facade.hostEvidence().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-evidence-title')\"\r\n [description]=\"t('no-evidence-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n }\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\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: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
937
+ }
938
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, decorators: [{
939
+ type: Component,
940
+ args: [{ selector: 'mt-email-delivery-host-evidence', imports: [TranslocoDirective, Button, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"host-evidence-heading\"\r\n >\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n @if (!facade.hostEvidenceLoading() && facade.hostEvidence().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-evidence-title')\"\r\n [description]=\"t('no-evidence-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n }\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
941
+ }] });
942
+ function identifierEntity$1(value) {
943
+ return {
944
+ name: '',
945
+ viewType: 'Text',
946
+ value: value ?? '—',
947
+ configuration: { hideName: true },
948
+ };
949
+ }
950
+ function humanizeIdentifier$1(value) {
951
+ return value
952
+ .replace(/[._-]+/g, ' ')
953
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
954
+ .replace(/\s+/g, ' ')
955
+ .trim()
956
+ .replace(/^./, (character) => character.toUpperCase())
957
+ .replace(/\bSmtp\b/g, 'SMTP')
958
+ .replace(/\bTls\b/g, 'TLS')
959
+ .replace(/\bOauth2?\b/gi, 'OAuth2')
960
+ .replace(/\bHttp\b/g, 'HTTP')
961
+ .replace(/\bApi\b/g, 'API');
962
+ }
963
+
964
+ class EmailDeliveryQuarantine {
965
+ modal = inject(ModalService);
966
+ transloco = inject(TranslocoService);
967
+ facade = inject(EmailDeliveryFacade);
968
+ activeLanguage = toSignal(this.transloco.langChanges$, {
969
+ initialValue: this.transloco.getActiveLang(),
970
+ });
971
+ rows = computed(() => this.facade.quarantine().map((item) => ({
972
+ ...item,
973
+ attemptIdentity: identifierEntity(item.attemptId),
974
+ errorLabel: this.facade.describeCode(item.errorCode),
975
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
976
+ columns = computed(() => {
977
+ this.activeLanguage();
978
+ return [
979
+ {
980
+ key: 'attemptIdentity',
981
+ label: this.t('attempt.id'),
982
+ type: 'entity',
983
+ width: '19rem',
984
+ shrink: true,
985
+ },
986
+ {
987
+ key: 'recoveryClassification',
988
+ label: this.t('recovery-classification'),
989
+ type: 'status',
990
+ statusMap: getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco)),
991
+ },
992
+ { key: 'errorLabel', label: this.t('issue') },
993
+ {
994
+ key: 'quarantinedAtUtc',
995
+ label: this.t('detected'),
996
+ type: 'dateTime',
997
+ },
998
+ ];
999
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
1000
+ actions = computed(() => {
1001
+ this.activeLanguage();
1002
+ return [
1003
+ {
1004
+ icon: 'arrow.refresh-cw-01',
1005
+ label: this.t('retry'),
1006
+ hidden: (row) => !this.facade.managementAvailable() ||
1007
+ row.recoveryClassification !==
1008
+ 'retryable-after-upgrade',
1009
+ action: (row) => this.decide(row, 'retry'),
1010
+ },
1011
+ {
1012
+ icon: 'general.check-circle',
1013
+ label: this.t('acknowledge'),
1014
+ color: 'danger',
1015
+ hidden: () => !this.facade.managementAvailable(),
1016
+ action: (row) => this.decide(row, 'acknowledge'),
1017
+ },
1018
+ ];
1019
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
1020
+ ngOnInit() {
1021
+ if (this.facade.quarantine().length === 0)
1022
+ this.load(true);
1023
+ }
1024
+ decide(item, action) {
1025
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
1026
+ header: this.t(action === 'retry' ? 'retry-quarantine' : 'acknowledge-quarantine'),
1027
+ styleClass: '!w-[36rem] max-w-[96vw]',
1028
+ dismissible: true,
1029
+ inputValues: {
1030
+ itemName: item.itemId,
1031
+ consequence: this.t(action === 'retry'
1032
+ ? 'quarantine-retry-warning'
1033
+ : 'quarantine-acknowledge-warning'),
1034
+ confirmLabel: this.t(action),
1035
+ },
1036
+ });
1037
+ dialog.onClose
1038
+ .pipe(take(1))
1039
+ .subscribe((decision) => {
1040
+ if (!decision)
1041
+ return;
1042
+ this.facade.decideQuarantine(item, action, decision.reason).subscribe({
1043
+ error: (error) => this.facade.showError(error),
1044
+ });
1045
+ });
1046
+ }
1047
+ loadMore() {
1048
+ this.load(false);
1049
+ }
1050
+ load(reset) {
1051
+ this.facade.loadQuarantine(reset).subscribe({
1052
+ error: (error) => this.facade.showError(error),
1053
+ });
1054
+ }
1055
+ t(key) {
1056
+ return this.transloco.translate(`email-delivery.${key}`);
1057
+ }
1058
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, deps: [], target: i0.ɵɵFactoryTarget.Component });
1059
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryQuarantine, isStandalone: true, selector: "mt-email-delivery-quarantine", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"quarantine-heading\"\r\n >\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n @if (!facade.quarantineLoading() && facade.quarantine().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-quarantine-title')\"\r\n [description]=\"t('no-quarantine-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n }\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\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: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1060
+ }
1061
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, decorators: [{
1062
+ type: Component,
1063
+ args: [{ selector: 'mt-email-delivery-quarantine', imports: [TranslocoDirective, Button, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"quarantine-heading\"\r\n >\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n @if (!facade.quarantineLoading() && facade.quarantine().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-quarantine-title')\"\r\n [description]=\"t('no-quarantine-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n }\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
1064
+ }] });
1065
+ function identifierEntity(value) {
1066
+ return {
1067
+ name: '',
1068
+ viewType: 'Text',
1069
+ value: value ?? '—',
1070
+ configuration: { hideName: true },
1071
+ };
1072
+ }
1073
+
1074
+ class EmailDeliveryActivity {
1075
+ advancedExpanded = signal(false, ...(ngDevMode ? [{ debugName: "advancedExpanded" }] : /* istanbul ignore next */ []));
1076
+ onAdvancedToggle(event) {
1077
+ this.advancedExpanded.set(event.currentTarget.open);
1078
+ }
1079
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, deps: [], target: i0.ɵɵFactoryTarget.Component });
1080
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryActivity, isStandalone: true, selector: "mt-email-delivery-activity", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-6\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <details\r\n class=\"group overflow-hidden rounded-xl border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"cursor-pointer list-none p-5 font-semibold text-color focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span class=\"flex items-center justify-between gap-3\">\r\n <span>\r\n {{ t(\"advanced-support\") }}\r\n <span class=\"mt-1 block text-sm font-normal text-muted-color\">{{\r\n t(\"advanced-support-description\")\r\n }}</span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"text-base transition-transform group-open:rotate-180\"\r\n />\r\n </span>\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-6 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: EmailDeliveryAttempts, selector: "mt-email-delivery-attempts" }, { kind: "component", type: EmailDeliveryQuarantine, selector: "mt-email-delivery-quarantine" }, { kind: "component", type: EmailDeliveryHostEvidence, selector: "mt-email-delivery-host-evidence" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1081
+ }
1082
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, decorators: [{
1083
+ type: Component,
1084
+ args: [{ selector: 'mt-email-delivery-activity', imports: [
1085
+ TranslocoDirective,
1086
+ Icon,
1087
+ EmailDeliveryAttempts,
1088
+ EmailDeliveryQuarantine,
1089
+ EmailDeliveryHostEvidence,
1090
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-6\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <details\r\n class=\"group overflow-hidden rounded-xl border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"cursor-pointer list-none p-5 font-semibold text-color focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span class=\"flex items-center justify-between gap-3\">\r\n <span>\r\n {{ t(\"advanced-support\") }}\r\n <span class=\"mt-1 block text-sm font-normal text-muted-color\">{{\r\n t(\"advanced-support-description\")\r\n }}</span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"text-base transition-transform group-open:rotate-180\"\r\n />\r\n </span>\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-6 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n" }]
1091
+ }] });
1092
+
1093
+ class EmailControlledTestDialog {
1094
+ connectionId = input.required(...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
1095
+ connectionName = input.required(...(ngDevMode ? [{ debugName: "connectionName" }] : /* istanbul ignore next */ []));
1096
+ modal = inject(ModalService);
1097
+ ref = inject(ModalRef);
1098
+ destroyRef = inject(DestroyRef);
1099
+ transloco = inject(TranslocoService);
1100
+ facade = inject(EmailDeliveryFacade);
1101
+ testRequestId = signal(createId(), ...(ngDevMode ? [{ debugName: "testRequestId" }] : /* istanbul ignore next */ []));
1102
+ timedOut = signal(false, ...(ngDevMode ? [{ debugName: "timedOut" }] : /* istanbul ignore next */ []));
1103
+ polling = signal(false, ...(ngDevMode ? [{ debugName: "polling" }] : /* istanbul ignore next */ []));
1104
+ result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
1105
+ pollIndex = 0;
1106
+ form = new FormControl({
1107
+ recipients: null,
1108
+ subject: 'Pplus email delivery test',
1109
+ body: 'This is a controlled Pplus email delivery test.',
1110
+ isHtml: false,
1111
+ }, { nonNullable: true });
1112
+ formStatus = toSignal(this.form.statusChanges, {
1113
+ initialValue: this.form.status,
1114
+ });
1115
+ canSubmit = computed(() => this.formStatus() === 'VALID' &&
1116
+ !this.facade.mutationLoading() &&
1117
+ !this.result(), ...(ngDevMode ? [{ debugName: "canSubmit" }] : /* istanbul ignore next */ []));
1118
+ formConfig = computed(() => ({
1119
+ sections: [
1120
+ {
1121
+ key: 'controlled-test',
1122
+ type: 'none',
1123
+ columns: 12,
1124
+ fields: [
1125
+ new TextareaFieldConfig({
1126
+ key: 'recipients',
1127
+ label: this.t('test.recipients'),
1128
+ placeholder: this.t('test.recipients-placeholder'),
1129
+ hint: this.t('test.recipients-hint'),
1130
+ colSpan: 12,
1131
+ rows: 3,
1132
+ validators: [
1133
+ ValidatorConfig.required(),
1134
+ ValidatorConfig.custom((value) => validRecipients(value), this.t('test.recipients-invalid')),
1135
+ ],
1136
+ }),
1137
+ new TextFieldConfig({
1138
+ key: 'subject',
1139
+ label: this.t('test.subject'),
1140
+ colSpan: 12,
1141
+ validators: [
1142
+ ValidatorConfig.required(),
1143
+ ValidatorConfig.maxLength(998),
1144
+ ValidatorConfig.pattern('^[^\\r\\n]*$'),
1145
+ ],
1146
+ }),
1147
+ new TextareaFieldConfig({
1148
+ key: 'body',
1149
+ label: this.t('test.body'),
1150
+ colSpan: 12,
1151
+ rows: 8,
1152
+ validators: [ValidatorConfig.maxLength(2_000_000)],
1153
+ }),
1154
+ new ToggleFieldConfig({
1155
+ key: 'isHtml',
1156
+ label: this.t('test.html'),
1157
+ colSpan: 12,
1158
+ }),
1159
+ ],
1160
+ },
1161
+ ],
1162
+ }), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
1163
+ submit() {
1164
+ if (!this.canSubmit()) {
1165
+ this.form.markAllAsTouched();
1166
+ return;
1167
+ }
1168
+ const value = this.form.getRawValue();
1169
+ this.facade
1170
+ .sendConnectionTest(this.connectionId(), {
1171
+ testRequestId: this.testRequestId(),
1172
+ recipients: parseRecipients(value.recipients),
1173
+ subject: value.subject?.trim() ?? '',
1174
+ body: value.body ?? '',
1175
+ isHtml: value.isHtml,
1176
+ })
1177
+ .subscribe({
1178
+ next: (result) => {
1179
+ this.result.set(result);
1180
+ this.timedOut.set(false);
1181
+ },
1182
+ error: (error) => {
1183
+ if (isAmbiguousTimeout(error) ||
1184
+ this.facade.errorAction(error) === 'poll_status') {
1185
+ this.startPolling();
1186
+ return;
1187
+ }
1188
+ this.facade.showError(error);
1189
+ },
1190
+ });
1191
+ }
1192
+ retrieve() {
1193
+ if (this.polling())
1194
+ return;
1195
+ this.retrieveStoredResult(false);
1196
+ }
1197
+ retrieveStoredResult(fromPoll) {
1198
+ this.facade
1199
+ .retrieveConnectionTest(this.connectionId(), this.testRequestId())
1200
+ .subscribe({
1201
+ next: (stored) => {
1202
+ if (stored.result) {
1203
+ this.result.set(stored.result);
1204
+ this.timedOut.set(false);
1205
+ this.polling.set(false);
1206
+ return;
1207
+ }
1208
+ if (fromPoll && isPendingRequestState(stored.requestState)) {
1209
+ this.schedulePoll();
1210
+ }
1211
+ else {
1212
+ this.polling.set(false);
1213
+ this.timedOut.set(true);
1214
+ }
1215
+ },
1216
+ error: (error) => {
1217
+ if (fromPoll && this.facade.errorAction(error) === 'poll_status') {
1218
+ this.schedulePoll();
1219
+ return;
1220
+ }
1221
+ this.polling.set(false);
1222
+ this.timedOut.set(true);
1223
+ this.facade.showError(error);
1224
+ },
1225
+ });
1226
+ }
1227
+ startPolling() {
1228
+ if (this.polling())
1229
+ return;
1230
+ this.timedOut.set(true);
1231
+ this.polling.set(true);
1232
+ this.pollIndex = 0;
1233
+ this.schedulePoll();
1234
+ }
1235
+ schedulePoll() {
1236
+ const delay = POLL_DELAYS_MS[this.pollIndex++];
1237
+ if (delay === undefined) {
1238
+ this.polling.set(false);
1239
+ return;
1240
+ }
1241
+ timer(delay)
1242
+ .pipe(takeUntilDestroyed(this.destroyRef))
1243
+ .subscribe(() => this.retrieveStoredResult(true));
1244
+ }
1245
+ close() {
1246
+ this.ref.close(this.result());
1247
+ }
1248
+ t(key) {
1249
+ return this.transloco.translate(`email-delivery.${key}`);
1250
+ }
1251
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
1252
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailControlledTestDialog, isStandalone: true, selector: "mt-email-controlled-test-dialog", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null }, connectionName: { classPropertyName: "connectionName", publicName: "connectionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (testResult.outcome === \"Accepted\") {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ 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: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "forcedDisabledFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys", "externalValues", "serverFieldErrors"], outputs: ["runtimeMessagesChange"] }, { 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: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1253
+ }
1254
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, decorators: [{
1255
+ type: Component,
1256
+ args: [{ selector: 'mt-email-controlled-test-dialog', imports: [
1257
+ ReactiveFormsModule,
1258
+ TranslocoDirective,
1259
+ DynamicForm,
1260
+ Button,
1261
+ EmailDeliveryStatus,
1262
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (testResult.outcome === \"Accepted\") {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n" }]
1263
+ }], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }], connectionName: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionName", required: true }] }] } });
1264
+ const POLL_DELAYS_MS = [1_000, 2_000, 4_000, 5_000, 5_000, 5_000];
1265
+ function isPendingRequestState(value) {
1266
+ return !['Completed', 'Failed', 'Cancelled', 'Canceled'].includes(value);
1267
+ }
1268
+ function parseRecipients(value) {
1269
+ return Array.from(new Set((value ?? '')
1270
+ .split(/[\s,;]+/)
1271
+ .map((item) => item.trim())
1272
+ .filter(Boolean)));
1273
+ }
1274
+ function validRecipients(value) {
1275
+ const recipients = parseRecipients(String(value ?? ''));
1276
+ return (recipients.length >= 1 &&
1277
+ recipients.length <= 100 &&
1278
+ recipients.every((recipient) => recipient.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)));
1279
+ }
1280
+ function isAmbiguousTimeout(error) {
1281
+ return (error instanceof HttpErrorResponse &&
1282
+ (error.status === 0 || error.status === 408 || error.status === 504));
1283
+ }
1284
+ function createId() {
1285
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
1286
+ return crypto.randomUUID();
1287
+ }
1288
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => {
1289
+ const random = Math.floor(Math.random() * 16);
1290
+ const value = character === 'x' ? random : (random & 0x3) | 0x8;
1291
+ return value.toString(16);
1292
+ });
1293
+ }
1294
+
1295
+ function getApplicableProviderFields(provider, authenticationMode) {
1296
+ if (!provider || !authenticationMode) {
1297
+ return [];
1298
+ }
1299
+ return provider.fields.filter((field) => field.authenticationModes.length === 0 ||
1300
+ field.authenticationModes.includes(authenticationMode));
1301
+ }
1302
+ function getApplicableProviderActions(provider, authenticationMode, authorizationModel) {
1303
+ if (!provider || !authenticationMode) {
1304
+ return [];
1305
+ }
1306
+ return (provider.actions ?? []).filter((action) => {
1307
+ if (!action.isExecutable) {
1308
+ return false;
1309
+ }
1310
+ if (action.authenticationModes.length > 0 &&
1311
+ !action.authenticationModes.includes(authenticationMode)) {
1312
+ return false;
1313
+ }
1314
+ return (!action.authorizationModels?.length ||
1315
+ (!!authorizationModel &&
1316
+ action.authorizationModels.includes(authorizationModel)));
1317
+ });
1318
+ }
1319
+ function parseProviderCapabilities(value) {
1320
+ return value
1321
+ .split(',')
1322
+ .map((item) => item.trim())
1323
+ .filter(Boolean);
1324
+ }
1325
+ function providerFieldControlKey(index) {
1326
+ return `providerField${index}`;
1327
+ }
1328
+ function credentialOperationControlKey(index) {
1329
+ return `credentialOperation${index}`;
1330
+ }
1331
+ function credentialStorageControlKey(index) {
1332
+ return `credentialStorage${index}`;
1333
+ }
1334
+ function credentialValueControlKey(index) {
1335
+ return `credentialValue${index}`;
1336
+ }
1337
+ function credentialReferenceControlKey(index) {
1338
+ return `credentialReference${index}`;
1339
+ }
1340
+ function buildConnectionRequest(value, provider, existingConcurrencyToken) {
1341
+ const mode = requiredTrimmed(value.authenticationMode);
1342
+ const publicConfiguration = {};
1343
+ const credentials = [];
1344
+ provider.fields.forEach((field, index) => {
1345
+ if (field.authenticationModes.length > 0 &&
1346
+ !field.authenticationModes.includes(mode)) {
1347
+ return;
1348
+ }
1349
+ if (field.isSecret) {
1350
+ const operation = value[credentialOperationControlKey(index)];
1351
+ if (!operation || operation === 'omitted') {
1352
+ return;
1353
+ }
1354
+ const storageKind = (value[credentialStorageControlKey(index)] ??
1355
+ 'protected-value');
1356
+ credentials.push({
1357
+ key: field.key,
1358
+ operation,
1359
+ storageKind,
1360
+ value: operation === 'replace' && storageKind === 'protected-value'
1361
+ ? optionalTrimmed(value[credentialValueControlKey(index)])
1362
+ : null,
1363
+ externalReference: operation === 'replace' && storageKind === 'external-reference'
1364
+ ? optionalTrimmed(value[credentialReferenceControlKey(index)])
1365
+ : null,
1366
+ });
1367
+ return;
1368
+ }
1369
+ const fieldValue = value[providerFieldControlKey(index)];
1370
+ if (fieldValue === null || fieldValue === undefined || fieldValue === '') {
1371
+ return;
1372
+ }
1373
+ publicConfiguration[field.key] =
1374
+ typeof fieldValue === 'boolean' ? String(fieldValue) : String(fieldValue);
1375
+ });
1376
+ const request = {
1377
+ name: requiredTrimmed(value.name),
1378
+ providerKey: provider.key,
1379
+ authenticationMode: mode,
1380
+ senderAddress: requiredTrimmed(value.senderAddress),
1381
+ senderDisplayName: optionalTrimmed(value.senderDisplayName),
1382
+ publicConfiguration,
1383
+ credentials,
1384
+ };
1385
+ return existingConcurrencyToken
1386
+ ? { ...request, concurrencyToken: existingConcurrencyToken }
1387
+ : request;
1388
+ }
1389
+ function requiredTrimmed(value) {
1390
+ return String(value ?? '').trim();
1391
+ }
1392
+ function optionalTrimmed(value) {
1393
+ const normalized = String(value ?? '').trim();
1394
+ return normalized || null;
1395
+ }
1396
+
1397
+ class EmailConnectionEditor {
1398
+ editorConnectionId = input(null, ...(ngDevMode ? [{ debugName: "editorConnectionId" }] : /* istanbul ignore next */ []));
1399
+ ref = inject(ModalRef);
1400
+ transloco = inject(TranslocoService);
1401
+ modal = inject(ModalService);
1402
+ facade = inject(EmailDeliveryFacade);
1403
+ connectionId = signal(null, ...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
1404
+ existingDetails = signal(null, ...(ngDevMode ? [{ debugName: "existingDetails" }] : /* istanbul ignore next */ []));
1405
+ editorLoading = signal(true, ...(ngDevMode ? [{ debugName: "editorLoading" }] : /* istanbul ignore next */ []));
1406
+ formGeneration = signal(0, ...(ngDevMode ? [{ debugName: "formGeneration" }] : /* istanbul ignore next */ []));
1407
+ formValue = signal(emptyEditorValue(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
1408
+ form = new FormControl(emptyEditorValue(), { nonNullable: true });
1409
+ formStatus = toSignal(this.form.statusChanges, {
1410
+ initialValue: this.form.status,
1411
+ });
1412
+ previousProviderKey = null;
1413
+ previousAuthenticationMode = null;
1414
+ structureSignature = '';
1415
+ isEdit = computed(() => !!this.connectionId(), ...(ngDevMode ? [{ debugName: "isEdit" }] : /* istanbul ignore next */ []));
1416
+ selectedProvider = computed(() => this.facade.providerByKey(this.formValue().providerKey), ...(ngDevMode ? [{ debugName: "selectedProvider" }] : /* istanbul ignore next */ []));
1417
+ selectedMode = computed(() => this.formValue().authenticationMode, ...(ngDevMode ? [{ debugName: "selectedMode" }] : /* istanbul ignore next */ []));
1418
+ applicableFields = computed(() => getApplicableProviderFields(this.selectedProvider(), this.selectedMode()), ...(ngDevMode ? [{ debugName: "applicableFields" }] : /* istanbul ignore next */ []));
1419
+ canSave = computed(() => !this.editorLoading() &&
1420
+ this.formStatus() === 'VALID' &&
1421
+ !this.facade.mutationLoading(), ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
1422
+ formConfig = computed(() => ({
1423
+ sections: [
1424
+ {
1425
+ key: 'basics',
1426
+ label: this.t('editor.basics'),
1427
+ type: 'header',
1428
+ columns: 12,
1429
+ fields: this.buildBasicFields(),
1430
+ },
1431
+ {
1432
+ key: 'configuration',
1433
+ label: this.t('editor.configuration'),
1434
+ type: 'header',
1435
+ columns: 12,
1436
+ fields: this.buildConfigurationFields(),
1437
+ },
1438
+ {
1439
+ key: 'credentials',
1440
+ label: this.t('editor.credentials'),
1441
+ type: 'header',
1442
+ columns: 12,
1443
+ fields: this.buildCredentialFields(),
1444
+ },
1445
+ ],
1446
+ }), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
1447
+ constructor() {
1448
+ this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
1449
+ this.handleFormValue(value);
1450
+ });
1451
+ }
1452
+ ngOnInit() {
1453
+ const id = this.editorConnectionId();
1454
+ this.connectionId.set(id);
1455
+ const catalog$ = this.facade.providers().length
1456
+ ? of(undefined)
1457
+ : this.facade.loadCatalog().pipe(map(() => undefined));
1458
+ catalog$
1459
+ .pipe(switchMap(() => (id ? this.facade.loadConnection(id) : of(null))), finalize(() => this.editorLoading.set(false)))
1460
+ .subscribe({
1461
+ next: (details) => {
1462
+ this.existingDetails.set(details);
1463
+ this.seedForm(details);
1464
+ },
1465
+ error: (error) => this.facade.showError(error),
1466
+ });
1467
+ }
1468
+ ngOnDestroy() {
1469
+ this.clearSecretControls();
1470
+ }
1471
+ save() {
1472
+ if (!this.canSave()) {
1473
+ this.form.markAllAsTouched();
1474
+ return;
1475
+ }
1476
+ const provider = this.selectedProvider();
1477
+ if (!provider)
1478
+ return;
1479
+ const id = this.connectionId();
1480
+ const request = buildConnectionRequest(this.form.getRawValue(), provider, this.existingDetails()?.connection.concurrencyToken);
1481
+ const operation = id
1482
+ ? this.facade.updateConnection(id, request)
1483
+ : this.facade.createConnection(request);
1484
+ operation.pipe(finalize(() => this.clearSecretControls())).subscribe({
1485
+ next: () => {
1486
+ this.ref.close(true);
1487
+ },
1488
+ error: (error) => this.handleSaveError(error),
1489
+ });
1490
+ }
1491
+ cancel() {
1492
+ this.clearSecretControls();
1493
+ this.ref.close(false);
1494
+ }
1495
+ seedForm(details) {
1496
+ const provider = details
1497
+ ? this.facade.providerByKey(details.connection.providerKey)
1498
+ : (this.facade.providers()[0] ?? null);
1499
+ const mode = details
1500
+ ? details.connection.authenticationMode
1501
+ : (provider?.authenticationModes[0]?.key ?? null);
1502
+ const value = {
1503
+ name: details?.connection.name ?? null,
1504
+ providerKey: provider?.key ?? null,
1505
+ authenticationMode: mode,
1506
+ senderAddress: details?.connection.senderAddress ?? null,
1507
+ senderDisplayName: details?.connection.senderDisplayName ?? null,
1508
+ };
1509
+ provider?.fields.forEach((field, index) => {
1510
+ if (!isApplicable(field, mode))
1511
+ return;
1512
+ if (field.isSecret) {
1513
+ const status = details?.credentials.find((credential) => credential.key === field.key);
1514
+ value[credentialOperationControlKey(index)] = details
1515
+ ? 'omitted'
1516
+ : 'replace';
1517
+ value[credentialStorageControlKey(index)] =
1518
+ status?.storageKind === 'ExternalReference'
1519
+ ? 'external-reference'
1520
+ : 'protected-value';
1521
+ value[credentialValueControlKey(index)] = null;
1522
+ value[credentialReferenceControlKey(index)] = null;
1523
+ return;
1524
+ }
1525
+ const rawValue = details?.connection.publicConfiguration[field.key] ??
1526
+ field.defaultValue;
1527
+ value[providerFieldControlKey(index)] = toEditorFieldValue(field, rawValue);
1528
+ });
1529
+ this.previousProviderKey = provider?.key ?? null;
1530
+ this.previousAuthenticationMode = mode;
1531
+ this.formValue.set(value);
1532
+ this.form.setValue(value, { emitEvent: false });
1533
+ this.structureSignature = this.getStructureSignature(value, provider);
1534
+ this.formGeneration.update((generation) => generation + 1);
1535
+ }
1536
+ handleFormValue(value) {
1537
+ let provider = this.facade.providerByKey(value.providerKey);
1538
+ const providerChanged = value.providerKey !== this.previousProviderKey;
1539
+ let mode = value.authenticationMode;
1540
+ if (provider &&
1541
+ !provider.authenticationModes.some((item) => item.key === mode)) {
1542
+ mode = provider.authenticationModes[0]?.key ?? null;
1543
+ }
1544
+ const modeChanged = mode !== this.previousAuthenticationMode;
1545
+ if (providerChanged || modeChanged) {
1546
+ const reset = this.resetProviderValues(value, provider, mode);
1547
+ this.previousProviderKey = provider?.key ?? null;
1548
+ this.previousAuthenticationMode = mode;
1549
+ this.formValue.set(reset);
1550
+ this.form.setValue(reset, { emitEvent: false });
1551
+ this.structureSignature = this.getStructureSignature(reset, provider);
1552
+ this.formGeneration.update((generation) => generation + 1);
1553
+ return;
1554
+ }
1555
+ provider = this.facade.providerByKey(value.providerKey);
1556
+ this.formValue.set(value);
1557
+ const signature = this.getStructureSignature(value, provider);
1558
+ if (signature !== this.structureSignature) {
1559
+ this.structureSignature = signature;
1560
+ this.formGeneration.update((generation) => generation + 1);
1561
+ }
1562
+ }
1563
+ resetProviderValues(value, provider, mode) {
1564
+ const reset = {
1565
+ name: value.name,
1566
+ providerKey: provider?.key ?? null,
1567
+ authenticationMode: mode,
1568
+ senderAddress: value.senderAddress,
1569
+ senderDisplayName: value.senderDisplayName,
1570
+ };
1571
+ provider?.fields.forEach((field, index) => {
1572
+ if (!isApplicable(field, mode))
1573
+ return;
1574
+ if (field.isSecret) {
1575
+ reset[credentialOperationControlKey(index)] = 'replace';
1576
+ reset[credentialStorageControlKey(index)] = 'protected-value';
1577
+ reset[credentialValueControlKey(index)] = null;
1578
+ reset[credentialReferenceControlKey(index)] = null;
1579
+ }
1580
+ else {
1581
+ reset[providerFieldControlKey(index)] = toEditorFieldValue(field, field.defaultValue);
1582
+ }
1583
+ });
1584
+ return reset;
1585
+ }
1586
+ buildBasicFields() {
1587
+ const provider = this.selectedProvider();
1588
+ return [
1589
+ new TextFieldConfig({
1590
+ key: 'name',
1591
+ label: this.t('connection.name'),
1592
+ colSpan: 12,
1593
+ validators: [
1594
+ ValidatorConfig.required(),
1595
+ ValidatorConfig.maxLength(150),
1596
+ ],
1597
+ }),
1598
+ new SelectFieldConfig({
1599
+ key: 'providerKey',
1600
+ label: this.t('connection.provider'),
1601
+ options: this.facade.providers().map((item) => ({
1602
+ label: item.displayName,
1603
+ value: item.key,
1604
+ })),
1605
+ optionLabel: 'label',
1606
+ optionValue: 'value',
1607
+ colSpan: 6,
1608
+ validators: [ValidatorConfig.required()],
1609
+ }),
1610
+ new SelectFieldConfig({
1611
+ key: 'authenticationMode',
1612
+ label: this.t('connection.authentication-mode'),
1613
+ options: provider?.authenticationModes.map((item) => ({
1614
+ label: item.displayName,
1615
+ value: item.key,
1616
+ })) ?? [],
1617
+ optionLabel: 'label',
1618
+ optionValue: 'value',
1619
+ colSpan: 6,
1620
+ validators: [ValidatorConfig.required()],
1621
+ }),
1622
+ new TextFieldConfig({
1623
+ key: 'senderAddress',
1624
+ label: this.t('connection.sender-address'),
1625
+ inputType: 'email',
1626
+ colSpan: 6,
1627
+ validators: [ValidatorConfig.required(), ValidatorConfig.email()],
1628
+ }),
1629
+ new TextFieldConfig({
1630
+ key: 'senderDisplayName',
1631
+ label: this.t('connection.sender-display-name'),
1632
+ colSpan: 6,
1633
+ validators: [ValidatorConfig.maxLength(200)],
1634
+ }),
1635
+ ];
1636
+ }
1637
+ buildConfigurationFields() {
1638
+ const provider = this.selectedProvider();
1639
+ if (!provider)
1640
+ return [];
1641
+ return provider.fields.flatMap((field, index) => {
1642
+ if (field.isSecret || !isApplicable(field, this.selectedMode())) {
1643
+ return [];
1644
+ }
1645
+ return [this.toProviderFieldConfig(field, index)];
1646
+ });
1647
+ }
1648
+ buildCredentialFields() {
1649
+ const provider = this.selectedProvider();
1650
+ if (!provider)
1651
+ return [];
1652
+ return provider.fields.flatMap((field, index) => {
1653
+ if (!field.isSecret || !isApplicable(field, this.selectedMode())) {
1654
+ return [];
1655
+ }
1656
+ const operationKey = credentialOperationControlKey(index);
1657
+ const storageKey = credentialStorageControlKey(index);
1658
+ const operation = (this.formValue()[operationKey] ??
1659
+ (this.isEdit() ? 'omitted' : 'replace'));
1660
+ const storageKind = (this.formValue()[storageKey] ??
1661
+ 'protected-value');
1662
+ const operationOptions = [
1663
+ ...(this.isEdit()
1664
+ ? [{ label: this.t('credential.keep'), value: 'omitted' }]
1665
+ : []),
1666
+ { label: this.t('credential.replace'), value: 'replace' },
1667
+ ...(!field.required && this.isEdit()
1668
+ ? [{ label: this.t('credential.clear'), value: 'clear' }]
1669
+ : []),
1670
+ ];
1671
+ const fields = [
1672
+ new SelectFieldConfig({
1673
+ key: operationKey,
1674
+ label: `${field.displayName} — ${this.t('credential.operation')}`,
1675
+ options: operationOptions,
1676
+ optionLabel: 'label',
1677
+ optionValue: 'value',
1678
+ colSpan: 6,
1679
+ validators: [ValidatorConfig.required()],
1680
+ }),
1681
+ ];
1682
+ if (operation !== 'replace')
1683
+ return fields;
1684
+ fields.push(new RadioButtonFieldConfig({
1685
+ key: storageKey,
1686
+ label: this.t('credential.storage-choice'),
1687
+ options: [
1688
+ {
1689
+ label: this.t('credential.protected-value'),
1690
+ value: 'protected-value',
1691
+ },
1692
+ {
1693
+ label: this.t('credential.external-reference'),
1694
+ value: 'external-reference',
1695
+ },
1696
+ ],
1697
+ optionLabel: 'label',
1698
+ optionValue: 'value',
1699
+ orientation: 'vertical',
1700
+ colSpan: 6,
1701
+ validators: [ValidatorConfig.required()],
1702
+ }));
1703
+ fields.push(storageKind === 'external-reference'
1704
+ ? new TextFieldConfig({
1705
+ key: credentialReferenceControlKey(index),
1706
+ label: this.t('credential.external-reference'),
1707
+ hint: this.t('credential.external-reference-hint'),
1708
+ colSpan: 12,
1709
+ validators: [
1710
+ ValidatorConfig.required(),
1711
+ ValidatorConfig.maxLength(500),
1712
+ ValidatorConfig.pattern('^EmailSecrets:[A-Za-z0-9_.-]{1,100}$'),
1713
+ ],
1714
+ })
1715
+ : new TextFieldConfig({
1716
+ key: credentialValueControlKey(index),
1717
+ label: field.displayName,
1718
+ inputType: 'password',
1719
+ hint: this.t('credential.protected-value-hint'),
1720
+ colSpan: 12,
1721
+ validators: [
1722
+ ValidatorConfig.required(),
1723
+ ...(field.maximumLength
1724
+ ? [ValidatorConfig.maxLength(field.maximumLength)]
1725
+ : []),
1726
+ ],
1727
+ }));
1728
+ return fields;
1729
+ });
1730
+ }
1731
+ toProviderFieldConfig(field, index) {
1732
+ const config = {
1733
+ key: providerFieldControlKey(index),
1734
+ label: field.displayName,
1735
+ colSpan: 6,
1736
+ readonly: !!this.existingDetails()?.authorizationState &&
1737
+ !field.editableAfterAuthorization,
1738
+ validators: providerFieldValidators(field, this.t.bind(this)),
1739
+ };
1740
+ if (field.dataType === 'Boolean') {
1741
+ return new ToggleFieldConfig(config);
1742
+ }
1743
+ if (field.dataType === 'Integer' || field.dataType === 'Port') {
1744
+ return new NumberFieldConfig({
1745
+ ...config,
1746
+ min: field.dataType === 'Port' ? 1 : undefined,
1747
+ max: field.dataType === 'Port' ? 65_535 : undefined,
1748
+ useGrouping: false,
1749
+ });
1750
+ }
1751
+ if (field.dataType === 'Choice') {
1752
+ return new SelectFieldConfig({
1753
+ ...config,
1754
+ options: (field.allowedValues ?? []).map((value) => ({
1755
+ label: value,
1756
+ value,
1757
+ })),
1758
+ optionLabel: 'label',
1759
+ optionValue: 'value',
1760
+ });
1761
+ }
1762
+ return new TextFieldConfig({
1763
+ ...config,
1764
+ inputType: field.dataType === 'Email' ? 'email' : 'text',
1765
+ });
1766
+ }
1767
+ getStructureSignature(value, provider) {
1768
+ const credentials = provider?.fields.flatMap((field, index) => field.isSecret && isApplicable(field, value.authenticationMode)
1769
+ ? [
1770
+ `${value[credentialOperationControlKey(index)] ?? ''}:${value[credentialStorageControlKey(index)] ?? ''}`,
1771
+ ]
1772
+ : []);
1773
+ return `${value.providerKey ?? ''}|${value.authenticationMode ?? ''}|${credentials?.join('|') ?? ''}`;
1774
+ }
1775
+ clearSecretControls() {
1776
+ const provider = this.selectedProvider();
1777
+ if (!provider)
1778
+ return;
1779
+ const value = { ...this.form.getRawValue() };
1780
+ provider.fields.forEach((field, index) => {
1781
+ if (!field.isSecret)
1782
+ return;
1783
+ value[credentialValueControlKey(index)] = null;
1784
+ value[credentialReferenceControlKey(index)] = null;
1785
+ });
1786
+ this.formValue.set(value);
1787
+ this.form.setValue(value, { emitEvent: false });
1788
+ this.formGeneration.update((generation) => generation + 1);
1789
+ }
1790
+ handleSaveError(error) {
1791
+ this.facade.showError(error);
1792
+ const id = this.connectionId();
1793
+ if (!(error instanceof HttpErrorResponse) || error.status !== 409 || !id) {
1794
+ return;
1795
+ }
1796
+ this.editorLoading.set(true);
1797
+ this.facade
1798
+ .loadConnection(id)
1799
+ .pipe(finalize(() => this.editorLoading.set(false)))
1800
+ .subscribe({
1801
+ next: (details) => {
1802
+ this.existingDetails.set(details);
1803
+ this.seedForm(details);
1804
+ },
1805
+ error: (loadError) => this.facade.showError(loadError),
1806
+ });
1807
+ }
1808
+ t(key) {
1809
+ return this.transloco.translate(`email-delivery.${key}`);
1810
+ }
1811
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1812
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionEditor, isStandalone: true, selector: "mt-email-connection-editor", inputs: { editorConnectionId: { classPropertyName: "editorConnectionId", publicName: "editorConnectionId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"editor.description\") }}\r\n </p>\r\n\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-3\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul class=\"mb-0 mt-2 list-disc space-y-1 ps-5 text-sm text-amber-900\">\r\n @for (warning of provider.warnings; track warning) {\r\n <li>{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @if (editorLoading()) {\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n } @else {\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n }\r\n\r\n @if (applicableFields().length === 0 && !editorLoading()) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ 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: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "forcedDisabledFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys", "externalValues", "serverFieldErrors"], outputs: ["runtimeMessagesChange"] }, { 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 });
1813
+ }
1814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, decorators: [{
1815
+ type: Component,
1816
+ args: [{ selector: 'mt-email-connection-editor', imports: [ReactiveFormsModule, TranslocoDirective, DynamicForm, Button], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"editor.description\") }}\r\n </p>\r\n\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-3\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul class=\"mb-0 mt-2 list-disc space-y-1 ps-5 text-sm text-amber-900\">\r\n @for (warning of provider.warnings; track warning) {\r\n <li>{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @if (editorLoading()) {\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n } @else {\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n }\r\n\r\n @if (applicableFields().length === 0 && !editorLoading()) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
1817
+ }], ctorParameters: () => [], propDecorators: { editorConnectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorConnectionId", required: false }] }] } });
1818
+ function emptyEditorValue() {
1819
+ return {
1820
+ name: null,
1821
+ providerKey: null,
1822
+ authenticationMode: null,
1823
+ senderAddress: null,
1824
+ senderDisplayName: null,
1825
+ };
1826
+ }
1827
+ function isApplicable(field, mode) {
1828
+ return (!!mode &&
1829
+ (field.authenticationModes.length === 0 ||
1830
+ field.authenticationModes.includes(mode)));
1831
+ }
1832
+ function toEditorFieldValue(field, value) {
1833
+ if (value === null || value === undefined || value === '')
1834
+ return null;
1835
+ if (field.dataType === 'Boolean')
1836
+ return value.toLowerCase() === 'true';
1837
+ if (field.dataType === 'Integer' || field.dataType === 'Port') {
1838
+ const numberValue = Number(value);
1839
+ return Number.isFinite(numberValue) ? numberValue : null;
1840
+ }
1841
+ return value;
1842
+ }
1843
+ function providerFieldValidators(field, translate) {
1844
+ const validators = [];
1845
+ if (field.required)
1846
+ validators.push(ValidatorConfig.required());
1847
+ if (field.maximumLength) {
1848
+ validators.push(ValidatorConfig.maxLength(field.maximumLength));
1849
+ }
1850
+ if (field.dataType === 'Email')
1851
+ validators.push(ValidatorConfig.email());
1852
+ if (field.dataType === 'Text') {
1853
+ validators.push(ValidatorConfig.pattern('^[^\\r\\n\\u0000]*$'));
1854
+ }
1855
+ if (field.dataType === 'Identifier') {
1856
+ validators.push(ValidatorConfig.pattern('^[A-Za-z0-9._-]+$'));
1857
+ }
1858
+ if (field.dataType === 'Host') {
1859
+ validators.push(ValidatorConfig.custom((value) => validHost(value), translate('validation.host')));
1860
+ }
1861
+ if (field.dataType === 'Url') {
1862
+ validators.push(ValidatorConfig.custom((value) => validHttpsUrl(value), translate('validation.https-url')));
1863
+ }
1864
+ if (field.dataType === 'HeaderName') {
1865
+ validators.push(ValidatorConfig.pattern('^X-[A-Za-z0-9-]+$'));
1866
+ }
1867
+ return validators;
1868
+ }
1869
+ function validHost(value) {
1870
+ const host = String(value ?? '')
1871
+ .trim()
1872
+ .toLowerCase();
1873
+ return (!!host &&
1874
+ host !== 'localhost' &&
1875
+ !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) &&
1876
+ /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,62}$/.test(host));
1877
+ }
1878
+ function validHttpsUrl(value) {
1879
+ try {
1880
+ const url = new URL(String(value ?? ''));
1881
+ return (url.protocol === 'https:' &&
1882
+ !url.username &&
1883
+ !url.password &&
1884
+ !url.hash &&
1885
+ url.hostname.toLowerCase() !== 'localhost');
1886
+ }
1887
+ catch {
1888
+ return false;
1889
+ }
1890
+ }
1891
+
1892
+ class EmailConnectionDetailsPage {
1893
+ connectionId = input.required(...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
1894
+ document = inject(DOCUMENT);
1895
+ ref = inject(ModalRef);
1896
+ confirmation = inject(ConfirmationService);
1897
+ transloco = inject(TranslocoService);
1898
+ modal = inject(ModalService);
1899
+ facade = inject(EmailDeliveryFacade);
1900
+ activeLanguage = toSignal(this.transloco.langChanges$, {
1901
+ initialValue: this.transloco.getActiveLang(),
1902
+ });
1903
+ actions = computed(() => {
1904
+ const details = this.facade.selectedConnection();
1905
+ if (!details || details.connection.id !== this.connectionId())
1906
+ return [];
1907
+ return getApplicableProviderActions(this.facade.providerByKey(details.connection.providerKey), details.connection.authenticationMode, details.connection.publicConfiguration['AuthorizationModel']);
1908
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
1909
+ provider = computed(() => {
1910
+ const details = this.facade.selectedConnection();
1911
+ return details
1912
+ ? this.facade.providerByKey(details.connection.providerKey)
1913
+ : null;
1914
+ }, ...(ngDevMode ? [{ debugName: "provider" }] : /* istanbul ignore next */ []));
1915
+ authenticationModeLabel = computed(() => {
1916
+ const details = this.facade.selectedConnection();
1917
+ if (!details)
1918
+ return '';
1919
+ return (this.provider()?.authenticationModes.find((mode) => mode.key === details.connection.authenticationMode)?.displayName ?? details.connection.authenticationMode);
1920
+ }, ...(ngDevMode ? [{ debugName: "authenticationModeLabel" }] : /* istanbul ignore next */ []));
1921
+ publicConfiguration = computed(() => {
1922
+ this.activeLanguage();
1923
+ const details = this.facade.selectedConnection();
1924
+ if (!details)
1925
+ return [];
1926
+ const fields = this.provider()?.fields ?? [];
1927
+ const fieldOrder = new Map(fields.map((field, index) => [field.key.toLowerCase(), index]));
1928
+ return Object.entries(details.connection.publicConfiguration)
1929
+ .filter(([, value]) => value !== '')
1930
+ .sort(([left], [right]) => (fieldOrder.get(left.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
1931
+ (fieldOrder.get(right.toLowerCase()) ?? Number.MAX_SAFE_INTEGER))
1932
+ .map(([key, value]) => {
1933
+ const field = fields.find((candidate) => candidate.key.toLowerCase() === key.toLowerCase());
1934
+ return {
1935
+ key,
1936
+ label: field?.displayName ?? humanizeIdentifier(key),
1937
+ value: this.configurationValueLabel(value, field),
1938
+ };
1939
+ });
1940
+ }, ...(ngDevMode ? [{ debugName: "publicConfiguration" }] : /* istanbul ignore next */ []));
1941
+ credentialItems = computed(() => {
1942
+ this.activeLanguage();
1943
+ const details = this.facade.selectedConnection();
1944
+ if (!details)
1945
+ return [];
1946
+ const fields = this.provider()?.fields ?? [];
1947
+ return details.credentials.map((credential) => ({
1948
+ key: credential.key,
1949
+ label: fields.find((field) => field.key.toLowerCase() === credential.key.toLowerCase())?.displayName ?? humanizeIdentifier(credential.key),
1950
+ configured: credential.configured,
1951
+ storageLabel: this.credentialStorageLabel(credential.storageKind),
1952
+ lastRotatedAtUtc: credential.lastRotatedAtUtc,
1953
+ }));
1954
+ }, ...(ngDevMode ? [{ debugName: "credentialItems" }] : /* istanbul ignore next */ []));
1955
+ actionMenuItems = computed(() => {
1956
+ this.activeLanguage();
1957
+ const details = this.facade.selectedConnection();
1958
+ if (!details || !this.facade.managementAvailable())
1959
+ return [];
1960
+ const items = this.interactiveActions().map((action) => ({
1961
+ id: action.key,
1962
+ label: action.displayName,
1963
+ icon: 'security.lock-unlocked-01',
1964
+ command: () => this.authorize(action),
1965
+ }));
1966
+ if (this.hasAction('validate')) {
1967
+ items.push({
1968
+ id: 'validate',
1969
+ label: this.t('validate'),
1970
+ icon: 'general.check-circle',
1971
+ command: () => this.validate(),
1972
+ });
1973
+ }
1974
+ if (this.hasAction('test-send')) {
1975
+ items.push({
1976
+ id: 'test-send',
1977
+ label: this.t('test-send'),
1978
+ icon: 'communication.send-01',
1979
+ command: () => this.test(),
1980
+ });
1981
+ }
1982
+ if (details.connection.state === 'Ready' &&
1983
+ !details.bindings.includes('notifications')) {
1984
+ items.push({
1985
+ id: 'bind',
1986
+ label: this.t('use-for-notifications'),
1987
+ icon: 'general.link-01',
1988
+ command: () => this.bind(),
1989
+ });
1990
+ }
1991
+ if (details.bindings.includes('notifications')) {
1992
+ items.push({
1993
+ id: 'unbind',
1994
+ label: this.t('stop-using'),
1995
+ icon: 'general.link-broken-01',
1996
+ severity: 'warn',
1997
+ command: () => this.unbind(),
1998
+ });
1999
+ }
2000
+ if (details.authorizationState) {
2001
+ items.push({
2002
+ id: 'revoke',
2003
+ label: this.t('revoke'),
2004
+ icon: 'security.lock-01',
2005
+ severity: 'warn',
2006
+ command: () => this.revoke(),
2007
+ });
2008
+ }
2009
+ if (details.bindings.length === 0) {
2010
+ items.push({ id: 'destructive-separator', separator: true }, {
2011
+ id: 'delete',
2012
+ label: this.t('delete'),
2013
+ icon: 'general.trash-01',
2014
+ severity: 'danger',
2015
+ command: () => this.delete(),
2016
+ });
2017
+ }
2018
+ return items;
2019
+ }, ...(ngDevMode ? [{ debugName: "actionMenuItems" }] : /* istanbul ignore next */ []));
2020
+ ngOnInit() {
2021
+ this.reload();
2022
+ }
2023
+ edit() {
2024
+ const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
2025
+ header: this.t('editor.edit-title'),
2026
+ styleClass: '!absolute !shadow-none !w-full !max-w-full',
2027
+ position: 'end',
2028
+ appendTo: 'page-content',
2029
+ dismissableMask: true,
2030
+ dismissible: true,
2031
+ inputValues: { editorConnectionId: this.connectionId() },
2032
+ });
2033
+ drawer.onClose.pipe(take(1)).subscribe((saved) => {
2034
+ if (saved)
2035
+ this.reload();
2036
+ });
2037
+ }
2038
+ validate() {
2039
+ this.facade.validateConnection(this.connectionId()).subscribe({
2040
+ error: (error) => this.facade.showError(error),
2041
+ });
2042
+ }
2043
+ test() {
2044
+ const details = this.facade.selectedConnection();
2045
+ if (!details)
2046
+ return;
2047
+ const dialog = this.modal.openModal(EmailControlledTestDialog, 'dialog', {
2048
+ header: this.t('test.title'),
2049
+ styleClass: '!w-[42rem] max-w-[96vw]',
2050
+ dismissible: true,
2051
+ inputValues: {
2052
+ connectionId: details.connection.id,
2053
+ connectionName: details.connection.name,
2054
+ },
2055
+ });
2056
+ dialog.onClose.pipe(take(1)).subscribe(() => this.reload());
2057
+ }
2058
+ authorize(action) {
2059
+ const kind = action.key === 'authorize' ? 'delegated' : 'admin-consent';
2060
+ this.facade.startAuthorization(this.connectionId(), kind).subscribe({
2061
+ next: (result) => this.document.location.assign(result.authorizationUrl),
2062
+ error: (error) => this.facade.showError(error),
2063
+ });
2064
+ }
2065
+ revoke() {
2066
+ const details = this.facade.selectedConnection();
2067
+ if (!details)
2068
+ return;
2069
+ this.confirmation.confirm({
2070
+ type: 'dialog',
2071
+ header: this.t('revoke'),
2072
+ message: this.t('confirm.revoke'),
2073
+ accept: () => this.facade.revokeAuthorization(this.connectionId()).subscribe({
2074
+ error: (error) => this.facade.showError(error),
2075
+ }),
2076
+ });
2077
+ }
2078
+ bind() {
2079
+ this.facade.bindConnection(this.connectionId()).subscribe({
2080
+ error: (error) => this.facade.showError(error),
2081
+ });
2082
+ }
2083
+ unbind() {
2084
+ this.confirmation.confirm({
2085
+ type: 'dialog',
2086
+ header: this.t('stop-using'),
2087
+ message: this.t('confirm.unbind'),
2088
+ accept: () => this.facade.unbindConnection(this.connectionId()).subscribe({
2089
+ error: (error) => this.facade.showError(error),
2090
+ }),
2091
+ });
2092
+ }
2093
+ delete() {
2094
+ const details = this.facade.selectedConnection();
2095
+ if (!details)
2096
+ return;
2097
+ this.confirmation.confirmDelete({
2098
+ type: 'dialog',
2099
+ message: this.t('confirm.delete'),
2100
+ accept: () => this.facade
2101
+ .deleteConnection(this.connectionId(), details.connection.concurrencyToken)
2102
+ .subscribe({
2103
+ next: () => this.ref.close(true),
2104
+ error: (error) => this.facade.showError(error),
2105
+ }),
2106
+ });
2107
+ }
2108
+ hasAction(key) {
2109
+ return this.actions().some((action) => action.key === key);
2110
+ }
2111
+ interactiveActions() {
2112
+ return this.actions().filter((action) => action.isInteractive);
2113
+ }
2114
+ close() {
2115
+ this.ref.close();
2116
+ }
2117
+ reload() {
2118
+ this.facade.loadConnection(this.connectionId()).subscribe({
2119
+ error: (error) => this.facade.showError(error),
2120
+ });
2121
+ }
2122
+ t(key) {
2123
+ return this.transloco.translate(`email-delivery.${key}`);
2124
+ }
2125
+ credentialStorageLabel(storageKind) {
2126
+ switch (storageKind.replace(/[-_\s]/g, '').toLowerCase()) {
2127
+ case 'externalreference':
2128
+ return this.t('credential.external-reference');
2129
+ case 'protectedvalue':
2130
+ return this.t('credential.protected-value');
2131
+ default:
2132
+ return humanizeIdentifier(storageKind);
2133
+ }
2134
+ }
2135
+ configurationValueLabel(value, field) {
2136
+ if (value.toLowerCase() === 'true')
2137
+ return this.t('yes');
2138
+ if (value.toLowerCase() === 'false')
2139
+ return this.t('no');
2140
+ return field?.dataType === 'Choice' ? humanizeIdentifier(value) : value;
2141
+ }
2142
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
2143
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionDetailsPage, isStandalone: true, selector: "mt-email-connection-details", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n }\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-lg border border-surface-200 bg-surface-50 p-4 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"text-sm text-muted-color\">{{ t(\"no-credentials\") }}</p>\r\n }\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\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: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Menu, selector: "mt-menu", inputs: ["model", "popup", "appendTo", "style", "styleClass", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex", "activeItemId"], outputs: ["activeItemIdChange", "onShow", "onHide", "onBlur", "onFocus", "onItemSelect"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2144
+ }
2145
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, decorators: [{
2146
+ type: Component,
2147
+ args: [{ selector: 'mt-email-connection-details', imports: [
2148
+ DatePipe,
2149
+ TranslocoDirective,
2150
+ Button,
2151
+ Card,
2152
+ EmptyState,
2153
+ Menu,
2154
+ EmailDeliveryStatus,
2155
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n }\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-lg border border-surface-200 bg-surface-50 p-4 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"text-sm text-muted-color\">{{ t(\"no-credentials\") }}</p>\r\n }\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
2156
+ }], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }] } });
2157
+ function humanizeIdentifier(value) {
2158
+ return value
2159
+ .replace(/[._-]+/g, ' ')
2160
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
2161
+ .replace(/\s+/g, ' ')
2162
+ .trim()
2163
+ .replace(/^./, (character) => character.toUpperCase())
2164
+ .replace(/\bSmtp\b/g, 'SMTP')
2165
+ .replace(/\bTls\b/g, 'TLS')
2166
+ .replace(/\bOauth2?\b/gi, 'OAuth2')
2167
+ .replace(/\bHttp\b/g, 'HTTP')
2168
+ .replace(/\bApi\b/g, 'API');
2169
+ }
2170
+
2171
+ class EmailDeliveryConnections {
2172
+ transloco = inject(TranslocoService);
2173
+ modal = inject(ModalService);
2174
+ confirmationService = inject(ConfirmationService);
2175
+ facade = inject(EmailDeliveryFacade);
2176
+ activeLanguage = toSignal(this.transloco.langChanges$, {
2177
+ initialValue: this.transloco.getActiveLang(),
2178
+ });
2179
+ canConfigure = this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel);
2180
+ rows = computed(() => this.facade.connections().map((connection) => ({
2181
+ ...connection,
2182
+ providerLabel: this.facade.providerByKey(connection.providerKey)?.displayName ??
2183
+ connection.providerKey,
2184
+ usedForNotifications: connection.bindings.includes('notifications'),
2185
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
2186
+ columns = computed(() => {
2187
+ this.activeLanguage();
2188
+ return [
2189
+ {
2190
+ key: 'name',
2191
+ label: this.t('connection.name'),
2192
+ sortable: true,
2193
+ filterConfig: { type: 'text' },
2194
+ },
2195
+ {
2196
+ key: 'providerLabel',
2197
+ label: this.t('connection.provider'),
2198
+ filterConfig: {
2199
+ type: 'select',
2200
+ options: this.facade.providers().map((provider) => ({
2201
+ label: provider.displayName,
2202
+ value: provider.displayName,
2203
+ })),
2204
+ },
2205
+ },
2206
+ {
2207
+ key: 'senderAddress',
2208
+ label: this.t('connection.sender'),
2209
+ filterConfig: { type: 'text' },
2210
+ },
2211
+ {
2212
+ key: 'state',
2213
+ label: this.t('connection.state'),
2214
+ type: 'status',
2215
+ filterConfig: {
2216
+ type: 'select',
2217
+ options: connectionStates.map((state) => ({
2218
+ label: this.t(`status.connection.${toKebabCase(state)}`),
2219
+ value: state,
2220
+ })),
2221
+ },
2222
+ statusMap: getEmailDeliveryStatusMap('connection', this.transloco.translate.bind(this.transloco)),
2223
+ },
2224
+ {
2225
+ key: 'usedForNotifications',
2226
+ label: this.t('used-for-notifications'),
2227
+ type: 'boolean',
2228
+ readonly: true,
2229
+ filterConfig: { type: 'boolean' },
2230
+ },
2231
+ {
2232
+ key: 'lastValidatedAtUtc',
2233
+ label: this.t('connection.last-validation'),
2234
+ type: 'dateTime',
2235
+ },
2236
+ ];
2237
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
2238
+ rowActions = computed(() => {
2239
+ this.activeLanguage();
2240
+ return [
2241
+ {
2242
+ icon: 'general.eye',
2243
+ label: this.t('details'),
2244
+ action: (row) => this.openDetails(row),
2245
+ },
2246
+ {
2247
+ icon: 'general.edit-01',
2248
+ label: this.t('edit'),
2249
+ hidden: () => !this.canMutate(),
2250
+ action: (row) => this.openEdit(row),
2251
+ },
2252
+ {
2253
+ icon: 'general.check-circle',
2254
+ label: this.t('validate'),
2255
+ hidden: (row) => !this.canMutate() ||
2256
+ !this.hasAction(row, 'validate'),
2257
+ loading: () => this.facade.mutationLoading() === 'validate',
2258
+ action: (row) => this.validate(row),
2259
+ },
2260
+ {
2261
+ icon: 'communication.send-01',
2262
+ label: this.t('test-send'),
2263
+ hidden: (row) => !this.canMutate() ||
2264
+ !this.hasAction(row, 'test-send'),
2265
+ action: (row) => this.openTest(row),
2266
+ },
2267
+ {
2268
+ icon: 'general.link-01',
2269
+ label: this.t('bind'),
2270
+ hidden: (row) => {
2271
+ const connection = row;
2272
+ return (!this.canMutate() ||
2273
+ connection.state !== 'Ready' ||
2274
+ connection.bindings.includes('notifications'));
2275
+ },
2276
+ action: (row) => this.bind(row),
2277
+ },
2278
+ {
2279
+ icon: 'general.link-broken-01',
2280
+ label: this.t('unbind'),
2281
+ color: 'warn',
2282
+ hidden: (row) => !this.canMutate() ||
2283
+ !row.bindings.includes('notifications'),
2284
+ action: (row) => this.confirmUnbind(row),
2285
+ },
2286
+ {
2287
+ icon: 'general.trash-01',
2288
+ label: this.t('delete'),
2289
+ color: 'danger',
2290
+ hidden: (row) => !this.canMutate() ||
2291
+ row.bindings.length > 0,
2292
+ action: (row) => this.confirmDelete(row),
2293
+ },
2294
+ ];
2295
+ }, ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
2296
+ createConnection() {
2297
+ this.openEditor(null);
2298
+ }
2299
+ openDetails(connection) {
2300
+ const drawer = this.modal.openModal(EmailConnectionDetailsPage, 'drawer', {
2301
+ header: this.t('connection-details'),
2302
+ styleClass: '!absolute !shadow-none !w-full !max-w-full',
2303
+ position: 'end',
2304
+ appendTo: 'page-content',
2305
+ dismissableMask: true,
2306
+ dismissible: true,
2307
+ inputValues: { connectionId: connection.id },
2308
+ });
2309
+ drawer.onClose.subscribe(() => {
2310
+ this.facade.loadConnections().subscribe({
2311
+ error: (error) => this.facade.showError(error),
2312
+ });
2313
+ });
2314
+ }
2315
+ openEdit(connection) {
2316
+ this.openEditor(connection.id);
2317
+ }
2318
+ validate(connection) {
2319
+ this.facade.validateConnection(connection.id).subscribe({
2320
+ error: (error) => this.facade.showError(error),
2321
+ });
2322
+ }
2323
+ openTest(connection) {
2324
+ this.modal.openModal(EmailControlledTestDialog, 'dialog', {
2325
+ header: this.t('test.title'),
2326
+ styleClass: '!w-[42rem] max-w-[96vw]',
2327
+ dismissible: true,
2328
+ inputValues: {
2329
+ connectionId: connection.id,
2330
+ connectionName: connection.name,
2331
+ },
2332
+ });
2333
+ }
2334
+ bind(connection) {
2335
+ this.facade.bindConnection(connection.id).subscribe({
2336
+ error: (error) => this.facade.showError(error),
2337
+ });
2338
+ }
2339
+ confirmUnbind(connection) {
2340
+ this.confirmationService.confirm({
2341
+ type: 'dialog',
2342
+ header: this.t('unbind'),
2343
+ message: this.t('confirm.unbind'),
2344
+ accept: () => this.facade.unbindConnection(connection.id).subscribe({
2345
+ error: (error) => this.facade.showError(error),
2346
+ }),
2347
+ });
2348
+ }
2349
+ confirmDelete(connection) {
2350
+ this.confirmationService.confirmDelete({
2351
+ type: 'dialog',
2352
+ message: this.t('confirm.delete'),
2353
+ accept: () => this.facade
2354
+ .deleteConnection(connection.id, connection.concurrencyToken)
2355
+ .subscribe({ error: (error) => this.facade.showError(error) }),
2356
+ });
2357
+ }
2358
+ canMutate() {
2359
+ return this.canConfigure && this.facade.managementAvailable();
2360
+ }
2361
+ openEditor(connectionId) {
2362
+ const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
2363
+ header: this.t(connectionId ? 'editor.edit-title' : 'editor.create-title'),
2364
+ styleClass: '!absolute !shadow-none !w-full !max-w-full',
2365
+ position: 'end',
2366
+ appendTo: 'page-content',
2367
+ dismissableMask: true,
2368
+ dismissible: true,
2369
+ inputValues: { editorConnectionId: connectionId },
2370
+ });
2371
+ drawer.onClose.subscribe((saved) => {
2372
+ if (!saved)
2373
+ return;
2374
+ this.facade.loadConnections().subscribe({
2375
+ error: (error) => this.facade.showError(error),
2376
+ });
2377
+ });
2378
+ }
2379
+ hasAction(connection, actionKey) {
2380
+ return getApplicableProviderActions(this.facade.providerByKey(connection.providerKey), connection.authenticationMode).some((action) => action.key === actionKey);
2381
+ }
2382
+ t(key) {
2383
+ return this.transloco.translate(`email-delivery.${key}`);
2384
+ }
2385
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, deps: [], target: i0.ɵɵFactoryTarget.Component });
2386
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryConnections, isStandalone: true, selector: "mt-email-delivery-connections", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-labelledby=\"connections-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h2 id=\"connections-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"connections-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"connections-description\") }}\r\n </p>\r\n </div>\r\n @if (\r\n canConfigure &&\r\n facade.managementAvailable() &&\r\n facade.providers().length > 0\r\n ) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else if (\r\n !facade.statusLoading() &&\r\n !facade.connectionsLoading() &&\r\n facade.connections().length === 0\r\n ) {\r\n <mt-empty-state\r\n [title]=\"t('no-connections-title')\"\r\n [description]=\"t('no-connections-description')\"\r\n [actionLabel]=\"\r\n canConfigure && facade.managementAvailable()\r\n ? t('create-connection')\r\n : undefined\r\n \"\r\n actionIcon=\"general.plus\"\r\n (actionClick)=\"createConnection()\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n </section>\r\n</ng-container>\r\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: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2387
+ }
2388
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, decorators: [{
2389
+ type: Component,
2390
+ args: [{ selector: 'mt-email-delivery-connections', imports: [TranslocoDirective, Button, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-labelledby=\"connections-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h2 id=\"connections-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"connections-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"connections-description\") }}\r\n </p>\r\n </div>\r\n @if (\r\n canConfigure &&\r\n facade.managementAvailable() &&\r\n facade.providers().length > 0\r\n ) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else if (\r\n !facade.statusLoading() &&\r\n !facade.connectionsLoading() &&\r\n facade.connections().length === 0\r\n ) {\r\n <mt-empty-state\r\n [title]=\"t('no-connections-title')\"\r\n [description]=\"t('no-connections-description')\"\r\n [actionLabel]=\"\r\n canConfigure && facade.managementAvailable()\r\n ? t('create-connection')\r\n : undefined\r\n \"\r\n actionIcon=\"general.plus\"\r\n (actionClick)=\"createConnection()\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
2391
+ }] });
2392
+ const connectionStates = [
2393
+ 'Draft',
2394
+ 'PendingAuthorization',
2395
+ 'PendingValidation',
2396
+ 'Ready',
2397
+ 'ReauthorizationRequired',
2398
+ 'ConditionalAccessRequired',
2399
+ 'Disabled',
2400
+ 'Invalid',
2401
+ ];
2402
+ function toKebabCase(value) {
2403
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
2404
+ }
2405
+
2406
+ class EmailDeliveryShell {
2407
+ route = inject(ActivatedRoute);
2408
+ router = inject(Router);
2409
+ transloco = inject(TranslocoService);
2410
+ toast = inject(ToastService);
2411
+ facade = inject(EmailDeliveryFacade);
2412
+ activeLanguage = toSignal(this.transloco.langChanges$, {
2413
+ initialValue: this.transloco.getActiveLang(),
2414
+ });
2415
+ activeTab = signal('connections', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
2416
+ canView = this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel);
2417
+ tabs = computed(() => {
2418
+ this.activeLanguage();
2419
+ return [
2420
+ { label: this.t('tabs.connections'), value: 'connections' },
2421
+ { label: this.t('tabs.activity'), value: 'activity' },
2422
+ ];
2423
+ }, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
2424
+ constructor() {
2425
+ this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
2426
+ this.activeTab.set(params.get('tab') === 'activity' ? 'activity' : 'connections');
2427
+ });
2428
+ }
2429
+ ngOnInit() {
2430
+ if (!this.canView)
2431
+ return;
2432
+ this.facade.initialize();
2433
+ this.syncTabFromUrl();
2434
+ this.handleAuthorizationCallback();
2435
+ }
2436
+ onTabChange(value) {
2437
+ if (value !== 'connections' && value !== 'activity')
2438
+ return;
2439
+ void this.router.navigate([], {
2440
+ relativeTo: this.route,
2441
+ queryParams: { tab: value === 'connections' ? null : value },
2442
+ queryParamsHandling: 'merge',
2443
+ });
2444
+ }
2445
+ goBack() {
2446
+ void this.router.navigate(['/control-panel/product-settings']);
2447
+ }
2448
+ syncTabFromUrl() {
2449
+ this.activeTab.set(this.route.snapshot.queryParamMap.get('tab') === 'activity'
2450
+ ? 'activity'
2451
+ : 'connections');
2452
+ }
2453
+ handleAuthorizationCallback() {
2454
+ const query = this.route.snapshot.queryParamMap;
2455
+ const authorization = query.get('authorization');
2456
+ const code = query.get('code');
2457
+ if (!authorization && !code)
2458
+ return;
2459
+ const message = this.facade.describeCode(code);
2460
+ if (authorization === 'completed') {
2461
+ this.toast.success(message);
2462
+ }
2463
+ else {
2464
+ this.toast.error(message);
2465
+ }
2466
+ const pendingConnectionId = this.facade.takePendingAuthorizationConnection();
2467
+ void this.router
2468
+ .navigate([], {
2469
+ relativeTo: this.route,
2470
+ queryParams: { authorization: null, code: null },
2471
+ queryParamsHandling: 'merge',
2472
+ replaceUrl: true,
2473
+ })
2474
+ .then(() => {
2475
+ if (!pendingConnectionId)
2476
+ return;
2477
+ this.facade.loadConnection(pendingConnectionId).subscribe({
2478
+ next: () => this.facade.loadConnections().subscribe(),
2479
+ error: (error) => this.facade.showError(error),
2480
+ });
2481
+ });
2482
+ }
2483
+ t(key) {
2484
+ return this.transloco.translate(`email-delivery.${key}`);
2485
+ }
2486
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
2487
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryShell, isStandalone: true, selector: "mt-email-delivery-shell", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n @if (canView) {\r\n <p class=\"mb-4 text-sm text-muted-color\">\r\n {{ t(\"description\") }}\r\n </p>\r\n @if (facade.status(); as status) {\r\n <div\r\n class=\"mb-4 rounded-lg border px-4 py-3\"\r\n [class.border-emerald-200]=\"status.isReady\"\r\n [class.bg-emerald-50]=\"status.isReady\"\r\n [class.border-amber-200]=\"!status.isReady && status.managementEnabled\"\r\n [class.bg-amber-50]=\"!status.isReady && status.managementEnabled\"\r\n [class.border-surface-300]=\"!status.managementEnabled\"\r\n [class.bg-surface-100]=\"!status.managementEnabled\"\r\n role=\"status\"\r\n >\r\n <p class=\"font-semibold text-color\">\r\n {{\r\n status.isReady\r\n ? t(\"ready\")\r\n : status.managementEnabled\r\n ? t(\"needs-setup\")\r\n : t(\"unavailable\")\r\n }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(status.code) }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n", dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: EmailDeliveryConnections, selector: "mt-email-delivery-connections" }, { kind: "component", type: EmailDeliveryActivity, selector: "mt-email-delivery-activity" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2488
+ }
2489
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, decorators: [{
2490
+ type: Component,
2491
+ args: [{ selector: 'mt-email-delivery-shell', imports: [
2492
+ Page,
2493
+ TranslocoDirective,
2494
+ EmptyState,
2495
+ EmailDeliveryConnections,
2496
+ EmailDeliveryActivity,
2497
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n @if (canView) {\r\n <p class=\"mb-4 text-sm text-muted-color\">\r\n {{ t(\"description\") }}\r\n </p>\r\n @if (facade.status(); as status) {\r\n <div\r\n class=\"mb-4 rounded-lg border px-4 py-3\"\r\n [class.border-emerald-200]=\"status.isReady\"\r\n [class.bg-emerald-50]=\"status.isReady\"\r\n [class.border-amber-200]=\"!status.isReady && status.managementEnabled\"\r\n [class.bg-amber-50]=\"!status.isReady && status.managementEnabled\"\r\n [class.border-surface-300]=\"!status.managementEnabled\"\r\n [class.bg-surface-100]=\"!status.managementEnabled\"\r\n role=\"status\"\r\n >\r\n <p class=\"font-semibold text-color\">\r\n {{\r\n status.isReady\r\n ? t(\"ready\")\r\n : status.managementEnabled\r\n ? t(\"needs-setup\")\r\n : t(\"unavailable\")\r\n }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(status.code) }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n" }]
2498
+ }], ctorParameters: () => [] });
2499
+
2500
+ export { EmailDeliveryShell };
2501
+ //# sourceMappingURL=masterteam-system-settings-email-delivery-shell-DowMjl4L.mjs.map