@masterteam/system-settings 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/i18n/ar.json +54 -14
- package/assets/i18n/en.json +54 -14
- package/assets/system-settings.css +1 -1
- package/fesm2022/{masterteam-system-settings-email-delivery-shell-DowMjl4L.mjs → masterteam-system-settings-email-delivery-shell-C_AhRF1f.mjs} +654 -219
- package/fesm2022/masterteam-system-settings-email-delivery-shell-C_AhRF1f.mjs.map +1 -0
- package/fesm2022/masterteam-system-settings.mjs +13 -2
- package/fesm2022/masterteam-system-settings.mjs.map +1 -1
- package/package.json +2 -2
- package/types/masterteam-system-settings.d.ts +10 -1
- package/fesm2022/masterteam-system-settings-email-delivery-shell-DowMjl4L.mjs.map +0 -1
|
@@ -1,31 +1,137 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { input, inject, computed, ChangeDetectionStrategy, Component, Injectable, signal, DestroyRef } from '@angular/core';
|
|
3
3
|
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
4
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
5
5
|
import { TranslocoService, TranslocoDirective } from '@jsverse/transloco';
|
|
6
|
+
import { Breadcrumb } from '@masterteam/components/breadcrumb';
|
|
6
7
|
import { EmptyState } from '@masterteam/components/empty-state';
|
|
7
8
|
import { Page } from '@masterteam/components/page';
|
|
8
9
|
import { ToastService } from '@masterteam/components/toast';
|
|
10
|
+
import { Tooltip } from '@masterteam/components/tooltip';
|
|
11
|
+
import { EntityStatus } from '@masterteam/components/entities';
|
|
9
12
|
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
10
13
|
import { mergeMap, of, throwError, catchError, map, switchMap, forkJoin, tap, finalize, take, timer } from 'rxjs';
|
|
11
|
-
import { EMAIL_DELIVERY_OPTIONS, EMAIL_DELIVERY_ATTEMPT_STATUSES, EMAIL_DELIVERY_PERMISSIONS } from './masterteam-system-settings.mjs';
|
|
14
|
+
import { EMAIL_DELIVERY_OPTIONS, isConnectionTestAttempt, EMAIL_DELIVERY_ATTEMPT_STATUSES, EMAIL_DELIVERY_PERMISSIONS } from './masterteam-system-settings.mjs';
|
|
12
15
|
import { Icon } from '@masterteam/icons';
|
|
13
|
-
import * as i1 from '@angular/forms';
|
|
14
|
-
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
15
16
|
import { Button } from '@masterteam/components/button';
|
|
16
17
|
import { ModalService } from '@masterteam/components/modal';
|
|
17
|
-
import { SelectField } from '@masterteam/components/select-field';
|
|
18
18
|
import { Table } from '@masterteam/components/table';
|
|
19
|
-
import { EntityStatus } from '@masterteam/components/entities';
|
|
20
19
|
import { DatePipe, DOCUMENT } from '@angular/common';
|
|
21
|
-
import { Card } from '@masterteam/components/card';
|
|
22
20
|
import { ModalRef } from '@masterteam/components/dialog';
|
|
21
|
+
import * as i1 from '@angular/forms';
|
|
22
|
+
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
23
23
|
import { TextareaField } from '@masterteam/components/textarea-field';
|
|
24
24
|
import { ToggleField } from '@masterteam/components/toggle-field';
|
|
25
25
|
import { ConfirmationService } from '@masterteam/components/confirmation';
|
|
26
26
|
import { TextareaFieldConfig, TextFieldConfig, ToggleFieldConfig, ValidatorConfig, SelectFieldConfig, RadioButtonFieldConfig, NumberFieldConfig } from '@masterteam/components';
|
|
27
27
|
import { DynamicForm } from '@masterteam/forms/dynamic-form';
|
|
28
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
|
+
}
|
|
29
135
|
|
|
30
136
|
class EmailDeliveryApi {
|
|
31
137
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
@@ -73,10 +179,19 @@ class EmailDeliveryHttpApi extends EmailDeliveryApi {
|
|
|
73
179
|
}
|
|
74
180
|
sendConnectionTest(id, request) {
|
|
75
181
|
return this.http
|
|
76
|
-
.post(this.url(`connections/${encodeURIComponent(id)}/test`), request)
|
|
77
|
-
.pipe(mergeMap((response) =>
|
|
78
|
-
|
|
79
|
-
|
|
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) => {
|
|
80
195
|
const body = error instanceof HttpErrorResponse ? error.error : error;
|
|
81
196
|
return isEmailSendResult(body) ? of(body) : throwError(() => error);
|
|
82
197
|
}));
|
|
@@ -158,6 +273,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
158
273
|
type: Injectable,
|
|
159
274
|
args: [{ providedIn: 'root' }]
|
|
160
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
|
+
};
|
|
161
290
|
function isEmailSendResult(value) {
|
|
162
291
|
return (typeof value === 'object' &&
|
|
163
292
|
value !== null &&
|
|
@@ -189,6 +318,7 @@ class EmailDeliveryFacade {
|
|
|
189
318
|
transloco = inject(TranslocoService);
|
|
190
319
|
toast = inject(ToastService);
|
|
191
320
|
initialized = false;
|
|
321
|
+
attemptsLoaded = false;
|
|
192
322
|
status = signal(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
193
323
|
providers = signal([], ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
|
|
194
324
|
errorCatalog = signal([], ...(ngDevMode ? [{ debugName: "errorCatalog" }] : /* istanbul ignore next */ []));
|
|
@@ -256,11 +386,11 @@ class EmailDeliveryFacade {
|
|
|
256
386
|
}
|
|
257
387
|
updateConnection(id, request) {
|
|
258
388
|
this.mutationLoading.set('update');
|
|
259
|
-
return this.api.updateConnection(id, request).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-updated')), finalize(() => this.mutationLoading.set(null)));
|
|
389
|
+
return this.api.updateConnection(id, request).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-updated')), finalize(() => this.mutationLoading.set(null)));
|
|
260
390
|
}
|
|
261
391
|
deleteConnection(id, concurrencyToken) {
|
|
262
392
|
this.mutationLoading.set('delete');
|
|
263
|
-
return this.api.deleteConnection(id, concurrencyToken).pipe(switchMap(() => forkJoin({
|
|
393
|
+
return this.api.deleteConnection(id, concurrencyToken).pipe(this.reloadOnConflict(id), switchMap(() => forkJoin({
|
|
264
394
|
connections: this.loadConnections(),
|
|
265
395
|
status: this.loadStatus(),
|
|
266
396
|
})), tap(() => {
|
|
@@ -270,26 +400,33 @@ class EmailDeliveryFacade {
|
|
|
270
400
|
}
|
|
271
401
|
validateConnection(id) {
|
|
272
402
|
this.mutationLoading.set('validate');
|
|
273
|
-
return this.api.validateConnection(id).pipe(switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), tap((result) => result.isValid
|
|
403
|
+
return this.api.validateConnection(id).pipe(this.reloadOnConflict(id), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), tap((result) => result.isValid
|
|
274
404
|
? this.toast.success(this.describeCode(result.code))
|
|
275
405
|
: this.toast.warn(this.describeCode(result.code))), finalize(() => this.mutationLoading.set(null)));
|
|
276
406
|
}
|
|
277
407
|
sendConnectionTest(id, request) {
|
|
278
408
|
this.mutationLoading.set('test');
|
|
279
409
|
this.lastConnectionTestRequestId.set(request.testRequestId);
|
|
280
|
-
return this.api.sendConnectionTest(id, request).pipe(tap((result) =>
|
|
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)));
|
|
281
414
|
}
|
|
282
415
|
retrieveConnectionTest(connectionId, testRequestId) {
|
|
283
416
|
this.mutationLoading.set('retrieve-test');
|
|
284
|
-
return this.api.getConnectionTest(connectionId, testRequestId).pipe(tap((stored) =>
|
|
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)));
|
|
285
422
|
}
|
|
286
423
|
bindConnection(id, purpose = 'notifications') {
|
|
287
424
|
this.mutationLoading.set('bind');
|
|
288
|
-
return this.api.bindConnection(purpose, id).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-bound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
425
|
+
return this.api.bindConnection(purpose, id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-bound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
289
426
|
}
|
|
290
427
|
unbindConnection(id, purpose = 'notifications') {
|
|
291
428
|
this.mutationLoading.set('unbind');
|
|
292
|
-
return this.api.unbindConnection(purpose).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-unbound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
429
|
+
return this.api.unbindConnection(purpose).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-unbound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
293
430
|
}
|
|
294
431
|
startAuthorization(id, kind) {
|
|
295
432
|
this.mutationLoading.set(kind);
|
|
@@ -300,7 +437,7 @@ class EmailDeliveryFacade {
|
|
|
300
437
|
}
|
|
301
438
|
revokeAuthorization(id) {
|
|
302
439
|
this.mutationLoading.set('revoke');
|
|
303
|
-
return this.api.revokeAuthorization(id).pipe(switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('authorization-revoked')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
440
|
+
return this.api.revokeAuthorization(id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('authorization-revoked')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
|
|
304
441
|
}
|
|
305
442
|
loadAttempts(reset = true, status = this.attemptStatusFilter()) {
|
|
306
443
|
this.attemptsLoading.set(true);
|
|
@@ -308,6 +445,7 @@ class EmailDeliveryFacade {
|
|
|
308
445
|
if (reset)
|
|
309
446
|
this.attemptStatusFilter.set(status);
|
|
310
447
|
return this.api.getAttempts(status, cursor, 50).pipe(tap((page) => {
|
|
448
|
+
this.attemptsLoaded = true;
|
|
311
449
|
this.attempts.set(reset ? page.items : mergeById(this.attempts(), page.items, 'id'));
|
|
312
450
|
this.attemptsNextCursor.set(page.nextCursor);
|
|
313
451
|
}), finalize(() => this.attemptsLoading.set(false)));
|
|
@@ -394,25 +532,81 @@ class EmailDeliveryFacade {
|
|
|
394
532
|
const action = this.transloco.translate(actionKey);
|
|
395
533
|
return action === actionKey ? code : `${action} (${code})`;
|
|
396
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* The one sentence every failure path shows. It prefers the backend's stable
|
|
537
|
+
* code, falls back to the transport status the contract assigns a required
|
|
538
|
+
* behaviour to (401/403/409/422/428/429/503), and always appends the
|
|
539
|
+
* correlation ID when one is available — that identifier is what support asks
|
|
540
|
+
* for, and it is the only part of a failure this page is allowed to keep.
|
|
541
|
+
*/
|
|
397
542
|
describeError(error) {
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
543
|
+
const failure = extractFailure(error);
|
|
544
|
+
const parts = [this.describeFailure(failure)];
|
|
545
|
+
if (failure.retryAfterSeconds !== null) {
|
|
546
|
+
parts.push(this.transloco.translate('email-delivery.retry-after', {
|
|
547
|
+
seconds: failure.retryAfterSeconds,
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
if (failure.correlationId) {
|
|
551
|
+
parts.push(`${this.transloco.translate('email-delivery.correlation-id')}: ${failure.correlationId}`);
|
|
552
|
+
}
|
|
553
|
+
return parts.join(' ');
|
|
403
554
|
}
|
|
404
555
|
errorAction(error) {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return (problem.action ||
|
|
409
|
-
this.errorCatalog().find((entry) => entry.code === problem.code)
|
|
556
|
+
const failure = extractFailure(error);
|
|
557
|
+
return (failure.action ||
|
|
558
|
+
this.errorCatalog().find((entry) => entry.code === failure.code)
|
|
410
559
|
?.frontendAction ||
|
|
411
|
-
|
|
560
|
+
statusAction(failure.status));
|
|
412
561
|
}
|
|
413
562
|
showError(error) {
|
|
414
563
|
this.toast.error(this.describeError(error));
|
|
415
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* A `409` means the server state moved under this page. The contract requires
|
|
567
|
+
* reloading it rather than re-submitting over it, so every mutation that can
|
|
568
|
+
* conflict routes through here: the latest connection, list and readiness are
|
|
569
|
+
* pulled back before the caller sees the rejection.
|
|
570
|
+
*/
|
|
571
|
+
reloadOnConflict(id) {
|
|
572
|
+
return catchError((error) => {
|
|
573
|
+
if (statusOf(error) !== 409)
|
|
574
|
+
return throwError(() => error);
|
|
575
|
+
const reload = id
|
|
576
|
+
? this.refreshConnection(id).pipe(map(() => undefined))
|
|
577
|
+
: forkJoin({
|
|
578
|
+
connections: this.loadConnections(),
|
|
579
|
+
status: this.loadStatus(),
|
|
580
|
+
}).pipe(map(() => undefined));
|
|
581
|
+
reload.subscribe({ error: () => undefined });
|
|
582
|
+
return throwError(() => error);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
describeFailure(failure) {
|
|
586
|
+
const translated = failure.code ? this.translateCode(failure.code) : null;
|
|
587
|
+
if (translated)
|
|
588
|
+
return translated;
|
|
589
|
+
const statusMessage = this.translateStatus(failure.status);
|
|
590
|
+
if (statusMessage) {
|
|
591
|
+
return failure.code
|
|
592
|
+
? `${statusMessage} (${failure.code})`
|
|
593
|
+
: statusMessage;
|
|
594
|
+
}
|
|
595
|
+
return this.describeCode(failure.code);
|
|
596
|
+
}
|
|
597
|
+
translateCode(code) {
|
|
598
|
+
const key = `email-delivery.codes.${code}`;
|
|
599
|
+
const translated = this.transloco.translate(key);
|
|
600
|
+
return translated === key ? null : translated;
|
|
601
|
+
}
|
|
602
|
+
translateStatus(status) {
|
|
603
|
+
const key = status === null ? null : HTTP_STATUS_KEYS[status];
|
|
604
|
+
if (!key)
|
|
605
|
+
return null;
|
|
606
|
+
const fullKey = `email-delivery.http.${key}`;
|
|
607
|
+
const translated = this.transloco.translate(fullKey);
|
|
608
|
+
return translated === fullKey ? null : translated;
|
|
609
|
+
}
|
|
416
610
|
takePendingAuthorizationConnection() {
|
|
417
611
|
if (typeof sessionStorage === 'undefined')
|
|
418
612
|
return null;
|
|
@@ -420,6 +614,17 @@ class EmailDeliveryFacade {
|
|
|
420
614
|
sessionStorage.removeItem(PENDING_AUTHORIZATION_STORAGE_KEY);
|
|
421
615
|
return id;
|
|
422
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* A completed controlled test writes its own Activity row, so the loaded
|
|
619
|
+
* Activity list is stale the moment the test returns. Only refresh once
|
|
620
|
+
* Activity has been opened — before that its own load covers it, and the
|
|
621
|
+
* status filter in effect there is preserved.
|
|
622
|
+
*/
|
|
623
|
+
refreshActivity() {
|
|
624
|
+
if (!this.attemptsLoaded)
|
|
625
|
+
return;
|
|
626
|
+
this.loadAttempts(true).subscribe({ error: () => undefined });
|
|
627
|
+
}
|
|
423
628
|
refreshConnection(id) {
|
|
424
629
|
return forkJoin({
|
|
425
630
|
details: this.api.getConnection(id),
|
|
@@ -446,20 +651,81 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
446
651
|
type: Injectable,
|
|
447
652
|
args: [{ providedIn: 'root' }]
|
|
448
653
|
}] });
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
654
|
+
const HTTP_STATUS_KEYS = {
|
|
655
|
+
401: 'unauthorized',
|
|
656
|
+
403: 'forbidden',
|
|
657
|
+
409: 'conflict',
|
|
658
|
+
422: 'unprocessable',
|
|
659
|
+
428: 'authorization-required',
|
|
660
|
+
429: 'too-many-requests',
|
|
661
|
+
503: 'unavailable',
|
|
662
|
+
};
|
|
663
|
+
const HTTP_STATUS_ACTIONS = {
|
|
664
|
+
403: 'request_permission',
|
|
665
|
+
409: 'refresh',
|
|
666
|
+
422: 'correct_configuration',
|
|
667
|
+
428: 'authorize',
|
|
668
|
+
429: 'retry_later',
|
|
669
|
+
503: 'retry_later',
|
|
670
|
+
};
|
|
671
|
+
function statusAction(status) {
|
|
672
|
+
return status === null ? null : (HTTP_STATUS_ACTIONS[status] ?? null);
|
|
673
|
+
}
|
|
674
|
+
function statusOf(error) {
|
|
675
|
+
return error instanceof HttpErrorResponse ? error.status : null;
|
|
676
|
+
}
|
|
677
|
+
function extractFailure(error) {
|
|
678
|
+
const body = error instanceof HttpErrorResponse ? error.error : error;
|
|
679
|
+
const problem = readProblem(body);
|
|
680
|
+
const headers = error instanceof HttpErrorResponse ? error.headers : null;
|
|
681
|
+
return {
|
|
682
|
+
code: problem?.code ?? null,
|
|
683
|
+
action: problem?.action || null,
|
|
684
|
+
correlationId: problem?.correlationId ||
|
|
685
|
+
headers?.get('x-correlation-id') ||
|
|
686
|
+
headers?.get('correlation-id') ||
|
|
687
|
+
null,
|
|
688
|
+
retryAfterSeconds: problem?.retryAfterSeconds ??
|
|
689
|
+
parseRetryAfter(headers?.get('retry-after') ?? null),
|
|
690
|
+
status: statusOf(error),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Two envelopes reach this page. The email contract answers with the problem
|
|
695
|
+
* shape itself; Gateway rejects a request before it ever gets there and answers
|
|
696
|
+
* with `{ errors: { code, message } }` — that is how `REQ_001` arrives, and
|
|
697
|
+
* reading only the first shape lost it.
|
|
698
|
+
*/
|
|
699
|
+
function readProblem(body) {
|
|
700
|
+
if (typeof body !== 'object' || body === null)
|
|
701
|
+
return null;
|
|
702
|
+
const problem = body;
|
|
703
|
+
if (typeof problem.code === 'string')
|
|
704
|
+
return problem;
|
|
705
|
+
const gatewayCode = problem.errors?.code;
|
|
706
|
+
if (typeof gatewayCode === 'string') {
|
|
707
|
+
return { ...problem, code: gatewayCode };
|
|
460
708
|
}
|
|
461
709
|
return null;
|
|
462
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* `Retry-After` is either a delay in seconds or an HTTP date. Both become a
|
|
713
|
+
* whole number of seconds so the message can say when to try again; anything
|
|
714
|
+
* unparseable or already in the past is simply not offered.
|
|
715
|
+
*/
|
|
716
|
+
function parseRetryAfter(value) {
|
|
717
|
+
if (!value)
|
|
718
|
+
return null;
|
|
719
|
+
const seconds = Number(value);
|
|
720
|
+
if (Number.isFinite(seconds)) {
|
|
721
|
+
return seconds > 0 ? Math.ceil(seconds) : null;
|
|
722
|
+
}
|
|
723
|
+
const date = Date.parse(value);
|
|
724
|
+
if (Number.isNaN(date))
|
|
725
|
+
return null;
|
|
726
|
+
const delta = Math.ceil((date - Date.now()) / 1000);
|
|
727
|
+
return delta > 0 ? delta : null;
|
|
728
|
+
}
|
|
463
729
|
function mergeById(current, incoming, key) {
|
|
464
730
|
const values = new Map(current.map((item) => [item[key], item]));
|
|
465
731
|
incoming.forEach((item) => values.set(item[key], item));
|
|
@@ -475,103 +741,6 @@ function mergeHostEvidence(current, incoming) {
|
|
|
475
741
|
return Array.from(values.values());
|
|
476
742
|
}
|
|
477
743
|
|
|
478
|
-
const STATUS_DEFINITIONS = {
|
|
479
|
-
connection: {
|
|
480
|
-
Draft: status('#64748B', 'connection.draft'),
|
|
481
|
-
PendingAuthorization: status('#D97706', 'connection.pending-authorization'),
|
|
482
|
-
PendingValidation: status('#0284C7', 'connection.pending-validation'),
|
|
483
|
-
Ready: status('#059669', 'connection.ready'),
|
|
484
|
-
ReauthorizationRequired: status('#EA580C', 'connection.reauthorization-required'),
|
|
485
|
-
ConditionalAccessRequired: status('#EA580C', 'connection.conditional-access-required'),
|
|
486
|
-
Disabled: status('#64748B', 'connection.disabled'),
|
|
487
|
-
Invalid: status('#DC2626', 'connection.invalid'),
|
|
488
|
-
},
|
|
489
|
-
attempt: {
|
|
490
|
-
Reserved: status('#64748B', 'attempt.reserved'),
|
|
491
|
-
ProviderInvocationStarted: status('#0284C7', 'attempt.provider-invocation-started'),
|
|
492
|
-
ProviderInvocationDefinitivelyNotAccepted: status('#DC2626', 'attempt.provider-not-accepted'),
|
|
493
|
-
ProviderResultReconciliation: status('#7C3AED', 'attempt.reconciling'),
|
|
494
|
-
ProviderResultPending: status('#0284C7', 'attempt.pending'),
|
|
495
|
-
Blocked: status('#DC2626', 'attempt.blocked'),
|
|
496
|
-
RetryScheduled: status('#D97706', 'attempt.retry-scheduled'),
|
|
497
|
-
Accepted: status('#059669', 'attempt.accepted'),
|
|
498
|
-
OutcomeUnknown: status('#EA580C', 'attempt.outcome-unknown'),
|
|
499
|
-
FailedPermanent: status('#DC2626', 'attempt.failed-permanent'),
|
|
500
|
-
DeadLettered: status('#BE123C', 'attempt.dead-lettered'),
|
|
501
|
-
},
|
|
502
|
-
support: {
|
|
503
|
-
active: status('#D97706', 'support.active'),
|
|
504
|
-
'repair-pending': status('#0284C7', 'support.repair-pending'),
|
|
505
|
-
repaired: status('#059669', 'support.repaired'),
|
|
506
|
-
'acknowledgement-pending': status('#0284C7', 'support.acknowledgement-pending'),
|
|
507
|
-
acknowledged: status('#64748B', 'support.acknowledged'),
|
|
508
|
-
Owned: status('#059669', 'support.owned'),
|
|
509
|
-
owned: status('#059669', 'support.owned'),
|
|
510
|
-
Missing: status('#D97706', 'support.missing'),
|
|
511
|
-
missing: status('#D97706', 'support.missing'),
|
|
512
|
-
Conflict: status('#DC2626', 'support.conflict'),
|
|
513
|
-
conflict: status('#DC2626', 'support.conflict'),
|
|
514
|
-
IdentityIncomplete: status('#EA580C', 'support.identity-incomplete'),
|
|
515
|
-
identityincomplete: status('#EA580C', 'support.identity-incomplete'),
|
|
516
|
-
'retryable-after-upgrade': status('#0284C7', 'support.retryable-after-upgrade'),
|
|
517
|
-
'repair-required': status('#EA580C', 'support.repair-required'),
|
|
518
|
-
'acknowledge-only': status('#64748B', 'support.acknowledge-only'),
|
|
519
|
-
configured: status('#059669', 'support.configured'),
|
|
520
|
-
'not-configured': status('#64748B', 'support.not-configured'),
|
|
521
|
-
},
|
|
522
|
-
};
|
|
523
|
-
class EmailDeliveryStatus {
|
|
524
|
-
value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
525
|
-
kind = input('support', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
|
|
526
|
-
transloco = inject(TranslocoService);
|
|
527
|
-
activeLanguage = toSignal(this.transloco.langChanges$, {
|
|
528
|
-
initialValue: this.transloco.getActiveLang(),
|
|
529
|
-
});
|
|
530
|
-
statusData = computed(() => {
|
|
531
|
-
this.activeLanguage();
|
|
532
|
-
return {
|
|
533
|
-
name: '',
|
|
534
|
-
viewType: 'Status',
|
|
535
|
-
value: getEmailDeliveryStatusValue(this.kind(), this.value(), (key) => this.transloco.translate(key)),
|
|
536
|
-
configuration: { hideName: true },
|
|
537
|
-
};
|
|
538
|
-
}, ...(ngDevMode ? [{ debugName: "statusData" }] : /* istanbul ignore next */ []));
|
|
539
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
540
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: EmailDeliveryStatus, isStandalone: true, selector: "mt-email-delivery-status", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: `<mt-entity-status [data]="statusData()" />`, isInline: true, dependencies: [{ kind: "component", type: EntityStatus, selector: "mt-entity-status", inputs: ["data", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
541
|
-
}
|
|
542
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, decorators: [{
|
|
543
|
-
type: Component,
|
|
544
|
-
args: [{
|
|
545
|
-
selector: 'mt-email-delivery-status',
|
|
546
|
-
imports: [EntityStatus],
|
|
547
|
-
template: `<mt-entity-status [data]="statusData()" />`,
|
|
548
|
-
host: { class: 'inline-flex' },
|
|
549
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
550
|
-
}]
|
|
551
|
-
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: false }] }] } });
|
|
552
|
-
function getEmailDeliveryStatusMap(kind, translate) {
|
|
553
|
-
return Object.fromEntries(Object.entries(STATUS_DEFINITIONS[kind]).map(([key, definition]) => [
|
|
554
|
-
key,
|
|
555
|
-
{
|
|
556
|
-
color: definition.color,
|
|
557
|
-
display: translate(`email-delivery.status.${definition.translationKey}`),
|
|
558
|
-
},
|
|
559
|
-
]));
|
|
560
|
-
}
|
|
561
|
-
function getEmailDeliveryStatusValue(kind, value, translate) {
|
|
562
|
-
const definition = STATUS_DEFINITIONS[kind][value];
|
|
563
|
-
return {
|
|
564
|
-
key: value,
|
|
565
|
-
display: definition
|
|
566
|
-
? translate(`email-delivery.status.${definition.translationKey}`)
|
|
567
|
-
: value,
|
|
568
|
-
color: definition?.color ?? '#64748B',
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
function status(color, translationKey) {
|
|
572
|
-
return { color, translationKey };
|
|
573
|
-
}
|
|
574
|
-
|
|
575
744
|
class EmailOperatorReasonDialog {
|
|
576
745
|
itemName = input.required(...(ngDevMode ? [{ debugName: "itemName" }] : /* istanbul ignore next */ []));
|
|
577
746
|
consequence = input.required(...(ngDevMode ? [{ debugName: "consequence" }] : /* istanbul ignore next */ []));
|
|
@@ -605,7 +774,7 @@ class EmailOperatorReasonDialog {
|
|
|
605
774
|
this.exceptionalOverride.getRawValue()));
|
|
606
775
|
}
|
|
607
776
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
608
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailOperatorReasonDialog, isStandalone: true, selector: "mt-email-operator-reason-dialog", inputs: { itemName: { classPropertyName: "itemName", publicName: "itemName", isSignal: true, isRequired: true, transformFunction: null }, consequence: { classPropertyName: "consequence", publicName: "consequence", isSignal: true, isRequired: true, transformFunction: null }, showExceptionalOverride: { classPropertyName: "showExceptionalOverride", publicName: "showExceptionalOverride", isSignal: true, isRequired: false, transformFunction: null }, confirmLabel: { classPropertyName: "confirmLabel", publicName: "confirmLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-
|
|
777
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailOperatorReasonDialog, isStandalone: true, selector: "mt-email-operator-reason-dialog", inputs: { itemName: { classPropertyName: "itemName", publicName: "itemName", isSignal: true, isRequired: true, transformFunction: null }, consequence: { classPropertyName: "consequence", publicName: "consequence", isSignal: true, isRequired: true, transformFunction: null }, showExceptionalOverride: { classPropertyName: "showExceptionalOverride", publicName: "showExceptionalOverride", isSignal: true, isRequired: false, transformFunction: null }, confirmLabel: { classPropertyName: "confirmLabel", publicName: "confirmLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <div class=\"flex flex-col gap-1\">\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n />\r\n <!-- mt-toggle-field has no hint input, so the consequence of an\r\n exceptional replay is rendered here instead of silently dropped. -->\r\n <p class=\"m-0 text-xs text-muted-color\">\r\n {{ t(\"exceptional-override-hint\") }}\r\n </p>\r\n </div>\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
609
778
|
}
|
|
610
779
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, decorators: [{
|
|
611
780
|
type: Component,
|
|
@@ -615,7 +784,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
615
784
|
Button,
|
|
616
785
|
TextareaField,
|
|
617
786
|
ToggleField,
|
|
618
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-
|
|
787
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <form\r\n [class]=\"'space-y-5 p-4 sm:p-5 ' + modal.contentClass\"\r\n (ngSubmit)=\"confirm()\"\r\n >\r\n <div\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <div class=\"flex flex-col gap-1\">\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n />\r\n <!-- mt-toggle-field has no hint input, so the consequence of an\r\n exceptional replay is rendered here instead of silently dropped. -->\r\n <p class=\"m-0 text-xs text-muted-color\">\r\n {{ t(\"exceptional-override-hint\") }}\r\n </p>\r\n </div>\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
|
|
619
788
|
}], propDecorators: { itemName: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemName", required: true }] }], consequence: [{ type: i0.Input, args: [{ isSignal: true, alias: "consequence", required: true }] }], showExceptionalOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExceptionalOverride", required: false }] }], confirmLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmLabel", required: false }] }] } });
|
|
620
789
|
|
|
621
790
|
class EmailAttemptDetails {
|
|
@@ -624,7 +793,17 @@ class EmailAttemptDetails {
|
|
|
624
793
|
transloco = inject(TranslocoService);
|
|
625
794
|
modal = inject(ModalService);
|
|
626
795
|
facade = inject(EmailDeliveryFacade);
|
|
796
|
+
isConnectionTest() {
|
|
797
|
+
return isConnectionTestAttempt(this.attempt());
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* A controlled test is an operator action against one connection, not product
|
|
801
|
+
* work the platform owes anyone — replaying it would send a second real email
|
|
802
|
+
* nobody asked for, so the backend's test rows are never replayable.
|
|
803
|
+
*/
|
|
627
804
|
canReplay() {
|
|
805
|
+
if (this.isConnectionTest())
|
|
806
|
+
return false;
|
|
628
807
|
return [
|
|
629
808
|
'Blocked',
|
|
630
809
|
'RetryScheduled',
|
|
@@ -676,52 +855,121 @@ class EmailAttemptDetails {
|
|
|
676
855
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
677
856
|
}
|
|
678
857
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
679
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailAttemptDetails, isStandalone: true, selector: "mt-email-attempt-details", inputs: { attempt: { classPropertyName: "attempt", publicName: "attempt", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-
|
|
858
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailAttemptDetails, isStandalone: true, selector: "mt-email-attempt-details", inputs: { attempt: { classPropertyName: "attempt", publicName: "attempt", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-md border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <!-- Same section shape the connection form uses (mt-client-form section\r\n mode): header strip on a bordered, flat, rounded-md container. -->\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.operation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{\r\n isConnectionTest()\r\n ? t(\"operation.connection-test\")\r\n : item.operationKind\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.count\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{ item.attemptCount }}\r\n </dd>\r\n </div>\r\n </dl>\r\n </section>\r\n\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n <p class=\"mt-2 text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint-hint\") }}\r\n </p>\r\n </div>\r\n @if (item.testRequestId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.testRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
680
859
|
}
|
|
681
860
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, decorators: [{
|
|
682
861
|
type: Component,
|
|
683
|
-
args: [{ selector: 'mt-email-attempt-details', imports: [DatePipe, TranslocoDirective, Button,
|
|
862
|
+
args: [{ selector: 'mt-email-attempt-details', imports: [DatePipe, TranslocoDirective, Button, EmailDeliveryStatus], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-md border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <!-- Same section shape the connection form uses (mt-client-form section\r\n mode): header strip on a bordered, flat, rounded-md container. -->\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.operation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{\r\n isConnectionTest()\r\n ? t(\"operation.connection-test\")\r\n : item.operationKind\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.count\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{ item.attemptCount }}\r\n </dd>\r\n </div>\r\n </dl>\r\n </section>\r\n\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n <p class=\"mt-2 text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint-hint\") }}\r\n </p>\r\n </div>\r\n @if (item.testRequestId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.testRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
|
|
684
863
|
}], propDecorators: { attempt: [{ type: i0.Input, args: [{ isSignal: true, alias: "attempt", required: true }] }] } });
|
|
685
864
|
function isExceptionalReplay(status) {
|
|
686
865
|
return status === 'Accepted' || status === 'OutcomeUnknown';
|
|
687
866
|
}
|
|
688
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Shared chrome for the Email Delivery overlays.
|
|
870
|
+
*
|
|
871
|
+
* Every drawer on this page opens over the page content (`appendTo:
|
|
872
|
+
* 'page-content'`), so it is positioned absolutely and drops its own shadow.
|
|
873
|
+
* It takes the full width only while there is no room to spare — from `lg` up
|
|
874
|
+
* it settles at half the page so the table stays readable behind it, which is
|
|
875
|
+
* also what keeps the forms and callouts inside from stretching into
|
|
876
|
+
* unreadably long lines.
|
|
877
|
+
*/
|
|
878
|
+
const EMAIL_DELIVERY_DRAWER_STYLE_CLASS = '!absolute !shadow-none !w-full !max-w-full md:!w-[70%] lg:!w-[50%]';
|
|
879
|
+
|
|
689
880
|
class EmailDeliveryAttempts {
|
|
690
881
|
modal = inject(ModalService);
|
|
691
882
|
transloco = inject(TranslocoService);
|
|
692
883
|
facade = inject(EmailDeliveryFacade);
|
|
693
|
-
status = new FormControl(null);
|
|
694
884
|
activeLanguage = toSignal(this.transloco.langChanges$, {
|
|
695
885
|
initialValue: this.transloco.getActiveLang(),
|
|
696
886
|
});
|
|
697
|
-
|
|
887
|
+
/**
|
|
888
|
+
* Status is the one filter the backend understands (`?status=`), so it is
|
|
889
|
+
* held here and round-tripped into the query; every other column filters the
|
|
890
|
+
* rows already loaded.
|
|
891
|
+
*/
|
|
892
|
+
tableFilters = signal({}, ...(ngDevMode ? [{ debugName: "tableFilters" }] : /* istanbul ignore next */ []));
|
|
893
|
+
/**
|
|
894
|
+
* Activity mixes product sends with the operator's own controlled tests. The
|
|
895
|
+
* table renders plain fields, so the labels are projected onto the row here;
|
|
896
|
+
* the extra properties are inert everywhere the raw attempt is used.
|
|
897
|
+
*/
|
|
898
|
+
rows = computed(() => {
|
|
698
899
|
this.activeLanguage();
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
900
|
+
return this.facade.attempts().map((attempt) => ({
|
|
901
|
+
...attempt,
|
|
902
|
+
operationLabel: this.operationLabel(attempt),
|
|
903
|
+
providerLabel: this.facade.providerByKey(attempt.providerKey)?.displayName ??
|
|
904
|
+
attempt.providerKey ??
|
|
905
|
+
'—',
|
|
906
|
+
resultLabel: attempt.resultCode
|
|
907
|
+
? this.facade.describeCode(attempt.resultCode)
|
|
908
|
+
: '—',
|
|
909
|
+
recipientFingerprintShort: shortenFingerprint(attempt.recipientFingerprint),
|
|
910
|
+
}));
|
|
911
|
+
}, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
708
912
|
columns = computed(() => {
|
|
709
913
|
this.activeLanguage();
|
|
914
|
+
const statusMap = getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco));
|
|
710
915
|
return [
|
|
711
916
|
{
|
|
712
917
|
key: 'status',
|
|
713
918
|
label: this.t('attempt.status'),
|
|
714
919
|
type: 'status',
|
|
715
|
-
|
|
920
|
+
width: '15rem',
|
|
921
|
+
statusMap,
|
|
922
|
+
filterConfig: {
|
|
923
|
+
type: 'select',
|
|
924
|
+
multiple: false,
|
|
925
|
+
options: EMAIL_DELIVERY_ATTEMPT_STATUSES.map((value) => ({
|
|
926
|
+
label: statusMap[value]?.display ?? value,
|
|
927
|
+
value,
|
|
928
|
+
})),
|
|
929
|
+
},
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
key: 'operationLabel',
|
|
933
|
+
label: this.t('attempt.operation'),
|
|
934
|
+
width: '14rem',
|
|
935
|
+
filterConfig: {
|
|
936
|
+
type: 'select',
|
|
937
|
+
options: this.distinctOptions((row) => row.operationLabel),
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
key: 'providerLabel',
|
|
942
|
+
label: this.t('attempt.provider'),
|
|
943
|
+
width: '15rem',
|
|
944
|
+
filterConfig: {
|
|
945
|
+
type: 'select',
|
|
946
|
+
options: this.distinctOptions((row) => row.providerLabel),
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
key: 'resultLabel',
|
|
951
|
+
label: this.t('attempt.result'),
|
|
952
|
+
filterConfig: { type: 'text' },
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
key: 'recipientFingerprintShort',
|
|
956
|
+
label: this.t('recipient-fingerprint'),
|
|
957
|
+
width: '15rem',
|
|
958
|
+
filterable: false,
|
|
716
959
|
},
|
|
717
|
-
{ key: 'providerKey', label: this.t('attempt.provider') },
|
|
718
|
-
{ key: 'recipientFingerprint', label: this.t('recipient-fingerprint') },
|
|
719
960
|
{
|
|
720
961
|
key: 'createdAtUtc',
|
|
721
962
|
label: this.t('attempt.created'),
|
|
722
963
|
type: 'dateTime',
|
|
964
|
+
width: '10rem',
|
|
965
|
+
filterConfig: { type: 'date' },
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
key: 'attemptCount',
|
|
969
|
+
label: this.t('attempt.count'),
|
|
970
|
+
type: 'number',
|
|
971
|
+
width: '7rem',
|
|
723
972
|
},
|
|
724
|
-
{ key: 'attemptCount', label: this.t('attempt.count'), type: 'number' },
|
|
725
973
|
];
|
|
726
974
|
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
727
975
|
actions = computed(() => {
|
|
@@ -737,7 +985,18 @@ class EmailDeliveryAttempts {
|
|
|
737
985
|
ngOnInit() {
|
|
738
986
|
this.load(true);
|
|
739
987
|
}
|
|
740
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Only the status filter reaches the backend; the rest narrow the rows the
|
|
990
|
+
* table already holds, so nothing else needs a round trip.
|
|
991
|
+
*/
|
|
992
|
+
onFiltersChange(filters) {
|
|
993
|
+
this.tableFilters.set(filters);
|
|
994
|
+
const status = readStatusFilter(filters['status']);
|
|
995
|
+
if (status === this.facade.attemptStatusFilter())
|
|
996
|
+
return;
|
|
997
|
+
this.load(true, status);
|
|
998
|
+
}
|
|
999
|
+
refresh() {
|
|
741
1000
|
this.load(true);
|
|
742
1001
|
}
|
|
743
1002
|
loadMore() {
|
|
@@ -746,7 +1005,7 @@ class EmailDeliveryAttempts {
|
|
|
746
1005
|
openDetails(attempt) {
|
|
747
1006
|
const ref = this.modal.openModal(EmailAttemptDetails, 'drawer', {
|
|
748
1007
|
header: this.t('attempt-details'),
|
|
749
|
-
styleClass:
|
|
1008
|
+
styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
|
|
750
1009
|
position: 'end',
|
|
751
1010
|
appendTo: 'page-content',
|
|
752
1011
|
dismissableMask: true,
|
|
@@ -758,28 +1017,62 @@ class EmailDeliveryAttempts {
|
|
|
758
1017
|
this.load(true);
|
|
759
1018
|
});
|
|
760
1019
|
}
|
|
761
|
-
load(reset) {
|
|
762
|
-
this.facade.loadAttempts(reset,
|
|
1020
|
+
load(reset, status = this.facade.attemptStatusFilter()) {
|
|
1021
|
+
this.facade.loadAttempts(reset, status).subscribe({
|
|
763
1022
|
error: (error) => this.facade.showError(error),
|
|
764
1023
|
});
|
|
765
1024
|
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Select filters over locally-filtered columns only offer values that are
|
|
1027
|
+
* actually present, so the operator never picks an option that empties the
|
|
1028
|
+
* table.
|
|
1029
|
+
*/
|
|
1030
|
+
distinctOptions(select) {
|
|
1031
|
+
const values = new Set(this.rows().map(select).filter(Boolean));
|
|
1032
|
+
return Array.from(values)
|
|
1033
|
+
.sort((left, right) => left.localeCompare(right))
|
|
1034
|
+
.map((value) => ({ label: value, value }));
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Controlled tests are always shown as "Connection test"; anything else falls
|
|
1038
|
+
* back to the backend's own operation kind rather than a guessed label.
|
|
1039
|
+
*/
|
|
1040
|
+
operationLabel(attempt) {
|
|
1041
|
+
return isConnectionTestAttempt(attempt)
|
|
1042
|
+
? this.t('operation.connection-test')
|
|
1043
|
+
: attempt.operationKind;
|
|
1044
|
+
}
|
|
766
1045
|
t(key) {
|
|
767
1046
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
768
1047
|
}
|
|
769
1048
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
770
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryAttempts, isStandalone: true, selector: "mt-email-delivery-attempts", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-
|
|
1049
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryAttempts, isStandalone: true, selector: "mt-email-delivery-attempts", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-label=\"{{ t('activity-title') }}\">\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [filters]=\"tableFilters()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-attempts-title')\"\r\n [emptyDescription]=\"t('no-attempts-description')\"\r\n filterMode=\"column\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n storageKey=\"email-delivery-attempts\"\r\n (filtersChange)=\"onFiltersChange($event)\"\r\n (rowClick)=\"openDetails($event)\"\r\n >\r\n <ng-template #captionEnd>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('refresh')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"refresh()\"\r\n />\r\n </ng-template>\r\n </mt-table>\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "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 });
|
|
771
1050
|
}
|
|
772
1051
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, decorators: [{
|
|
773
1052
|
type: Component,
|
|
774
|
-
args: [{ selector: 'mt-email-delivery-attempts', imports: [
|
|
775
|
-
ReactiveFormsModule,
|
|
776
|
-
TranslocoDirective,
|
|
777
|
-
Button,
|
|
778
|
-
EmptyState,
|
|
779
|
-
SelectField,
|
|
780
|
-
Table,
|
|
781
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-labelledby=\"attempts-heading\">\r\n <div>\r\n <h2 id=\"attempts-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"activity-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"activity-description\") }}\r\n </p>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-end\">\r\n <mt-select-field\r\n class=\"w-full sm:w-72\"\r\n [formControl]=\"status\"\r\n [label]=\"t('attempt.status-filter')\"\r\n [options]=\"statusOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n />\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('apply-filter')\"\r\n (onClick)=\"applyFilter()\"\r\n />\r\n </div>\r\n\r\n @if (!facade.attemptsLoading() && facade.attempts().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-attempts-title')\"\r\n [description]=\"t('no-attempts-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"facade.attempts()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
|
|
1053
|
+
args: [{ selector: 'mt-email-delivery-attempts', imports: [TranslocoDirective, Button, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-label=\"{{ t('activity-title') }}\">\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [filters]=\"tableFilters()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-attempts-title')\"\r\n [emptyDescription]=\"t('no-attempts-description')\"\r\n filterMode=\"column\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n storageKey=\"email-delivery-attempts\"\r\n (filtersChange)=\"onFiltersChange($event)\"\r\n (rowClick)=\"openDetails($event)\"\r\n >\r\n <ng-template #captionEnd>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('refresh')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"refresh()\"\r\n />\r\n </ng-template>\r\n </mt-table>\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
|
|
782
1054
|
}] });
|
|
1055
|
+
/**
|
|
1056
|
+
* The status column is a single-select, so its stored value is the raw status.
|
|
1057
|
+
* An empty selection clears the backend filter rather than sending a blank.
|
|
1058
|
+
*/
|
|
1059
|
+
function readStatusFilter(value) {
|
|
1060
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
1061
|
+
return typeof raw === 'string' && raw.trim() !== '' ? raw : null;
|
|
1062
|
+
}
|
|
1063
|
+
const FINGERPRINT_PREFIX_LENGTH = 12;
|
|
1064
|
+
/**
|
|
1065
|
+
* A 64-character hash has nowhere to wrap, so rendering it whole pushed its
|
|
1066
|
+
* column far past the width the table asked for. The list only needs enough of
|
|
1067
|
+
* it to tell two recipients apart; the drawer still shows the value in full.
|
|
1068
|
+
*/
|
|
1069
|
+
function shortenFingerprint(value) {
|
|
1070
|
+
if (!value)
|
|
1071
|
+
return '—';
|
|
1072
|
+
return value.length > FINGERPRINT_PREFIX_LENGTH
|
|
1073
|
+
? value.slice(0, FINGERPRINT_PREFIX_LENGTH) + '…'
|
|
1074
|
+
: value;
|
|
1075
|
+
}
|
|
783
1076
|
|
|
784
1077
|
class EmailDeliveryHostEvidence {
|
|
785
1078
|
document = inject(DOCUMENT);
|
|
@@ -801,30 +1094,37 @@ class EmailDeliveryHostEvidence {
|
|
|
801
1094
|
this.activeLanguage();
|
|
802
1095
|
const statusMap = getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco));
|
|
803
1096
|
return [
|
|
804
|
-
{
|
|
1097
|
+
{
|
|
1098
|
+
key: 'evidenceKindLabel',
|
|
1099
|
+
label: this.t('evidence-kind'),
|
|
1100
|
+
width: '11rem',
|
|
1101
|
+
},
|
|
805
1102
|
{
|
|
806
1103
|
key: 'resolutionState',
|
|
807
1104
|
label: this.t('resolution-state'),
|
|
808
1105
|
type: 'status',
|
|
1106
|
+
width: '12rem',
|
|
809
1107
|
statusMap,
|
|
810
1108
|
},
|
|
811
1109
|
{
|
|
812
1110
|
key: 'ownershipState',
|
|
813
1111
|
label: this.t('ownership-state'),
|
|
814
1112
|
type: 'status',
|
|
1113
|
+
width: '11rem',
|
|
815
1114
|
statusMap,
|
|
816
1115
|
},
|
|
817
1116
|
{
|
|
818
1117
|
key: 'detectedAtUtc',
|
|
819
1118
|
label: this.t('detected'),
|
|
820
1119
|
type: 'dateTime',
|
|
1120
|
+
width: '10rem',
|
|
821
1121
|
},
|
|
822
1122
|
{ key: 'errorLabel', label: this.t('issue') },
|
|
823
1123
|
{
|
|
824
1124
|
key: 'evidenceOccurrenceIdentity',
|
|
825
1125
|
label: this.t('evidence-occurrence-id'),
|
|
826
1126
|
type: 'entity',
|
|
827
|
-
width: '
|
|
1127
|
+
width: '15rem',
|
|
828
1128
|
shrink: true,
|
|
829
1129
|
},
|
|
830
1130
|
];
|
|
@@ -933,11 +1233,11 @@ class EmailDeliveryHostEvidence {
|
|
|
933
1233
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
934
1234
|
}
|
|
935
1235
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
936
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryHostEvidence, isStandalone: true, selector: "mt-email-delivery-host-evidence", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section
|
|
1236
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryHostEvidence, isStandalone: true, selector: "mt-email-delivery-host-evidence", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-3\" aria-labelledby=\"host-evidence-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-evidence-title')\"\r\n [emptyDescription]=\"t('no-evidence-description')\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
937
1237
|
}
|
|
938
1238
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, decorators: [{
|
|
939
1239
|
type: Component,
|
|
940
|
-
args: [{ selector: 'mt-email-delivery-host-evidence', imports: [TranslocoDirective, Button,
|
|
1240
|
+
args: [{ selector: 'mt-email-delivery-host-evidence', imports: [TranslocoDirective, Button, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-3\" aria-labelledby=\"host-evidence-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-evidence-title')\"\r\n [emptyDescription]=\"t('no-evidence-description')\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
|
|
941
1241
|
}] });
|
|
942
1242
|
function identifierEntity$1(value) {
|
|
943
1243
|
return {
|
|
@@ -980,13 +1280,14 @@ class EmailDeliveryQuarantine {
|
|
|
980
1280
|
key: 'attemptIdentity',
|
|
981
1281
|
label: this.t('attempt.id'),
|
|
982
1282
|
type: 'entity',
|
|
983
|
-
width: '
|
|
1283
|
+
width: '15rem',
|
|
984
1284
|
shrink: true,
|
|
985
1285
|
},
|
|
986
1286
|
{
|
|
987
1287
|
key: 'recoveryClassification',
|
|
988
1288
|
label: this.t('recovery-classification'),
|
|
989
1289
|
type: 'status',
|
|
1290
|
+
width: '12rem',
|
|
990
1291
|
statusMap: getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco)),
|
|
991
1292
|
},
|
|
992
1293
|
{ key: 'errorLabel', label: this.t('issue') },
|
|
@@ -994,6 +1295,7 @@ class EmailDeliveryQuarantine {
|
|
|
994
1295
|
key: 'quarantinedAtUtc',
|
|
995
1296
|
label: this.t('detected'),
|
|
996
1297
|
type: 'dateTime',
|
|
1298
|
+
width: '10rem',
|
|
997
1299
|
},
|
|
998
1300
|
];
|
|
999
1301
|
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
@@ -1056,11 +1358,11 @@ class EmailDeliveryQuarantine {
|
|
|
1056
1358
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
1057
1359
|
}
|
|
1058
1360
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1059
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryQuarantine, isStandalone: true, selector: "mt-email-delivery-quarantine", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section
|
|
1361
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryQuarantine, isStandalone: true, selector: "mt-email-delivery-quarantine", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-3\" aria-labelledby=\"quarantine-heading\">\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-quarantine-title')\"\r\n [emptyDescription]=\"t('no-quarantine-description')\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1060
1362
|
}
|
|
1061
1363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, decorators: [{
|
|
1062
1364
|
type: Component,
|
|
1063
|
-
args: [{ selector: 'mt-email-delivery-quarantine', imports: [TranslocoDirective, Button,
|
|
1365
|
+
args: [{ selector: 'mt-email-delivery-quarantine', imports: [TranslocoDirective, Button, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-3\" aria-labelledby=\"quarantine-heading\">\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-quarantine-title')\"\r\n [emptyDescription]=\"t('no-quarantine-description')\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
|
|
1064
1366
|
}] });
|
|
1065
1367
|
function identifierEntity(value) {
|
|
1066
1368
|
return {
|
|
@@ -1077,7 +1379,7 @@ class EmailDeliveryActivity {
|
|
|
1077
1379
|
this.advancedExpanded.set(event.currentTarget.open);
|
|
1078
1380
|
}
|
|
1079
1381
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1080
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryActivity, isStandalone: true, selector: "mt-email-delivery-activity", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-
|
|
1382
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryActivity, isStandalone: true, selector: "mt-email-delivery-activity", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-5\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <!-- Operator recovery tools. Kept collapsed and visually quieter than the\r\n Activity table above it: they are for incident work, not daily use. -->\r\n <details\r\n class=\"group overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"flex cursor-pointer list-none items-center gap-3 p-4 focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span\r\n class=\"inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-surface-100 text-muted-color\"\r\n >\r\n <mt-icon icon=\"general.tool-02\" aria-hidden=\"true\" />\r\n </span>\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block font-semibold text-color\">\r\n {{ t(\"advanced-support\") }}\r\n </span>\r\n <span class=\"mt-0.5 block text-sm font-normal text-muted-color\">\r\n {{ t(\"advanced-support-description\") }}\r\n </span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"shrink-0 text-base text-muted-color transition-transform group-open:rotate-180\"\r\n />\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-4 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: EmailDeliveryAttempts, selector: "mt-email-delivery-attempts" }, { kind: "component", type: EmailDeliveryQuarantine, selector: "mt-email-delivery-quarantine" }, { kind: "component", type: EmailDeliveryHostEvidence, selector: "mt-email-delivery-host-evidence" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1081
1383
|
}
|
|
1082
1384
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, decorators: [{
|
|
1083
1385
|
type: Component,
|
|
@@ -1087,7 +1389,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1087
1389
|
EmailDeliveryAttempts,
|
|
1088
1390
|
EmailDeliveryQuarantine,
|
|
1089
1391
|
EmailDeliveryHostEvidence,
|
|
1090
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-
|
|
1392
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div class=\"grid gap-5\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <!-- Operator recovery tools. Kept collapsed and visually quieter than the\r\n Activity table above it: they are for incident work, not daily use. -->\r\n <details\r\n class=\"group overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"flex cursor-pointer list-none items-center gap-3 p-4 focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span\r\n class=\"inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-surface-100 text-muted-color\"\r\n >\r\n <mt-icon icon=\"general.tool-02\" aria-hidden=\"true\" />\r\n </span>\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block font-semibold text-color\">\r\n {{ t(\"advanced-support\") }}\r\n </span>\r\n <span class=\"mt-0.5 block text-sm font-normal text-muted-color\">\r\n {{ t(\"advanced-support-description\") }}\r\n </span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"shrink-0 text-base text-muted-color transition-transform group-open:rotate-180\"\r\n />\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-4 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n" }]
|
|
1091
1393
|
}] });
|
|
1092
1394
|
|
|
1093
1395
|
class EmailControlledTestDialog {
|
|
@@ -1106,7 +1408,7 @@ class EmailControlledTestDialog {
|
|
|
1106
1408
|
form = new FormControl({
|
|
1107
1409
|
recipients: null,
|
|
1108
1410
|
subject: 'Pplus email delivery test',
|
|
1109
|
-
body: 'This is a controlled
|
|
1411
|
+
body: 'This is a controlled test.',
|
|
1110
1412
|
isHtml: false,
|
|
1111
1413
|
}, { nonNullable: true });
|
|
1112
1414
|
formStatus = toSignal(this.form.statusChanges, {
|
|
@@ -1160,6 +1462,15 @@ class EmailControlledTestDialog {
|
|
|
1160
1462
|
},
|
|
1161
1463
|
],
|
|
1162
1464
|
}), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
|
|
1465
|
+
/**
|
|
1466
|
+
* The provider-acceptance caveat is shown once. When the backend already
|
|
1467
|
+
* answers with the accepted-delivery-unconfirmed code, the code message
|
|
1468
|
+
* above carries that sentence and the extra line would just repeat it.
|
|
1469
|
+
*/
|
|
1470
|
+
showsAcceptedCaveat(result) {
|
|
1471
|
+
return (result.outcome === 'Accepted' &&
|
|
1472
|
+
result.code !== ACCEPTED_DELIVERY_UNCONFIRMED_CODE);
|
|
1473
|
+
}
|
|
1163
1474
|
submit() {
|
|
1164
1475
|
if (!this.canSubmit()) {
|
|
1165
1476
|
this.form.markAllAsTouched();
|
|
@@ -1186,6 +1497,13 @@ class EmailControlledTestDialog {
|
|
|
1186
1497
|
return;
|
|
1187
1498
|
}
|
|
1188
1499
|
this.facade.showError(error);
|
|
1500
|
+
// The backend rejected this GUID because it already stands for a
|
|
1501
|
+
// different send. Retrieving it would return someone else's test and
|
|
1502
|
+
// reusing it can never succeed, so the dialog mints a fresh one and
|
|
1503
|
+
// waits for the operator to press Send again — it never resends.
|
|
1504
|
+
if (isTestRequestConflict(error)) {
|
|
1505
|
+
this.testRequestId.set(createId());
|
|
1506
|
+
}
|
|
1189
1507
|
},
|
|
1190
1508
|
});
|
|
1191
1509
|
}
|
|
@@ -1249,7 +1567,7 @@ class EmailControlledTestDialog {
|
|
|
1249
1567
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
1250
1568
|
}
|
|
1251
1569
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1252
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailControlledTestDialog, isStandalone: true, selector: "mt-email-controlled-test-dialog", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null }, connectionName: { classPropertyName: "connectionName", publicName: "connectionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-
|
|
1570
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailControlledTestDialog, isStandalone: true, selector: "mt-email-controlled-test-dialog", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null }, connectionName: { classPropertyName: "connectionName", publicName: "connectionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n <!-- The GUID this dialog will keep for the whole exchange. It is what a\r\n lost response is retrieved with, so the operator can record it before\r\n sending rather than reconstructing it afterwards. -->\r\n <p class=\"mb-4 text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}:\r\n <span class=\"break-all font-mono\">{{ testRequestId() }}</span>\r\n </p>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (showsAcceptedCaveat(testResult)) {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "forcedDisabledFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys", "externalValues", "serverFieldErrors"], outputs: ["runtimeMessagesChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1253
1571
|
}
|
|
1254
1572
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, decorators: [{
|
|
1255
1573
|
type: Component,
|
|
@@ -1259,8 +1577,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1259
1577
|
DynamicForm,
|
|
1260
1578
|
Button,
|
|
1261
1579
|
EmailDeliveryStatus,
|
|
1262
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-
|
|
1580
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div\r\n class=\"mb-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n <!-- The GUID this dialog will keep for the whole exchange. It is what a\r\n lost response is retrieved with, so the operator can record it before\r\n sending rather than reconstructing it afterwards. -->\r\n <p class=\"mb-4 text-xs text-muted-color\">\r\n {{ t(\"test-request-id\") }}:\r\n <span class=\"break-all font-mono\">{{ testRequestId() }}</span>\r\n </p>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-md border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (showsAcceptedCaveat(testResult)) {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n" }]
|
|
1263
1581
|
}], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }], connectionName: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionName", required: true }] }] } });
|
|
1582
|
+
const ACCEPTED_DELIVERY_UNCONFIRMED_CODE = 'email.accepted_delivery_unconfirmed';
|
|
1583
|
+
const TEST_REQUEST_CONFLICT_CODE = 'email.delivery_test_request_conflict';
|
|
1584
|
+
function isTestRequestConflict(error) {
|
|
1585
|
+
const body = error instanceof HttpErrorResponse ? error.error : error;
|
|
1586
|
+
return (typeof body === 'object' &&
|
|
1587
|
+
body !== null &&
|
|
1588
|
+
body.code === TEST_REQUEST_CONFLICT_CODE);
|
|
1589
|
+
}
|
|
1264
1590
|
const POLL_DELAYS_MS = [1_000, 2_000, 4_000, 5_000, 5_000, 5_000];
|
|
1265
1591
|
function isPendingRequestState(value) {
|
|
1266
1592
|
return !['Completed', 'Failed', 'Cancelled', 'Canceled'].includes(value);
|
|
@@ -1419,30 +1745,24 @@ class EmailConnectionEditor {
|
|
|
1419
1745
|
canSave = computed(() => !this.editorLoading() &&
|
|
1420
1746
|
this.formStatus() === 'VALID' &&
|
|
1421
1747
|
!this.facade.mutationLoading(), ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
1748
|
+
configurationFields = computed(() => this.buildConfigurationFields(), ...(ngDevMode ? [{ debugName: "configurationFields" }] : /* istanbul ignore next */ []));
|
|
1749
|
+
/**
|
|
1750
|
+
* Empty whenever the selected mode authenticates without a stored secret —
|
|
1751
|
+
* an anonymous QC-test SMTP endpoint, for instance. That is a valid provider
|
|
1752
|
+
* descriptor, not a load failure.
|
|
1753
|
+
*/
|
|
1754
|
+
credentialFields = computed(() => this.buildCredentialFields(), ...(ngDevMode ? [{ debugName: "credentialFields" }] : /* istanbul ignore next */ []));
|
|
1755
|
+
/**
|
|
1756
|
+
* A section header over an empty body reads as something that failed to
|
|
1757
|
+
* load, so a section with no applicable fields is not rendered at all.
|
|
1758
|
+
*/
|
|
1422
1759
|
formConfig = computed(() => ({
|
|
1760
|
+
layout: SECTION_MODE_LAYOUT,
|
|
1423
1761
|
sections: [
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
columns: 12,
|
|
1429
|
-
fields: this.buildBasicFields(),
|
|
1430
|
-
},
|
|
1431
|
-
{
|
|
1432
|
-
key: 'configuration',
|
|
1433
|
-
label: this.t('editor.configuration'),
|
|
1434
|
-
type: 'header',
|
|
1435
|
-
columns: 12,
|
|
1436
|
-
fields: this.buildConfigurationFields(),
|
|
1437
|
-
},
|
|
1438
|
-
{
|
|
1439
|
-
key: 'credentials',
|
|
1440
|
-
label: this.t('editor.credentials'),
|
|
1441
|
-
type: 'header',
|
|
1442
|
-
columns: 12,
|
|
1443
|
-
fields: this.buildCredentialFields(),
|
|
1444
|
-
},
|
|
1445
|
-
],
|
|
1762
|
+
this.section('basics', this.buildBasicFields()),
|
|
1763
|
+
this.section('configuration', this.configurationFields()),
|
|
1764
|
+
this.section('credentials', this.credentialFields()),
|
|
1765
|
+
].filter((section) => section.fields.length > 0),
|
|
1446
1766
|
}), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
|
|
1447
1767
|
constructor() {
|
|
1448
1768
|
this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
|
@@ -1583,6 +1903,22 @@ class EmailConnectionEditor {
|
|
|
1583
1903
|
});
|
|
1584
1904
|
return reset;
|
|
1585
1905
|
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Every section wears the same shell mt-client-form gives its sections — a
|
|
1908
|
+
* bordered, flat, rounded-md card with a header strip — so the connection
|
|
1909
|
+
* form reads like the runtime forms in the rest of the product.
|
|
1910
|
+
*/
|
|
1911
|
+
section(key, fields) {
|
|
1912
|
+
return {
|
|
1913
|
+
key,
|
|
1914
|
+
label: this.t(`editor.${key}`),
|
|
1915
|
+
type: 'header',
|
|
1916
|
+
columns: 12,
|
|
1917
|
+
headerClass: SECTION_MODE_HEADER_CLASS,
|
|
1918
|
+
bodyClass: SECTION_MODE_BODY_CLASS,
|
|
1919
|
+
fields,
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1586
1922
|
buildBasicFields() {
|
|
1587
1923
|
const provider = this.selectedProvider();
|
|
1588
1924
|
return [
|
|
@@ -1809,12 +2145,24 @@ class EmailConnectionEditor {
|
|
|
1809
2145
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
1810
2146
|
}
|
|
1811
2147
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1812
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionEditor, isStandalone: true, selector: "mt-email-connection-editor", inputs: { editorConnectionId: { classPropertyName: "editorConnectionId", publicName: "editorConnectionId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <
|
|
2148
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionEditor, isStandalone: true, selector: "mt-email-connection-editor", inputs: { editorConnectionId: { classPropertyName: "editorConnectionId", publicName: "editorConnectionId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n @if (editorLoading()) {\r\n <div\r\n class=\"flex flex-col gap-5\"\r\n role=\"status\"\r\n [attr.aria-label]=\"t('loading')\"\r\n >\r\n @for (section of [1, 2, 3]; track section) {\r\n <div class=\"overflow-hidden rounded-md border border-surface-200\">\r\n <div class=\"border-b border-surface-200 bg-surface-50 px-4 py-3\">\r\n <p-skeleton\r\n width=\"10rem\"\r\n height=\"1.25rem\"\r\n borderRadius=\"0.375rem\"\r\n />\r\n </div>\r\n <div class=\"grid gap-4 p-4 sm:grid-cols-2\">\r\n @for (field of [1, 2, 3, 4]; track field) {\r\n <p-skeleton height=\"3.25rem\" borderRadius=\"0.375rem\" />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul\r\n class=\"mb-0 mt-2.5 list-disc space-y-2 ps-5 text-sm leading-relaxed text-amber-900\"\r\n >\r\n @for (warning of provider.warnings; track warning) {\r\n <li class=\"ps-1\">{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (applicableFields().length === 0) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "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 });
|
|
1813
2149
|
}
|
|
1814
2150
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, decorators: [{
|
|
1815
2151
|
type: Component,
|
|
1816
|
-
args: [{ selector: 'mt-email-connection-editor', imports: [
|
|
2152
|
+
args: [{ selector: 'mt-email-connection-editor', imports: [
|
|
2153
|
+
ReactiveFormsModule,
|
|
2154
|
+
TranslocoDirective,
|
|
2155
|
+
SkeletonModule,
|
|
2156
|
+
DynamicForm,
|
|
2157
|
+
Button,
|
|
2158
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n @if (editorLoading()) {\r\n <div\r\n class=\"flex flex-col gap-5\"\r\n role=\"status\"\r\n [attr.aria-label]=\"t('loading')\"\r\n >\r\n @for (section of [1, 2, 3]; track section) {\r\n <div class=\"overflow-hidden rounded-md border border-surface-200\">\r\n <div class=\"border-b border-surface-200 bg-surface-50 px-4 py-3\">\r\n <p-skeleton\r\n width=\"10rem\"\r\n height=\"1.25rem\"\r\n borderRadius=\"0.375rem\"\r\n />\r\n </div>\r\n <div class=\"grid gap-4 p-4 sm:grid-cols-2\">\r\n @for (field of [1, 2, 3, 4]; track field) {\r\n <p-skeleton height=\"3.25rem\" borderRadius=\"0.375rem\" />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul\r\n class=\"mb-0 mt-2.5 list-disc space-y-2 ps-5 text-sm leading-relaxed text-amber-900\"\r\n >\r\n @for (warning of provider.warnings; track warning) {\r\n <li class=\"ps-1\">{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (applicableFields().length === 0) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
|
|
1817
2159
|
}], ctorParameters: () => [], propDecorators: { editorConnectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorConnectionId", required: false }] }] } });
|
|
2160
|
+
// Mirrors the section-mode styling mt-client-form applies to its own sections.
|
|
2161
|
+
const SECTION_MODE_LAYOUT = {
|
|
2162
|
+
sectionClass: 'flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0',
|
|
2163
|
+
};
|
|
2164
|
+
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';
|
|
2165
|
+
const SECTION_MODE_BODY_CLASS = '@container grid grid-cols-12 gap-4 items-start p-4';
|
|
1818
2166
|
function emptyEditorValue() {
|
|
1819
2167
|
return {
|
|
1820
2168
|
name: null,
|
|
@@ -1912,6 +2260,34 @@ class EmailConnectionDetailsPage {
|
|
|
1912
2260
|
? this.facade.providerByKey(details.connection.providerKey)
|
|
1913
2261
|
: null;
|
|
1914
2262
|
}, ...(ngDevMode ? [{ debugName: "provider" }] : /* istanbul ignore next */ []));
|
|
2263
|
+
/**
|
|
2264
|
+
* The provider descriptor's own warnings, shown here and not only while
|
|
2265
|
+
* editing: a connection whose mode carries a standing caveat — the QC SMTP
|
|
2266
|
+
* endpoint that runs without TLS or authentication, for instance — must
|
|
2267
|
+
* still say so to whoever opens it later.
|
|
2268
|
+
*/
|
|
2269
|
+
providerWarnings = computed(() => this.provider()?.warnings ?? [], ...(ngDevMode ? [{ debugName: "providerWarnings" }] : /* istanbul ignore next */ []));
|
|
2270
|
+
/**
|
|
2271
|
+
* The contract assigns each connection state one next step. Saying it in
|
|
2272
|
+
* words means the chip is not the only place the operator can read what to
|
|
2273
|
+
* do, which is also what keeps the state legible without relying on color.
|
|
2274
|
+
*/
|
|
2275
|
+
stateGuidance = computed(() => {
|
|
2276
|
+
this.activeLanguage();
|
|
2277
|
+
const details = this.facade.selectedConnection();
|
|
2278
|
+
if (!details)
|
|
2279
|
+
return '';
|
|
2280
|
+
return this.optionalT(`state-guidance.${toKebabCase$1(details.connection.state)}`);
|
|
2281
|
+
}, ...(ngDevMode ? [{ debugName: "stateGuidance" }] : /* istanbul ignore next */ []));
|
|
2282
|
+
/**
|
|
2283
|
+
* The last validation code, so an invalid connection shows the reason rather
|
|
2284
|
+
* than only a chip that says something went wrong.
|
|
2285
|
+
*/
|
|
2286
|
+
lastValidationMessage = computed(() => {
|
|
2287
|
+
this.activeLanguage();
|
|
2288
|
+
const code = this.facade.selectedConnection()?.connection.lastValidationCode;
|
|
2289
|
+
return code ? this.facade.describeCode(code) : '';
|
|
2290
|
+
}, ...(ngDevMode ? [{ debugName: "lastValidationMessage" }] : /* istanbul ignore next */ []));
|
|
1915
2291
|
authenticationModeLabel = computed(() => {
|
|
1916
2292
|
const details = this.facade.selectedConnection();
|
|
1917
2293
|
if (!details)
|
|
@@ -2023,7 +2399,7 @@ class EmailConnectionDetailsPage {
|
|
|
2023
2399
|
edit() {
|
|
2024
2400
|
const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
|
|
2025
2401
|
header: this.t('editor.edit-title'),
|
|
2026
|
-
styleClass:
|
|
2402
|
+
styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
|
|
2027
2403
|
position: 'end',
|
|
2028
2404
|
appendTo: 'page-content',
|
|
2029
2405
|
dismissableMask: true,
|
|
@@ -2122,6 +2498,12 @@ class EmailConnectionDetailsPage {
|
|
|
2122
2498
|
t(key) {
|
|
2123
2499
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
2124
2500
|
}
|
|
2501
|
+
/** Empty rather than the raw key when a state has no guidance sentence. */
|
|
2502
|
+
optionalT(key) {
|
|
2503
|
+
const fullKey = `email-delivery.${key}`;
|
|
2504
|
+
const translated = this.transloco.translate(fullKey);
|
|
2505
|
+
return translated === fullKey ? '' : translated;
|
|
2506
|
+
}
|
|
2125
2507
|
credentialStorageLabel(storageKind) {
|
|
2126
2508
|
switch (storageKind.replace(/[-_\s]/g, '').toLowerCase()) {
|
|
2127
2509
|
case 'externalreference':
|
|
@@ -2140,20 +2522,23 @@ class EmailConnectionDetailsPage {
|
|
|
2140
2522
|
return field?.dataType === 'Choice' ? humanizeIdentifier(value) : value;
|
|
2141
2523
|
}
|
|
2142
2524
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2143
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionDetailsPage, isStandalone: true, selector: "mt-email-connection-details", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n }\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-lg border border-surface-200 bg-surface-50 p-4 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"text-sm text-muted-color\">{{ t(\"no-credentials\") }}</p>\r\n }\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Menu, selector: "mt-menu", inputs: ["model", "popup", "appendTo", "style", "styleClass", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex", "activeItemId"], outputs: ["activeItemIdChange", "onShow", "onHide", "onBlur", "onFocus", "onItemSelect"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2525
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailConnectionDetailsPage, isStandalone: true, selector: "mt-email-connection-details", inputs: { connectionId: { classPropertyName: "connectionId", publicName: "connectionId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div\r\n [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\"\r\n role=\"status\"\r\n [attr.aria-label]=\"t('loading')\"\r\n >\r\n <div class=\"grid w-full gap-5\">\r\n <p-skeleton height=\"5rem\" borderRadius=\"0.375rem\" />\r\n @for (section of [1, 2, 3]; track section) {\r\n <div class=\"overflow-hidden rounded-md border border-surface-200\">\r\n <div class=\"border-b border-surface-200 bg-surface-50 px-4 py-3\">\r\n <p-skeleton\r\n width=\"12rem\"\r\n height=\"1.25rem\"\r\n borderRadius=\"0.375rem\"\r\n />\r\n </div>\r\n <div class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (tile of [1, 2, 3]; track tile) {\r\n <p-skeleton height=\"4rem\" borderRadius=\"0.375rem\" />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-md border border-surface-200 bg-surface-0 p-4 sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n <!-- The next step in words, so the state is never carried by the\r\n chip's color alone. -->\r\n @if (stateGuidance(); as guidance) {\r\n <p class=\"mb-0 mt-2 text-sm text-color\">{{ guidance }}</p>\r\n }\r\n @if (lastValidationMessage(); as lastResult) {\r\n <p class=\"mb-0 mt-1 text-sm text-muted-color\">\r\n {{ t(\"last-result\") }}: {{ lastResult }}\r\n </p>\r\n }\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 self-start\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <!-- The descriptor's standing caveats for this provider and mode. The\r\n editor shows them while configuring; they belong here too, because\r\n whoever opens an existing connection is the one deciding whether\r\n it may carry production notification email. -->\r\n @if (providerWarnings().length > 0) {\r\n <section\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4\"\r\n role=\"note\"\r\n aria-labelledby=\"connection-provider-warnings-heading\"\r\n >\r\n <h3\r\n id=\"connection-provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h3>\r\n <ul\r\n class=\"mb-0 mt-2.5 list-disc space-y-2 ps-5 text-sm leading-relaxed text-amber-900\"\r\n >\r\n @for (warning of providerWarnings(); track warning) {\r\n <li class=\"ps-1\">{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n\r\n <!-- Same section shape the connection form uses (mt-client-form section\r\n mode): header strip on a bordered, flat, rounded-md container. -->\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n }\r\n\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3 p-4\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-md border border-surface-200 bg-surface-50 p-3 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"no-credentials\") }}\r\n </p>\r\n }\r\n </div>\r\n </section>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "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 });
|
|
2144
2526
|
}
|
|
2145
2527
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, decorators: [{
|
|
2146
2528
|
type: Component,
|
|
2147
2529
|
args: [{ selector: 'mt-email-connection-details', imports: [
|
|
2148
2530
|
DatePipe,
|
|
2149
2531
|
TranslocoDirective,
|
|
2532
|
+
SkeletonModule,
|
|
2150
2533
|
Button,
|
|
2151
|
-
Card,
|
|
2152
2534
|
EmptyState,
|
|
2153
2535
|
Menu,
|
|
2154
2536
|
EmailDeliveryStatus,
|
|
2155
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n }\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-lg border border-surface-200 bg-surface-50 p-4 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"text-sm text-muted-color\">{{ t(\"no-credentials\") }}</p>\r\n }\r\n </div>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
|
|
2537
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (facade.detailsLoading()) {\r\n <div\r\n [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\"\r\n role=\"status\"\r\n [attr.aria-label]=\"t('loading')\"\r\n >\r\n <div class=\"grid w-full gap-5\">\r\n <p-skeleton height=\"5rem\" borderRadius=\"0.375rem\" />\r\n @for (section of [1, 2, 3]; track section) {\r\n <div class=\"overflow-hidden rounded-md border border-surface-200\">\r\n <div class=\"border-b border-surface-200 bg-surface-50 px-4 py-3\">\r\n <p-skeleton\r\n width=\"12rem\"\r\n height=\"1.25rem\"\r\n borderRadius=\"0.375rem\"\r\n />\r\n </div>\r\n <div class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (tile of [1, 2, 3]; track tile) {\r\n <p-skeleton height=\"4rem\" borderRadius=\"0.375rem\" />\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (facade.selectedConnection(); as details) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <section\r\n class=\"flex flex-col gap-3 rounded-md border border-surface-200 bg-surface-0 p-4 sm:flex-row sm:items-center sm:justify-between\"\r\n >\r\n <div class=\"min-w-0\">\r\n <h2 class=\"m-0 truncate text-lg font-semibold text-color\">\r\n {{ details.connection.name }}\r\n </h2>\r\n <p class=\"mb-0 mt-1 break-all text-sm text-muted-color\">\r\n {{ details.connection.senderAddress }}\r\n </p>\r\n <!-- The next step in words, so the state is never carried by the\r\n chip's color alone. -->\r\n @if (stateGuidance(); as guidance) {\r\n <p class=\"mb-0 mt-2 text-sm text-color\">{{ guidance }}</p>\r\n }\r\n @if (lastValidationMessage(); as lastResult) {\r\n <p class=\"mb-0 mt-1 text-sm text-muted-color\">\r\n {{ t(\"last-result\") }}: {{ lastResult }}\r\n </p>\r\n }\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 self-start\"\r\n kind=\"connection\"\r\n [value]=\"details.connection.state\"\r\n />\r\n </section>\r\n\r\n <!-- The descriptor's standing caveats for this provider and mode. The\r\n editor shows them while configuring; they belong here too, because\r\n whoever opens an existing connection is the one deciding whether\r\n it may carry production notification email. -->\r\n @if (providerWarnings().length > 0) {\r\n <section\r\n class=\"rounded-md border border-amber-200 bg-amber-50 p-4\"\r\n role=\"note\"\r\n aria-labelledby=\"connection-provider-warnings-heading\"\r\n >\r\n <h3\r\n id=\"connection-provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h3>\r\n <ul\r\n class=\"mb-0 mt-2.5 list-disc space-y-2 ps-5 text-sm leading-relaxed text-amber-900\"\r\n >\r\n @for (warning of providerWarnings(); track warning) {\r\n <li class=\"ps-1\">{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n\r\n <!-- Same section shape the connection form uses (mt-client-form section\r\n mode): header strip on a bordered, flat, rounded-md container. -->\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"connection-overview\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ provider()?.displayName || details.connection.providerKey }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.authentication-mode\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ authenticationModeLabel() }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"used-for-notifications\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.bindings.includes(\"notifications\")\r\n ? t(\"yes\")\r\n : t(\"no\")\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.last-validation\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n {{\r\n details.connection.lastValidatedAtUtc\r\n ? (details.connection.lastValidatedAtUtc | date: \"medium\")\r\n : \"\u2014\"\r\n }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-md border border-surface-200 bg-surface-50 p-3\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"authorization-state\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.authorizationState || \"\u2014\" }}\r\n </dd>\r\n </div>\r\n @if (details.connection.senderDisplayName) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"connection.sender-display-name\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ details.connection.senderDisplayName }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n\r\n @if (publicConfiguration().length > 0) {\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"provider-settings\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3\">\r\n @for (entry of publicConfiguration(); track entry.key) {\r\n <div\r\n class=\"rounded-md border border-surface-200 bg-surface-50 p-3\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">{{ entry.label }}</dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ entry.value }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </section>\r\n }\r\n\r\n <section\r\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\r\n >\r\n <h3\r\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\r\n >\r\n {{ t(\"credential-status\") }}\r\n </h3>\r\n <div class=\"grid gap-3 p-4\">\r\n @for (credential of credentialItems(); track credential.key) {\r\n <div\r\n class=\"grid gap-3 rounded-md border border-surface-200 bg-surface-50 p-3 sm:grid-cols-3 sm:items-center\"\r\n >\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 break-words font-medium text-color\">\r\n {{ credential.label }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n {{ credential.key }}\r\n </p>\r\n </div>\r\n <div class=\"min-w-0\">\r\n <p class=\"m-0 text-sm font-medium text-color\">\r\n {{ credential.storageLabel }}\r\n </p>\r\n <p class=\"mb-0 mt-1 text-xs text-muted-color\">\r\n @if (credential.lastRotatedAtUtc) {\r\n {{ t(\"last-rotated\") }} \u00B7\r\n {{ credential.lastRotatedAtUtc | date: \"medium\" }}\r\n } @else {\r\n {{ t(\"last-rotated\") }} \u00B7 \u2014\r\n }\r\n </p>\r\n </div>\r\n <mt-email-delivery-status\r\n class=\"shrink-0 sm:justify-self-end\"\r\n kind=\"support\"\r\n [value]=\"\r\n credential.configured ? 'configured' : 'not-configured'\r\n \"\r\n />\r\n </div>\r\n } @empty {\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"no-credentials\") }}\r\n </p>\r\n }\r\n </div>\r\n </section>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (actionMenuItems().length > 0) {\r\n <mt-button\r\n [label]=\"t('actions')\"\r\n icon=\"general.dots-vertical\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"actionMenu.toggle($event)\"\r\n />\r\n <mt-menu\r\n #actionMenu\r\n [model]=\"actionMenuItems()\"\r\n appendTo=\"body\"\r\n [ariaLabel]=\"t('actions')\"\r\n />\r\n }\r\n <mt-button\r\n [label]=\"t('edit')\"\r\n icon=\"general.edit-01\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"edit()\"\r\n />\r\n </div>\r\n } @else {\r\n <div [class]=\"modal.contentClass + ' p-4 sm:p-5'\">\r\n <mt-empty-state illustration=\"none\" [title]=\"t('connection-not-found')\" />\r\n </div>\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button [label]=\"t('close')\" variant=\"outlined\" (onClick)=\"close()\" />\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
|
|
2156
2538
|
}], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }] } });
|
|
2539
|
+
function toKebabCase$1(value) {
|
|
2540
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
2541
|
+
}
|
|
2157
2542
|
function humanizeIdentifier(value) {
|
|
2158
2543
|
return value
|
|
2159
2544
|
.replace(/[._-]+/g, ' ')
|
|
@@ -2177,6 +2562,10 @@ class EmailDeliveryConnections {
|
|
|
2177
2562
|
initialValue: this.transloco.getActiveLang(),
|
|
2178
2563
|
});
|
|
2179
2564
|
canConfigure = this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel);
|
|
2565
|
+
/** One rule for both the caption action and the empty-state action. */
|
|
2566
|
+
canCreate = computed(() => this.canConfigure &&
|
|
2567
|
+
this.facade.managementAvailable() &&
|
|
2568
|
+
this.facade.providers().length > 0, ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
|
|
2180
2569
|
rows = computed(() => this.facade.connections().map((connection) => ({
|
|
2181
2570
|
...connection,
|
|
2182
2571
|
providerLabel: this.facade.providerByKey(connection.providerKey)?.displayName ??
|
|
@@ -2195,6 +2584,7 @@ class EmailDeliveryConnections {
|
|
|
2195
2584
|
{
|
|
2196
2585
|
key: 'providerLabel',
|
|
2197
2586
|
label: this.t('connection.provider'),
|
|
2587
|
+
width: '18rem',
|
|
2198
2588
|
filterConfig: {
|
|
2199
2589
|
type: 'select',
|
|
2200
2590
|
options: this.facade.providers().map((provider) => ({
|
|
@@ -2206,12 +2596,14 @@ class EmailDeliveryConnections {
|
|
|
2206
2596
|
{
|
|
2207
2597
|
key: 'senderAddress',
|
|
2208
2598
|
label: this.t('connection.sender'),
|
|
2599
|
+
width: '18rem',
|
|
2209
2600
|
filterConfig: { type: 'text' },
|
|
2210
2601
|
},
|
|
2211
2602
|
{
|
|
2212
2603
|
key: 'state',
|
|
2213
2604
|
label: this.t('connection.state'),
|
|
2214
2605
|
type: 'status',
|
|
2606
|
+
width: '15rem',
|
|
2215
2607
|
filterConfig: {
|
|
2216
2608
|
type: 'select',
|
|
2217
2609
|
options: connectionStates.map((state) => ({
|
|
@@ -2223,8 +2615,9 @@ class EmailDeliveryConnections {
|
|
|
2223
2615
|
},
|
|
2224
2616
|
{
|
|
2225
2617
|
key: 'usedForNotifications',
|
|
2226
|
-
label: this.t('
|
|
2618
|
+
label: this.t('in-use'),
|
|
2227
2619
|
type: 'boolean',
|
|
2620
|
+
width: '10rem',
|
|
2228
2621
|
readonly: true,
|
|
2229
2622
|
filterConfig: { type: 'boolean' },
|
|
2230
2623
|
},
|
|
@@ -2232,6 +2625,7 @@ class EmailDeliveryConnections {
|
|
|
2232
2625
|
key: 'lastValidatedAtUtc',
|
|
2233
2626
|
label: this.t('connection.last-validation'),
|
|
2234
2627
|
type: 'dateTime',
|
|
2628
|
+
width: '15rem',
|
|
2235
2629
|
},
|
|
2236
2630
|
];
|
|
2237
2631
|
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
@@ -2299,7 +2693,7 @@ class EmailDeliveryConnections {
|
|
|
2299
2693
|
openDetails(connection) {
|
|
2300
2694
|
const drawer = this.modal.openModal(EmailConnectionDetailsPage, 'drawer', {
|
|
2301
2695
|
header: this.t('connection-details'),
|
|
2302
|
-
styleClass:
|
|
2696
|
+
styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
|
|
2303
2697
|
position: 'end',
|
|
2304
2698
|
appendTo: 'page-content',
|
|
2305
2699
|
dismissableMask: true,
|
|
@@ -2361,7 +2755,7 @@ class EmailDeliveryConnections {
|
|
|
2361
2755
|
openEditor(connectionId) {
|
|
2362
2756
|
const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
|
|
2363
2757
|
header: this.t(connectionId ? 'editor.edit-title' : 'editor.create-title'),
|
|
2364
|
-
styleClass:
|
|
2758
|
+
styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
|
|
2365
2759
|
position: 'end',
|
|
2366
2760
|
appendTo: 'page-content',
|
|
2367
2761
|
dismissableMask: true,
|
|
@@ -2383,11 +2777,11 @@ class EmailDeliveryConnections {
|
|
|
2383
2777
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
2384
2778
|
}
|
|
2385
2779
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2386
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryConnections, isStandalone: true, selector: "mt-email-delivery-connections", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-
|
|
2780
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryConnections, isStandalone: true, selector: "mt-email-delivery-connections", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-label=\"{{ t('connections-title') }}\">\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-connections-title')\"\r\n [emptyDescription]=\"t('no-connections-description')\"\r\n [emptyActionLabel]=\"canCreate() ? t('create-connection') : undefined\"\r\n emptyActionIcon=\"general.plus\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (emptyAction)=\"createConnection()\"\r\n (rowClick)=\"openDetails($event)\"\r\n >\r\n <ng-template #captionEnd>\r\n @if (canCreate()) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </ng-template>\r\n </mt-table>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2387
2781
|
}
|
|
2388
2782
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, decorators: [{
|
|
2389
2783
|
type: Component,
|
|
2390
|
-
args: [{ selector: 'mt-email-delivery-connections', imports: [TranslocoDirective, Button, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-
|
|
2784
|
+
args: [{ selector: 'mt-email-delivery-connections', imports: [TranslocoDirective, Button, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section class=\"space-y-4\" aria-label=\"{{ t('connections-title') }}\">\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n [emptyTitle]=\"t('no-connections-title')\"\r\n [emptyDescription]=\"t('no-connections-description')\"\r\n [emptyActionLabel]=\"canCreate() ? t('create-connection') : undefined\"\r\n emptyActionIcon=\"general.plus\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (emptyAction)=\"createConnection()\"\r\n (rowClick)=\"openDetails($event)\"\r\n >\r\n <ng-template #captionEnd>\r\n @if (canCreate()) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </ng-template>\r\n </mt-table>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
|
|
2391
2785
|
}] });
|
|
2392
2786
|
const connectionStates = [
|
|
2393
2787
|
'Draft',
|
|
@@ -2421,6 +2815,43 @@ class EmailDeliveryShell {
|
|
|
2421
2815
|
{ label: this.t('tabs.activity'), value: 'activity' },
|
|
2422
2816
|
];
|
|
2423
2817
|
}, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
|
|
2818
|
+
/**
|
|
2819
|
+
* Same trail the sibling control-panel pages render, so Email Delivery reads
|
|
2820
|
+
* as part of Product Settings rather than a standalone screen.
|
|
2821
|
+
*/
|
|
2822
|
+
breadcrumbItems = computed(() => {
|
|
2823
|
+
this.activeLanguage();
|
|
2824
|
+
return [
|
|
2825
|
+
{
|
|
2826
|
+
label: '',
|
|
2827
|
+
icon: 'general.home-line',
|
|
2828
|
+
routerLink: '/control-panel/workspaces',
|
|
2829
|
+
},
|
|
2830
|
+
{
|
|
2831
|
+
label: this.t('breadcrumb.product-settings'),
|
|
2832
|
+
routerLink: PRODUCT_SETTINGS_ROUTE,
|
|
2833
|
+
},
|
|
2834
|
+
{ label: this.t('title') },
|
|
2835
|
+
];
|
|
2836
|
+
}, ...(ngDevMode ? [{ debugName: "breadcrumbItems" }] : /* istanbul ignore next */ []));
|
|
2837
|
+
/**
|
|
2838
|
+
* Readiness moved out of a page-wide banner and onto a chip beside the title.
|
|
2839
|
+
* The banner's explanation survives as the chip's tooltip, so the reason is
|
|
2840
|
+
* still one hover away without a block of text above every tab.
|
|
2841
|
+
*/
|
|
2842
|
+
readiness = computed(() => {
|
|
2843
|
+
const status = this.facade.status();
|
|
2844
|
+
if (!status)
|
|
2845
|
+
return null;
|
|
2846
|
+
if (!status.managementEnabled)
|
|
2847
|
+
return 'unavailable';
|
|
2848
|
+
return status.isReady ? 'ready' : 'needs-setup';
|
|
2849
|
+
}, ...(ngDevMode ? [{ debugName: "readiness" }] : /* istanbul ignore next */ []));
|
|
2850
|
+
readinessHint = computed(() => {
|
|
2851
|
+
this.activeLanguage();
|
|
2852
|
+
const status = this.facade.status();
|
|
2853
|
+
return status ? this.facade.describeCode(status.code) : '';
|
|
2854
|
+
}, ...(ngDevMode ? [{ debugName: "readinessHint" }] : /* istanbul ignore next */ []));
|
|
2424
2855
|
constructor() {
|
|
2425
2856
|
this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
|
|
2426
2857
|
this.activeTab.set(params.get('tab') === 'activity' ? 'activity' : 'connections');
|
|
@@ -2443,7 +2874,7 @@ class EmailDeliveryShell {
|
|
|
2443
2874
|
});
|
|
2444
2875
|
}
|
|
2445
2876
|
goBack() {
|
|
2446
|
-
void this.router.navigate([
|
|
2877
|
+
void this.router.navigate([PRODUCT_SETTINGS_ROUTE]);
|
|
2447
2878
|
}
|
|
2448
2879
|
syncTabFromUrl() {
|
|
2449
2880
|
this.activeTab.set(this.route.snapshot.queryParamMap.get('tab') === 'activity'
|
|
@@ -2484,18 +2915,22 @@ class EmailDeliveryShell {
|
|
|
2484
2915
|
return this.transloco.translate(`email-delivery.${key}`);
|
|
2485
2916
|
}
|
|
2486
2917
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2487
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryShell, isStandalone: true, selector: "mt-email-delivery-shell", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n
|
|
2918
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryShell, isStandalone: true, selector: "mt-email-delivery-shell", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n <!-- Declared unconditionally: a content query cannot reach a template that\r\n only exists inside a control-flow block, so the condition lives here. -->\r\n <ng-template #titleEnd>\r\n @if (canView && readiness(); as state) {\r\n <span class=\"shrink-0\" [mtTooltip]=\"readinessHint()\">\r\n <mt-email-delivery-status kind=\"readiness\" [value]=\"state\" />\r\n </span>\r\n }\r\n </ng-template>\r\n\r\n @if (canView) {\r\n <div\r\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\"\r\n >\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems()\"\r\n styleClass=\"mx-1 flex justify-start\"\r\n />\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n </div>\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n", dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: 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 });
|
|
2488
2919
|
}
|
|
2489
2920
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, decorators: [{
|
|
2490
2921
|
type: Component,
|
|
2491
2922
|
args: [{ selector: 'mt-email-delivery-shell', imports: [
|
|
2492
2923
|
Page,
|
|
2493
2924
|
TranslocoDirective,
|
|
2925
|
+
Breadcrumb,
|
|
2494
2926
|
EmptyState,
|
|
2927
|
+
Tooltip,
|
|
2928
|
+
EmailDeliveryStatus,
|
|
2495
2929
|
EmailDeliveryConnections,
|
|
2496
2930
|
EmailDeliveryActivity,
|
|
2497
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n
|
|
2931
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n <!-- Declared unconditionally: a content query cannot reach a template that\r\n only exists inside a control-flow block, so the condition lives here. -->\r\n <ng-template #titleEnd>\r\n @if (canView && readiness(); as state) {\r\n <span class=\"shrink-0\" [mtTooltip]=\"readinessHint()\">\r\n <mt-email-delivery-status kind=\"readiness\" [value]=\"state\" />\r\n </span>\r\n }\r\n </ng-template>\r\n\r\n @if (canView) {\r\n <div\r\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\"\r\n >\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems()\"\r\n styleClass=\"mx-1 flex justify-start\"\r\n />\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n </div>\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n" }]
|
|
2498
2932
|
}], ctorParameters: () => [] });
|
|
2933
|
+
const PRODUCT_SETTINGS_ROUTE = '/control-panel/product-settings';
|
|
2499
2934
|
|
|
2500
2935
|
export { EmailDeliveryShell };
|
|
2501
|
-
//# sourceMappingURL=masterteam-system-settings-email-delivery-shell-
|
|
2936
|
+
//# sourceMappingURL=masterteam-system-settings-email-delivery-shell-C_AhRF1f.mjs.map
|