@masterteam/system-settings 0.0.16 → 0.0.18

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