@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.
@@ -1,31 +1,137 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, signal, computed, input, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
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) => isEmailSendResult(response)
78
- ? of(response)
79
- : throwError(() => response)), catchError((error) => {
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) => this.lastConnectionTest.set(result)), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), finalize(() => this.mutationLoading.set(null)));
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) => this.lastConnectionTest.set(stored.result)), finalize(() => this.mutationLoading.set(null)));
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 problem = extractProblem(error);
399
- const base = this.describeCode(problem?.code);
400
- return problem?.correlationId
401
- ? `${base} (${this.transloco.translate('email-delivery.correlation-id')}: ${problem.correlationId})`
402
- : base;
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 problem = extractProblem(error);
406
- if (!problem)
407
- return null;
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
- null);
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
- function extractProblem(error) {
450
- if (error instanceof HttpErrorResponse) {
451
- const value = error.error;
452
- return value && typeof value.code === 'string'
453
- ? value
454
- : null;
455
- }
456
- if (typeof error === 'object' &&
457
- error !== null &&
458
- typeof error.code === 'string') {
459
- return error;
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-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n [hint]=\"t('exceptional-override-hint')\"\r\n />\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <p class=\"m-0 break-words font-semibold\">{{ itemName() }}</p>\r\n <p class=\"mb-0 mt-1\">{{ consequence() }}</p>\r\n </div>\r\n\r\n <mt-textarea-field\r\n [formControl]=\"reason\"\r\n [label]=\"t('operator-reason')\"\r\n [hint]=\"t('operator-reason-hint')\"\r\n [required]=\"true\"\r\n [maxLength]=\"500\"\r\n rows=\"4\"\r\n />\r\n\r\n @if (showExceptionalOverride()) {\r\n <mt-toggle-field\r\n [formControl]=\"exceptionalOverride\"\r\n [label]=\"t('exceptional-override')\"\r\n [hint]=\"t('exceptional-override-hint')\"\r\n />\r\n }\r\n </form>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n type=\"button\"\r\n severity=\"warn\"\r\n [label]=\"confirmLabel() || t('confirm-action')\"\r\n [disabled]=\"!canConfirm()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"confirm()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
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-lg border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n </dl>\r\n </mt-card>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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, Card, EmailDeliveryStatus], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n @if (attempt(); as item) {\r\n <div [class]=\"'min-h-0 overflow-y-auto p-4 sm:p-5 ' + modal.contentClass\">\r\n <div class=\"grid w-full gap-5\">\r\n <div\r\n class=\"rounded-lg border px-4 py-3 text-sm\"\r\n [class.border-amber-200]=\"item.status === 'OutcomeUnknown'\"\r\n [class.bg-amber-50]=\"item.status === 'OutcomeUnknown'\"\r\n [class.border-blue-200]=\"item.status !== 'OutcomeUnknown'\"\r\n [class.bg-blue-50]=\"item.status !== 'OutcomeUnknown'\"\r\n role=\"status\"\r\n >\r\n @if (item.status === \"OutcomeUnknown\") {\r\n {{ t(\"outcome-unknown-copy\") }}\r\n } @else if (item.outcome === \"Accepted\") {\r\n {{ t(\"accepted-copy\") }}\r\n } @else {\r\n {{ facade.describeCode(item.resultCode) }}\r\n }\r\n </div>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"delivery-summary\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.status\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.status\"\r\n />\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.outcome\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-2\">\r\n @if (item.outcome) {\r\n <mt-email-delivery-status\r\n kind=\"attempt\"\r\n [value]=\"item.outcome\"\r\n />\r\n } @else {\r\n <span class=\"font-medium text-color\">\u2014</span>\r\n }\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.provider\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-words font-medium text-color\">\r\n {{ providerLabel(item.providerKey) }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"attempt.created\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 font-medium text-color\">\r\n <time [attr.datetime]=\"item.createdAtUtc\">{{\r\n item.createdAtUtc | date: \"medium\"\r\n }}</time>\r\n </dd>\r\n </div>\r\n </dl>\r\n </mt-card>\r\n\r\n <mt-card class=\"block\">\r\n <h3 class=\"mb-4 text-base font-semibold text-color\">\r\n {{ t(\"technical-identifiers\") }}\r\n </h3>\r\n <dl class=\"grid gap-3 sm:grid-cols-2\">\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"recipient-fingerprint\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.recipientFingerprint }}\r\n </dd>\r\n </div>\r\n <div class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\">\r\n <dt class=\"text-xs text-muted-color\">{{ t(\"attempt.id\") }}</dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.id }}\r\n </dd>\r\n </div>\r\n @if (item.correlationId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"correlation-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.correlationId }}\r\n </dd>\r\n </div>\r\n }\r\n @if (item.providerRequestId) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 p-4\"\r\n >\r\n <dt class=\"text-xs text-muted-color\">\r\n {{ t(\"provider-request-id\") }}\r\n </dt>\r\n <dd class=\"mb-0 mt-1 break-all font-mono text-sm text-color\">\r\n {{ item.providerRequestId }}\r\n </dd>\r\n </div>\r\n }\r\n </dl>\r\n </mt-card>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('close')\"\r\n variant=\"outlined\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (canReplay()) {\r\n <mt-button\r\n severity=\"warn\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('replay-attempt')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'replay-' + item.id\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"replay()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</ng-container>\r\n" }]
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
- statusOptions = computed(() => {
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
- const statusMap = getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco));
700
- return [
701
- { label: this.t('all-statuses'), value: null },
702
- ...EMAIL_DELIVERY_ATTEMPT_STATUSES.map((value) => ({
703
- label: statusMap[value]?.display ?? value,
704
- value,
705
- })),
706
- ];
707
- }, ...(ngDevMode ? [{ debugName: "statusOptions" }] : /* istanbul ignore next */ []));
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
- statusMap: getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco)),
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
- applyFilter() {
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: '!absolute !shadow-none !w-full !max-w-full',
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, this.status.getRawValue()).subscribe({
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-labelledby=\"attempts-heading\">\r\n <div>\r\n <h2 id=\"attempts-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"activity-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"activity-description\") }}\r\n </p>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-3 sm:flex-row sm:items-end\">\r\n <mt-select-field\r\n class=\"w-full sm:w-72\"\r\n [formControl]=\"status\"\r\n [label]=\"t('attempt.status-filter')\"\r\n [options]=\"statusOptions()\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n />\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('apply-filter')\"\r\n (onClick)=\"applyFilter()\"\r\n />\r\n </div>\r\n\r\n @if (!facade.attemptsLoading() && facade.attempts().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-attempts-title')\"\r\n [description]=\"t('no-attempts-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"facade.attempts()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n [generalSearch]=\"true\"\r\n dataKey=\"id\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n\r\n @if (facade.attemptsNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.attemptsLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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
- { key: 'evidenceKindLabel', label: this.t('evidence-kind') },
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: '20rem',
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\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"host-evidence-heading\"\r\n >\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n @if (!facade.hostEvidenceLoading() && facade.hostEvidence().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-evidence-title')\"\r\n [description]=\"t('no-evidence-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n }\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"host-evidence-heading\"\r\n >\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h3 id=\"host-evidence-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"host-evidence-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"host-evidence-description\") }}\r\n </p>\r\n </div>\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [label]=\"t('rebuild-evidence-index')\"\r\n [disabled]=\"!facade.managementAvailable()\"\r\n [loading]=\"facade.mutationLoading() === 'rebuild-evidence'\"\r\n (onClick)=\"confirmRebuild()\"\r\n />\r\n </div>\r\n\r\n @if (facade.hostEvidenceScanBudgetExhausted()) {\r\n <div\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-950\"\r\n role=\"status\"\r\n >\r\n {{ t(\"scan-budget-exhausted\") }}\r\n </div>\r\n }\r\n\r\n @if (!facade.hostEvidenceLoading() && facade.hostEvidence().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-evidence-title')\"\r\n [description]=\"t('no-evidence-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"evidenceRowId\"\r\n storageKey=\"email-delivery-host-evidence\"\r\n />\r\n }\r\n\r\n @if (facade.hostEvidenceNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.hostEvidenceLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
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: '19rem',
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\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"quarantine-heading\"\r\n >\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n @if (!facade.quarantineLoading() && facade.quarantine().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-quarantine-title')\"\r\n [description]=\"t('no-quarantine-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n }\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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, EmptyState, Table], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <section\r\n class=\"space-y-4 rounded-xl border border-surface-200 bg-surface-0 p-4 shadow-xs sm:p-5\"\r\n aria-labelledby=\"quarantine-heading\"\r\n >\r\n <div>\r\n <h3 id=\"quarantine-heading\" class=\"font-semibold text-color\">\r\n {{ t(\"quarantine-title\") }}\r\n </h3>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"quarantine-description\") }}\r\n </p>\r\n </div>\r\n\r\n @if (!facade.quarantineLoading() && facade.quarantine().length === 0) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-quarantine-title')\"\r\n [description]=\"t('no-quarantine-description')\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"actions()\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n dataKey=\"itemId\"\r\n storageKey=\"email-delivery-quarantine\"\r\n />\r\n }\r\n\r\n @if (facade.quarantineNextCursor()) {\r\n <div class=\"flex justify-center\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('load-more')\"\r\n [loading]=\"facade.quarantineLoading()\"\r\n (onClick)=\"loadMore()\"\r\n />\r\n </div>\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
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-6\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <details\r\n class=\"group overflow-hidden rounded-xl border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"cursor-pointer list-none p-5 font-semibold text-color focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span class=\"flex items-center justify-between gap-3\">\r\n <span>\r\n {{ t(\"advanced-support\") }}\r\n <span class=\"mt-1 block text-sm font-normal text-muted-color\">{{\r\n t(\"advanced-support-description\")\r\n }}</span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"text-base transition-transform group-open:rotate-180\"\r\n />\r\n </span>\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-6 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: EmailDeliveryAttempts, selector: "mt-email-delivery-attempts" }, { kind: "component", type: EmailDeliveryQuarantine, selector: "mt-email-delivery-quarantine" }, { kind: "component", type: EmailDeliveryHostEvidence, selector: "mt-email-delivery-host-evidence" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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-6\">\r\n <mt-email-delivery-attempts class=\"block min-w-0\" />\r\n\r\n <details\r\n class=\"group overflow-hidden rounded-xl border border-surface-200 bg-surface-0\"\r\n (toggle)=\"onAdvancedToggle($event)\"\r\n >\r\n <summary\r\n class=\"cursor-pointer list-none p-5 font-semibold text-color focus-visible:outline-2 focus-visible:outline-offset-2\"\r\n >\r\n <span class=\"flex items-center justify-between gap-3\">\r\n <span>\r\n {{ t(\"advanced-support\") }}\r\n <span class=\"mt-1 block text-sm font-normal text-muted-color\">{{\r\n t(\"advanced-support-description\")\r\n }}</span>\r\n </span>\r\n <mt-icon\r\n icon=\"arrow.chevron-down\"\r\n aria-hidden=\"true\"\r\n class=\"text-base transition-transform group-open:rotate-180\"\r\n />\r\n </span>\r\n </summary>\r\n @if (advancedExpanded()) {\r\n <div\r\n class=\"grid gap-6 border-t border-surface-200 bg-surface-50/60 p-4 sm:p-5\"\r\n >\r\n <mt-email-delivery-quarantine class=\"block min-w-0\" />\r\n <mt-email-delivery-host-evidence class=\"block min-w-0\" />\r\n </div>\r\n }\r\n </details>\r\n </div>\r\n</ng-container>\r\n" }]
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 Pplus email delivery test.',
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-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (testResult.outcome === \"Accepted\") {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "forcedDisabledFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys", "externalValues", "serverFieldErrors"], outputs: ["runtimeMessagesChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950\"\r\n role=\"note\"\r\n >\r\n <strong>{{ t(\"test.real-email-warning-title\") }}</strong>\r\n {{ t(\"test.real-email-warning\") }}\r\n </div>\r\n\r\n @if (!result()) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n\r\n @if (timedOut()) {\r\n <div\r\n class=\"mt-4 rounded-lg border border-orange-200 bg-orange-50 px-4 py-3\"\r\n role=\"alert\"\r\n >\r\n <p class=\"font-semibold text-orange-950\">\r\n {{ t(\"test.timeout-title\") }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-orange-900\">\r\n {{ t(\"test.timeout-description\") }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (result(); as testResult) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-4\"\r\n aria-live=\"polite\"\r\n >\r\n <mt-email-delivery-status kind=\"attempt\" [value]=\"testResult.outcome\" />\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(testResult.code) }}\r\n </p>\r\n @if (testResult.outcome === \"Accepted\") {\r\n <p class=\"mt-3 text-sm font-medium text-amber-800\">\r\n {{ t(\"accepted-unconfirmed\") }}\r\n </p>\r\n }\r\n @if (testResult.outcome === \"OutcomeUnknown\") {\r\n <p class=\"mt-3 text-sm font-medium text-red-700\">\r\n {{ t(\"outcome-unknown-warning\") }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n [label]=\"t('close')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"close()\"\r\n />\r\n @if (timedOut()) {\r\n <mt-button\r\n [label]=\"t('test.retrieve')\"\r\n icon=\"arrow.refresh-cw-01\"\r\n [disabled]=\"polling()\"\r\n [loading]=\"polling() || facade.mutationLoading() === 'retrieve-test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"retrieve()\"\r\n />\r\n } @else if (!result()) {\r\n <mt-button\r\n [label]=\"t('test.send')\"\r\n icon=\"communication.send-01\"\r\n [disabled]=\"!canSubmit()\"\r\n [loading]=\"facade.mutationLoading() === 'test'\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"submit()\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n" }]
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
- key: 'basics',
1426
- label: this.t('editor.basics'),
1427
- type: 'header',
1428
- columns: 12,
1429
- fields: this.buildBasicFields(),
1430
- },
1431
- {
1432
- key: 'configuration',
1433
- label: this.t('editor.configuration'),
1434
- type: 'header',
1435
- columns: 12,
1436
- fields: this.buildConfigurationFields(),
1437
- },
1438
- {
1439
- key: 'credentials',
1440
- label: this.t('editor.credentials'),
1441
- type: 'header',
1442
- columns: 12,
1443
- fields: this.buildCredentialFields(),
1444
- },
1445
- ],
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 <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"editor.description\") }}\r\n </p>\r\n\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-3\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul class=\"mb-0 mt-2 list-disc space-y-1 ps-5 text-sm text-amber-900\">\r\n @for (warning of provider.warnings; track warning) {\r\n <li>{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @if (editorLoading()) {\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n } @else {\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n }\r\n\r\n @if (applicableFields().length === 0 && !editorLoading()) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "forcedDisabledFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys", "externalValues", "serverFieldErrors"], outputs: ["runtimeMessagesChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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: [ReactiveFormsModule, TranslocoDirective, DynamicForm, Button], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <div\r\n [class]=\"\r\n 'flex min-h-0 flex-col gap-5 overflow-y-auto p-4 sm:p-5 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <p class=\"m-0 text-sm text-muted-color\">\r\n {{ t(\"editor.description\") }}\r\n </p>\r\n\r\n @if (selectedProvider(); as provider) {\r\n @if (provider.warnings.length > 0) {\r\n <section\r\n class=\"rounded-lg border border-amber-200 bg-amber-50 px-4 py-3\"\r\n role=\"note\"\r\n aria-labelledby=\"provider-warnings-heading\"\r\n >\r\n <h2\r\n id=\"provider-warnings-heading\"\r\n class=\"m-0 text-sm font-semibold text-amber-950\"\r\n >\r\n {{ t(\"provider-warnings\") }}\r\n </h2>\r\n <ul class=\"mb-0 mt-2 list-disc space-y-1 ps-5 text-sm text-amber-900\">\r\n @for (warning of provider.warnings; track warning) {\r\n <li>{{ warning }}</li>\r\n }\r\n </ul>\r\n </section>\r\n }\r\n }\r\n\r\n @if (isEdit()) {\r\n <div\r\n class=\"rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-950\"\r\n role=\"note\"\r\n >\r\n {{ t(\"editor.update-invalidates-proof\") }}\r\n </div>\r\n }\r\n\r\n @if (editorLoading()) {\r\n <div class=\"py-12 text-center text-sm text-muted-color\" role=\"status\">\r\n {{ t(\"loading\") }}\r\n </div>\r\n } @else {\r\n @for (generation of [formGeneration()]; track generation) {\r\n <mt-dynamic-form [formConfig]=\"formConfig()\" [formControl]=\"form\" />\r\n }\r\n }\r\n\r\n @if (applicableFields().length === 0 && !editorLoading()) {\r\n <div\r\n class=\"rounded-lg border border-surface-200 bg-surface-50 px-4 py-3 text-sm text-muted-color\"\r\n >\r\n {{ t(\"editor.select-provider-mode\") }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n [disabled]=\"!!facade.mutationLoading()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"cancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n icon=\"general.save-01\"\r\n [disabled]=\"!canSave()\"\r\n [loading]=\"facade.mutationLoading() === (isEdit() ? 'update' : 'create')\"\r\n styleClass=\"w-full sm:w-auto\"\r\n (onClick)=\"save()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
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: '!absolute !shadow-none !w-full !max-w-full',
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('used-for-notifications'),
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: '!absolute !shadow-none !w-full !max-w-full',
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: '!absolute !shadow-none !w-full !max-w-full',
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-labelledby=\"connections-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h2 id=\"connections-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"connections-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"connections-description\") }}\r\n </p>\r\n </div>\r\n @if (\r\n canConfigure &&\r\n facade.managementAvailable() &&\r\n facade.providers().length > 0\r\n ) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else if (\r\n !facade.statusLoading() &&\r\n !facade.connectionsLoading() &&\r\n facade.connections().length === 0\r\n ) {\r\n <mt-empty-state\r\n [title]=\"t('no-connections-title')\"\r\n [description]=\"t('no-connections-description')\"\r\n [actionLabel]=\"\r\n canConfigure && facade.managementAvailable()\r\n ? t('create-connection')\r\n : undefined\r\n \"\r\n actionIcon=\"general.plus\"\r\n (actionClick)=\"createConnection()\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n </section>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs", "ariaLabel"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "clickableRows", "generalSearch", "lazyLocalSearch", "showFilters", "filterMode", "loading", "updating", "lazy", "lazyLocalSort", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "storageKey", "storageMode", "persistStateExclude", "exportable", "printable", "groupable", "groupCountMap", "cellClickFilter", "freezeActions", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "captionStartTemplate", "captionEndTemplate", "emptyTitle", "emptyDescription", "emptyActionLabel", "emptyActionIcon", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy", "sortField", "sortDirection"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "emptyAction", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange", "sortFieldChange", "sortDirectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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-labelledby=\"connections-heading\">\r\n <div\r\n class=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\"\r\n >\r\n <div>\r\n <h2 id=\"connections-heading\" class=\"text-lg font-semibold text-color\">\r\n {{ t(\"connections-title\") }}\r\n </h2>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ t(\"connections-description\") }}\r\n </p>\r\n </div>\r\n @if (\r\n canConfigure &&\r\n facade.managementAvailable() &&\r\n facade.providers().length > 0\r\n ) {\r\n <mt-button\r\n [label]=\"t('create-connection')\"\r\n icon=\"general.plus\"\r\n (onClick)=\"createConnection()\"\r\n />\r\n }\r\n </div>\r\n\r\n @if (\r\n !facade.statusLoading() &&\r\n !facade.catalogLoading() &&\r\n facade.providers().length === 0\r\n ) {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('no-providers-title')\"\r\n [description]=\"t('no-providers-description')\"\r\n />\r\n } @else if (\r\n !facade.statusLoading() &&\r\n !facade.connectionsLoading() &&\r\n facade.connections().length === 0\r\n ) {\r\n <mt-empty-state\r\n [title]=\"t('no-connections-title')\"\r\n [description]=\"t('no-connections-description')\"\r\n [actionLabel]=\"\r\n canConfigure && facade.managementAvailable()\r\n ? t('create-connection')\r\n : undefined\r\n \"\r\n actionIcon=\"general.plus\"\r\n (actionClick)=\"createConnection()\"\r\n />\r\n } @else {\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"columns()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"\r\n facade.statusLoading() ||\r\n facade.connectionsLoading() ||\r\n facade.catalogLoading()\r\n \"\r\n [generalSearch]=\"true\"\r\n [showFilters]=\"true\"\r\n filterMode=\"column\"\r\n actionShape=\"popover\"\r\n [clickableRows]=\"true\"\r\n dataKey=\"id\"\r\n storageKey=\"email-delivery-connections\"\r\n (rowClick)=\"openDetails($event)\"\r\n />\r\n }\r\n </section>\r\n</ng-container>\r\n" }]
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(['/control-panel/product-settings']);
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 @if (canView) {\r\n <p class=\"mb-4 text-sm text-muted-color\">\r\n {{ t(\"description\") }}\r\n </p>\r\n @if (facade.status(); as status) {\r\n <div\r\n class=\"mb-4 rounded-lg border px-4 py-3\"\r\n [class.border-emerald-200]=\"status.isReady\"\r\n [class.bg-emerald-50]=\"status.isReady\"\r\n [class.border-amber-200]=\"!status.isReady && status.managementEnabled\"\r\n [class.bg-amber-50]=\"!status.isReady && status.managementEnabled\"\r\n [class.border-surface-300]=\"!status.managementEnabled\"\r\n [class.bg-surface-100]=\"!status.managementEnabled\"\r\n role=\"status\"\r\n >\r\n <p class=\"font-semibold text-color\">\r\n {{\r\n status.isReady\r\n ? t(\"ready\")\r\n : status.managementEnabled\r\n ? t(\"needs-setup\")\r\n : t(\"unavailable\")\r\n }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(status.code) }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n", dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "component", type: EmailDeliveryConnections, selector: "mt-email-delivery-connections" }, { kind: "component", type: EmailDeliveryActivity, selector: "mt-email-delivery-activity" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 @if (canView) {\r\n <p class=\"mb-4 text-sm text-muted-color\">\r\n {{ t(\"description\") }}\r\n </p>\r\n @if (facade.status(); as status) {\r\n <div\r\n class=\"mb-4 rounded-lg border px-4 py-3\"\r\n [class.border-emerald-200]=\"status.isReady\"\r\n [class.bg-emerald-50]=\"status.isReady\"\r\n [class.border-amber-200]=\"!status.isReady && status.managementEnabled\"\r\n [class.bg-amber-50]=\"!status.isReady && status.managementEnabled\"\r\n [class.border-surface-300]=\"!status.managementEnabled\"\r\n [class.bg-surface-100]=\"!status.managementEnabled\"\r\n role=\"status\"\r\n >\r\n <p class=\"font-semibold text-color\">\r\n {{\r\n status.isReady\r\n ? t(\"ready\")\r\n : status.managementEnabled\r\n ? t(\"needs-setup\")\r\n : t(\"unavailable\")\r\n }}\r\n </p>\r\n <p class=\"mt-1 text-sm text-muted-color\">\r\n {{ facade.describeCode(status.code) }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n" }]
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-DowMjl4L.mjs.map
2936
+ //# sourceMappingURL=masterteam-system-settings-email-delivery-shell-C_AhRF1f.mjs.map