@masterteam/system-settings 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2213 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, ChangeDetectionStrategy, Component, signal, computed, DestroyRef } from '@angular/core';
3
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { ActivatedRoute, Router } from '@angular/router';
5
+ import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
6
+ import { Breadcrumb } from '@masterteam/components/breadcrumb';
7
+ import { EmptyState } from '@masterteam/components/empty-state';
8
+ import { Page } from '@masterteam/components/page';
9
+ import { ToastService } from '@masterteam/components/toast';
10
+ import { Tooltip } from '@masterteam/components/tooltip';
11
+ import { E as EmailDeliveryFacade, i as isConnectionTestAttempt, a as EmailDeliveryStatus, g as getEmailDeliveryStatusMap, b as EMAIL_DELIVERY_ATTEMPT_STATUSES, c as createEmailTestRequestId, d as areEmailTestRecipientsValid, p as parseEmailTestRecipients, e as isEmailTestRequestPending, f as EMAIL_DELIVERY_PERMISSIONS } from './masterteam-system-settings-masterteam-system-settings-CO-XTJOi.mjs';
12
+ import { Icon } from '@masterteam/icons';
13
+ import { Button } from '@masterteam/components/button';
14
+ import { ModalService } from '@masterteam/components/modal';
15
+ import { Table } from '@masterteam/components/table';
16
+ import { take, timer, of, map, switchMap, finalize } from 'rxjs';
17
+ import { DatePipe, DOCUMENT } from '@angular/common';
18
+ import { ModalRef } from '@masterteam/components/dialog';
19
+ import * as i1 from '@angular/forms';
20
+ import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
21
+ import { TextareaField } from '@masterteam/components/textarea-field';
22
+ import { ToggleField } from '@masterteam/components/toggle-field';
23
+ import { ConfirmationService } from '@masterteam/components/confirmation';
24
+ import { HttpErrorResponse } from '@angular/common/http';
25
+ import { TextareaFieldConfig, TextFieldConfig, ToggleFieldConfig, ValidatorConfig, SelectFieldConfig, RadioButtonFieldConfig, NumberFieldConfig } from '@masterteam/components';
26
+ import { DynamicForm } from '@masterteam/forms/dynamic-form';
27
+ import { Menu } from '@masterteam/components/menu';
28
+ import * as i1$1 from 'primeng/skeleton';
29
+ import { SkeletonModule } from 'primeng/skeleton';
30
+
31
+ class EmailOperatorReasonDialog {
32
+ itemName = input.required(...(ngDevMode ? [{ debugName: "itemName" }] : /* istanbul ignore next */ []));
33
+ consequence = input.required(...(ngDevMode ? [{ debugName: "consequence" }] : /* istanbul ignore next */ []));
34
+ showExceptionalOverride = input(false, ...(ngDevMode ? [{ debugName: "showExceptionalOverride" }] : /* istanbul ignore next */ []));
35
+ confirmLabel = input(null, ...(ngDevMode ? [{ debugName: "confirmLabel" }] : /* istanbul ignore next */ []));
36
+ ref = inject(ModalRef);
37
+ modal = inject(ModalService);
38
+ reason = new FormControl('', {
39
+ nonNullable: true,
40
+ validators: [Validators.required, Validators.maxLength(500)],
41
+ });
42
+ exceptionalOverride = new FormControl(false, {
43
+ nonNullable: true,
44
+ });
45
+ cancel() {
46
+ this.ref.close();
47
+ }
48
+ confirm() {
49
+ this.reason.markAsTouched();
50
+ this.exceptionalOverride.markAsTouched();
51
+ if (!this.canConfirm())
52
+ return;
53
+ this.ref.close({
54
+ reason: this.reason.getRawValue().trim(),
55
+ exceptionalOverride: this.exceptionalOverride.getRawValue(),
56
+ });
57
+ }
58
+ canConfirm() {
59
+ return (this.reason.valid &&
60
+ (!this.showExceptionalOverride() ||
61
+ this.exceptionalOverride.getRawValue()));
62
+ }
63
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
64
+ 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 });
65
+ }
66
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailOperatorReasonDialog, decorators: [{
67
+ type: Component,
68
+ args: [{ selector: 'mt-email-operator-reason-dialog', imports: [
69
+ ReactiveFormsModule,
70
+ TranslocoDirective,
71
+ Button,
72
+ TextareaField,
73
+ ToggleField,
74
+ ], 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" }]
75
+ }], 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 }] }] } });
76
+
77
+ class EmailAttemptDetails {
78
+ attempt = input.required(...(ngDevMode ? [{ debugName: "attempt" }] : /* istanbul ignore next */ []));
79
+ ref = inject(ModalRef);
80
+ transloco = inject(TranslocoService);
81
+ modal = inject(ModalService);
82
+ facade = inject(EmailDeliveryFacade);
83
+ isConnectionTest() {
84
+ return isConnectionTestAttempt(this.attempt());
85
+ }
86
+ /**
87
+ * A controlled test is an operator action against one connection, not product
88
+ * work the platform owes anyone — replaying it would send a second real email
89
+ * nobody asked for, so the backend's test rows are never replayable.
90
+ */
91
+ canReplay() {
92
+ if (this.isConnectionTest())
93
+ return false;
94
+ return [
95
+ 'Blocked',
96
+ 'RetryScheduled',
97
+ 'Accepted',
98
+ 'FailedPermanent',
99
+ 'DeadLettered',
100
+ 'OutcomeUnknown',
101
+ ].includes(this.attempt().status);
102
+ }
103
+ providerLabel(providerKey) {
104
+ return (this.facade.providerByKey(providerKey)?.displayName ?? providerKey ?? '—');
105
+ }
106
+ replay() {
107
+ const attempt = this.attempt();
108
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
109
+ header: this.t('replay-attempt'),
110
+ styleClass: '!w-[36rem] max-w-[96vw]',
111
+ dismissible: true,
112
+ inputValues: {
113
+ itemName: attempt.id,
114
+ consequence: isExceptionalReplay(attempt.status)
115
+ ? this.t('replay-exceptional-warning')
116
+ : this.t('replay-warning'),
117
+ showExceptionalOverride: isExceptionalReplay(attempt.status),
118
+ confirmLabel: this.t('replay-attempt'),
119
+ },
120
+ });
121
+ dialog.onClose
122
+ .pipe(take(1))
123
+ .subscribe((decision) => {
124
+ if (!decision)
125
+ return;
126
+ this.facade
127
+ .replayAttempt(attempt, {
128
+ concurrencyToken: attempt.concurrencyToken,
129
+ exceptionalOverride: decision.exceptionalOverride,
130
+ reason: decision.reason,
131
+ })
132
+ .subscribe({
133
+ next: (result) => this.ref.close(result.attempt),
134
+ error: (error) => this.facade.showError(error),
135
+ });
136
+ });
137
+ }
138
+ close() {
139
+ this.ref.close();
140
+ }
141
+ t(key) {
142
+ return this.transloco.translate(`email-delivery.${key}`);
143
+ }
144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, deps: [], target: i0.ɵɵFactoryTarget.Component });
145
+ 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 });
146
+ }
147
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailAttemptDetails, decorators: [{
148
+ type: Component,
149
+ 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" }]
150
+ }], propDecorators: { attempt: [{ type: i0.Input, args: [{ isSignal: true, alias: "attempt", required: true }] }] } });
151
+ function isExceptionalReplay(status) {
152
+ return status === 'Accepted' || status === 'OutcomeUnknown';
153
+ }
154
+
155
+ /**
156
+ * Shared chrome for the Email Delivery overlays.
157
+ *
158
+ * Every drawer on this page opens over the page content (`appendTo:
159
+ * 'page-content'`), so it is positioned absolutely and drops its own shadow.
160
+ * It takes the full width only while there is no room to spare — from `lg` up
161
+ * it settles at half the page so the table stays readable behind it, which is
162
+ * also what keeps the forms and callouts inside from stretching into
163
+ * unreadably long lines.
164
+ */
165
+ const EMAIL_DELIVERY_DRAWER_STYLE_CLASS = '!absolute !shadow-none !w-full !max-w-full md:!w-[70%] lg:!w-[50%]';
166
+
167
+ class EmailDeliveryAttempts {
168
+ modal = inject(ModalService);
169
+ transloco = inject(TranslocoService);
170
+ facade = inject(EmailDeliveryFacade);
171
+ activeLanguage = toSignal(this.transloco.langChanges$, {
172
+ initialValue: this.transloco.getActiveLang(),
173
+ });
174
+ /**
175
+ * Status is the one filter the backend understands (`?status=`), so it is
176
+ * held here and round-tripped into the query; every other column filters the
177
+ * rows already loaded.
178
+ */
179
+ tableFilters = signal({}, ...(ngDevMode ? [{ debugName: "tableFilters" }] : /* istanbul ignore next */ []));
180
+ /**
181
+ * Activity mixes product sends with the operator's own controlled tests. The
182
+ * table renders plain fields, so the labels are projected onto the row here;
183
+ * the extra properties are inert everywhere the raw attempt is used.
184
+ */
185
+ rows = computed(() => {
186
+ this.activeLanguage();
187
+ return this.facade.attempts().map((attempt) => ({
188
+ ...attempt,
189
+ operationLabel: this.operationLabel(attempt),
190
+ providerLabel: this.facade.providerByKey(attempt.providerKey)?.displayName ??
191
+ attempt.providerKey ??
192
+ '—',
193
+ resultLabel: attempt.resultCode
194
+ ? this.facade.describeCode(attempt.resultCode)
195
+ : '—',
196
+ recipientFingerprintShort: shortenFingerprint(attempt.recipientFingerprint),
197
+ }));
198
+ }, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
199
+ columns = computed(() => {
200
+ this.activeLanguage();
201
+ const statusMap = getEmailDeliveryStatusMap('attempt', this.transloco.translate.bind(this.transloco));
202
+ return [
203
+ {
204
+ key: 'status',
205
+ label: this.t('attempt.status'),
206
+ type: 'status',
207
+ width: '15rem',
208
+ statusMap,
209
+ filterConfig: {
210
+ type: 'select',
211
+ multiple: false,
212
+ options: EMAIL_DELIVERY_ATTEMPT_STATUSES.map((value) => ({
213
+ label: statusMap[value]?.display ?? value,
214
+ value,
215
+ })),
216
+ },
217
+ },
218
+ {
219
+ key: 'operationLabel',
220
+ label: this.t('attempt.operation'),
221
+ width: '14rem',
222
+ filterConfig: {
223
+ type: 'select',
224
+ options: this.distinctOptions((row) => row.operationLabel),
225
+ },
226
+ },
227
+ {
228
+ key: 'providerLabel',
229
+ label: this.t('attempt.provider'),
230
+ width: '15rem',
231
+ filterConfig: {
232
+ type: 'select',
233
+ options: this.distinctOptions((row) => row.providerLabel),
234
+ },
235
+ },
236
+ {
237
+ key: 'resultLabel',
238
+ label: this.t('attempt.result'),
239
+ filterConfig: { type: 'text' },
240
+ },
241
+ {
242
+ key: 'recipientFingerprintShort',
243
+ label: this.t('recipient-fingerprint'),
244
+ width: '15rem',
245
+ filterable: false,
246
+ },
247
+ {
248
+ key: 'createdAtUtc',
249
+ label: this.t('attempt.created'),
250
+ type: 'dateTime',
251
+ width: '10rem',
252
+ filterConfig: { type: 'date' },
253
+ },
254
+ {
255
+ key: 'attemptCount',
256
+ label: this.t('attempt.count'),
257
+ type: 'number',
258
+ width: '7rem',
259
+ },
260
+ ];
261
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
262
+ actions = computed(() => {
263
+ this.activeLanguage();
264
+ return [
265
+ {
266
+ icon: 'general.eye',
267
+ label: this.t('view-details'),
268
+ action: (row) => this.openDetails(row),
269
+ },
270
+ ];
271
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
272
+ ngOnInit() {
273
+ this.load(true);
274
+ }
275
+ /**
276
+ * Only the status filter reaches the backend; the rest narrow the rows the
277
+ * table already holds, so nothing else needs a round trip.
278
+ */
279
+ onFiltersChange(filters) {
280
+ this.tableFilters.set(filters);
281
+ const status = readStatusFilter(filters['status']);
282
+ if (status === this.facade.attemptStatusFilter())
283
+ return;
284
+ this.load(true, status);
285
+ }
286
+ refresh() {
287
+ this.load(true);
288
+ }
289
+ loadMore() {
290
+ this.load(false);
291
+ }
292
+ openDetails(attempt) {
293
+ const ref = this.modal.openModal(EmailAttemptDetails, 'drawer', {
294
+ header: this.t('attempt-details'),
295
+ styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
296
+ position: 'end',
297
+ appendTo: 'page-content',
298
+ dismissableMask: true,
299
+ dismissible: true,
300
+ inputValues: { attempt },
301
+ });
302
+ ref.onClose.pipe(take(1)).subscribe((updated) => {
303
+ if (updated)
304
+ this.load(true);
305
+ });
306
+ }
307
+ load(reset, status = this.facade.attemptStatusFilter()) {
308
+ this.facade.loadAttempts(reset, status).subscribe({
309
+ error: (error) => this.facade.showError(error),
310
+ });
311
+ }
312
+ /**
313
+ * Select filters over locally-filtered columns only offer values that are
314
+ * actually present, so the operator never picks an option that empties the
315
+ * table.
316
+ */
317
+ distinctOptions(select) {
318
+ const values = new Set(this.rows().map(select).filter(Boolean));
319
+ return Array.from(values)
320
+ .sort((left, right) => left.localeCompare(right))
321
+ .map((value) => ({ label: value, value }));
322
+ }
323
+ /**
324
+ * Controlled tests are always shown as "Connection test"; anything else falls
325
+ * back to the backend's own operation kind rather than a guessed label.
326
+ */
327
+ operationLabel(attempt) {
328
+ return isConnectionTestAttempt(attempt)
329
+ ? this.t('operation.connection-test')
330
+ : attempt.operationKind;
331
+ }
332
+ t(key) {
333
+ return this.transloco.translate(`email-delivery.${key}`);
334
+ }
335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, deps: [], target: i0.ɵɵFactoryTarget.Component });
336
+ 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 });
337
+ }
338
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryAttempts, decorators: [{
339
+ type: Component,
340
+ 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" }]
341
+ }] });
342
+ /**
343
+ * The status column is a single-select, so its stored value is the raw status.
344
+ * An empty selection clears the backend filter rather than sending a blank.
345
+ */
346
+ function readStatusFilter(value) {
347
+ const raw = Array.isArray(value) ? value[0] : value;
348
+ return typeof raw === 'string' && raw.trim() !== '' ? raw : null;
349
+ }
350
+ const FINGERPRINT_PREFIX_LENGTH = 12;
351
+ /**
352
+ * A 64-character hash has nowhere to wrap, so rendering it whole pushed its
353
+ * column far past the width the table asked for. The list only needs enough of
354
+ * it to tell two recipients apart; the drawer still shows the value in full.
355
+ */
356
+ function shortenFingerprint(value) {
357
+ if (!value)
358
+ return '—';
359
+ return value.length > FINGERPRINT_PREFIX_LENGTH
360
+ ? value.slice(0, FINGERPRINT_PREFIX_LENGTH) + '…'
361
+ : value;
362
+ }
363
+
364
+ class EmailDeliveryHostEvidence {
365
+ document = inject(DOCUMENT);
366
+ modal = inject(ModalService);
367
+ confirmation = inject(ConfirmationService);
368
+ transloco = inject(TranslocoService);
369
+ facade = inject(EmailDeliveryFacade);
370
+ activeLanguage = toSignal(this.transloco.langChanges$, {
371
+ initialValue: this.transloco.getActiveLang(),
372
+ });
373
+ rows = computed(() => this.facade.hostEvidence().map((item) => ({
374
+ ...item,
375
+ evidenceRowId: item.evidenceOccurrenceId ?? item.itemId,
376
+ evidenceKindLabel: humanizeIdentifier$1(item.evidenceKind),
377
+ evidenceOccurrenceIdentity: identifierEntity$1(item.evidenceOccurrenceId),
378
+ errorLabel: this.facade.describeCode(item.errorCode),
379
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
380
+ columns = computed(() => {
381
+ this.activeLanguage();
382
+ const statusMap = getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco));
383
+ return [
384
+ {
385
+ key: 'evidenceKindLabel',
386
+ label: this.t('evidence-kind'),
387
+ width: '11rem',
388
+ },
389
+ {
390
+ key: 'resolutionState',
391
+ label: this.t('resolution-state'),
392
+ type: 'status',
393
+ width: '12rem',
394
+ statusMap,
395
+ },
396
+ {
397
+ key: 'ownershipState',
398
+ label: this.t('ownership-state'),
399
+ type: 'status',
400
+ width: '11rem',
401
+ statusMap,
402
+ },
403
+ {
404
+ key: 'detectedAtUtc',
405
+ label: this.t('detected'),
406
+ type: 'dateTime',
407
+ width: '10rem',
408
+ },
409
+ { key: 'errorLabel', label: this.t('issue') },
410
+ {
411
+ key: 'evidenceOccurrenceIdentity',
412
+ label: this.t('evidence-occurrence-id'),
413
+ type: 'entity',
414
+ width: '15rem',
415
+ shrink: true,
416
+ },
417
+ ];
418
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
419
+ actions = computed(() => {
420
+ this.activeLanguage();
421
+ return [
422
+ {
423
+ icon: 'general.download-01',
424
+ label: this.t('export'),
425
+ hidden: (row) => {
426
+ const item = row;
427
+ return !item.canExport || !item.evidenceOccurrenceId;
428
+ },
429
+ action: (row) => this.export(row),
430
+ },
431
+ {
432
+ icon: 'general.tool-02',
433
+ label: this.t('repair'),
434
+ hidden: (row) => !this.facade.managementAvailable() ||
435
+ !row.canRepair ||
436
+ !row.evidenceOccurrenceId ||
437
+ !row.decisionToken,
438
+ action: (row) => this.decide(row, 'repair'),
439
+ },
440
+ {
441
+ icon: 'general.check-circle',
442
+ label: this.t('acknowledge'),
443
+ color: 'danger',
444
+ hidden: (row) => !this.facade.managementAvailable() ||
445
+ !row.evidenceOccurrenceId ||
446
+ !row.decisionToken,
447
+ action: (row) => this.decide(row, 'acknowledge'),
448
+ },
449
+ ];
450
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
451
+ ngOnInit() {
452
+ if (this.facade.hostEvidence().length === 0)
453
+ this.load(true);
454
+ }
455
+ export(item) {
456
+ if (!item.evidenceOccurrenceId)
457
+ return;
458
+ this.facade.exportHostEvidence(item.evidenceOccurrenceId).subscribe({
459
+ next: (download) => {
460
+ const url = URL.createObjectURL(download.blob);
461
+ const anchor = this.document.createElement('a');
462
+ anchor.href = url;
463
+ anchor.download = download.fileName;
464
+ anchor.rel = 'noopener';
465
+ anchor.click();
466
+ URL.revokeObjectURL(url);
467
+ },
468
+ error: (error) => this.facade.showError(error),
469
+ });
470
+ }
471
+ decide(item, action) {
472
+ if (!item.evidenceOccurrenceId || !item.decisionToken)
473
+ return;
474
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
475
+ header: this.t(action === 'repair' ? 'repair-evidence' : 'acknowledge-evidence'),
476
+ styleClass: '!w-[36rem] max-w-[96vw]',
477
+ dismissible: true,
478
+ inputValues: {
479
+ itemName: item.evidenceOccurrenceId,
480
+ consequence: this.t(action === 'repair'
481
+ ? 'evidence-repair-warning'
482
+ : 'evidence-acknowledge-warning'),
483
+ confirmLabel: this.t(action),
484
+ },
485
+ });
486
+ dialog.onClose
487
+ .pipe(take(1))
488
+ .subscribe((decision) => {
489
+ if (!decision)
490
+ return;
491
+ this.facade
492
+ .decideHostEvidence(item, action, decision.reason)
493
+ .subscribe({
494
+ error: (error) => {
495
+ this.facade.showError(error);
496
+ this.load(true);
497
+ },
498
+ });
499
+ });
500
+ }
501
+ confirmRebuild() {
502
+ this.confirmation.confirm({
503
+ type: 'dialog',
504
+ header: this.t('rebuild-evidence-index'),
505
+ message: this.t('rebuild-evidence-warning'),
506
+ accept: () => this.facade.rebuildEvidenceIndex(200).subscribe({
507
+ error: (error) => this.facade.showError(error),
508
+ }),
509
+ });
510
+ }
511
+ loadMore() {
512
+ this.load(false);
513
+ }
514
+ load(reset) {
515
+ this.facade.loadHostEvidence(reset).subscribe({
516
+ error: (error) => this.facade.showError(error),
517
+ });
518
+ }
519
+ t(key) {
520
+ return this.transloco.translate(`email-delivery.${key}`);
521
+ }
522
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, deps: [], target: i0.ɵɵFactoryTarget.Component });
523
+ 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 });
524
+ }
525
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHostEvidence, decorators: [{
526
+ type: Component,
527
+ 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" }]
528
+ }] });
529
+ function identifierEntity$1(value) {
530
+ return {
531
+ name: '',
532
+ viewType: 'Text',
533
+ value: value ?? '—',
534
+ configuration: { hideName: true },
535
+ };
536
+ }
537
+ function humanizeIdentifier$1(value) {
538
+ return value
539
+ .replace(/[._-]+/g, ' ')
540
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
541
+ .replace(/\s+/g, ' ')
542
+ .trim()
543
+ .replace(/^./, (character) => character.toUpperCase())
544
+ .replace(/\bSmtp\b/g, 'SMTP')
545
+ .replace(/\bTls\b/g, 'TLS')
546
+ .replace(/\bOauth2?\b/gi, 'OAuth2')
547
+ .replace(/\bHttp\b/g, 'HTTP')
548
+ .replace(/\bApi\b/g, 'API');
549
+ }
550
+
551
+ class EmailDeliveryQuarantine {
552
+ modal = inject(ModalService);
553
+ transloco = inject(TranslocoService);
554
+ facade = inject(EmailDeliveryFacade);
555
+ activeLanguage = toSignal(this.transloco.langChanges$, {
556
+ initialValue: this.transloco.getActiveLang(),
557
+ });
558
+ rows = computed(() => this.facade.quarantine().map((item) => ({
559
+ ...item,
560
+ attemptIdentity: identifierEntity(item.attemptId),
561
+ errorLabel: this.facade.describeCode(item.errorCode),
562
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
563
+ columns = computed(() => {
564
+ this.activeLanguage();
565
+ return [
566
+ {
567
+ key: 'attemptIdentity',
568
+ label: this.t('attempt.id'),
569
+ type: 'entity',
570
+ width: '15rem',
571
+ shrink: true,
572
+ },
573
+ {
574
+ key: 'recoveryClassification',
575
+ label: this.t('recovery-classification'),
576
+ type: 'status',
577
+ width: '12rem',
578
+ statusMap: getEmailDeliveryStatusMap('support', this.transloco.translate.bind(this.transloco)),
579
+ },
580
+ { key: 'errorLabel', label: this.t('issue') },
581
+ {
582
+ key: 'quarantinedAtUtc',
583
+ label: this.t('detected'),
584
+ type: 'dateTime',
585
+ width: '10rem',
586
+ },
587
+ ];
588
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
589
+ actions = computed(() => {
590
+ this.activeLanguage();
591
+ return [
592
+ {
593
+ icon: 'arrow.refresh-cw-01',
594
+ label: this.t('retry'),
595
+ hidden: (row) => !this.facade.managementAvailable() ||
596
+ row.recoveryClassification !==
597
+ 'retryable-after-upgrade',
598
+ action: (row) => this.decide(row, 'retry'),
599
+ },
600
+ {
601
+ icon: 'general.check-circle',
602
+ label: this.t('acknowledge'),
603
+ color: 'danger',
604
+ hidden: () => !this.facade.managementAvailable(),
605
+ action: (row) => this.decide(row, 'acknowledge'),
606
+ },
607
+ ];
608
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
609
+ ngOnInit() {
610
+ if (this.facade.quarantine().length === 0)
611
+ this.load(true);
612
+ }
613
+ decide(item, action) {
614
+ const dialog = this.modal.openModal(EmailOperatorReasonDialog, 'dialog', {
615
+ header: this.t(action === 'retry' ? 'retry-quarantine' : 'acknowledge-quarantine'),
616
+ styleClass: '!w-[36rem] max-w-[96vw]',
617
+ dismissible: true,
618
+ inputValues: {
619
+ itemName: item.itemId,
620
+ consequence: this.t(action === 'retry'
621
+ ? 'quarantine-retry-warning'
622
+ : 'quarantine-acknowledge-warning'),
623
+ confirmLabel: this.t(action),
624
+ },
625
+ });
626
+ dialog.onClose
627
+ .pipe(take(1))
628
+ .subscribe((decision) => {
629
+ if (!decision)
630
+ return;
631
+ this.facade.decideQuarantine(item, action, decision.reason).subscribe({
632
+ error: (error) => this.facade.showError(error),
633
+ });
634
+ });
635
+ }
636
+ loadMore() {
637
+ this.load(false);
638
+ }
639
+ load(reset) {
640
+ this.facade.loadQuarantine(reset).subscribe({
641
+ error: (error) => this.facade.showError(error),
642
+ });
643
+ }
644
+ t(key) {
645
+ return this.transloco.translate(`email-delivery.${key}`);
646
+ }
647
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, deps: [], target: i0.ɵɵFactoryTarget.Component });
648
+ 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 });
649
+ }
650
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryQuarantine, decorators: [{
651
+ type: Component,
652
+ 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" }]
653
+ }] });
654
+ function identifierEntity(value) {
655
+ return {
656
+ name: '',
657
+ viewType: 'Text',
658
+ value: value ?? '—',
659
+ configuration: { hideName: true },
660
+ };
661
+ }
662
+
663
+ class EmailDeliveryActivity {
664
+ advancedExpanded = signal(false, ...(ngDevMode ? [{ debugName: "advancedExpanded" }] : /* istanbul ignore next */ []));
665
+ onAdvancedToggle(event) {
666
+ this.advancedExpanded.set(event.currentTarget.open);
667
+ }
668
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, deps: [], target: i0.ɵɵFactoryTarget.Component });
669
+ 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 });
670
+ }
671
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryActivity, decorators: [{
672
+ type: Component,
673
+ args: [{ selector: 'mt-email-delivery-activity', imports: [
674
+ TranslocoDirective,
675
+ Icon,
676
+ EmailDeliveryAttempts,
677
+ EmailDeliveryQuarantine,
678
+ EmailDeliveryHostEvidence,
679
+ ], 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" }]
680
+ }] });
681
+
682
+ class EmailControlledTestDialog {
683
+ connectionId = input.required(...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
684
+ connectionName = input.required(...(ngDevMode ? [{ debugName: "connectionName" }] : /* istanbul ignore next */ []));
685
+ modal = inject(ModalService);
686
+ ref = inject(ModalRef);
687
+ destroyRef = inject(DestroyRef);
688
+ transloco = inject(TranslocoService);
689
+ facade = inject(EmailDeliveryFacade);
690
+ testRequestId = signal(createEmailTestRequestId(), ...(ngDevMode ? [{ debugName: "testRequestId" }] : /* istanbul ignore next */ []));
691
+ timedOut = signal(false, ...(ngDevMode ? [{ debugName: "timedOut" }] : /* istanbul ignore next */ []));
692
+ polling = signal(false, ...(ngDevMode ? [{ debugName: "polling" }] : /* istanbul ignore next */ []));
693
+ result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
694
+ pollIndex = 0;
695
+ form = new FormControl({
696
+ recipients: null,
697
+ subject: 'Pplus email delivery test',
698
+ body: 'This is a controlled test.',
699
+ isHtml: false,
700
+ }, { nonNullable: true });
701
+ formStatus = toSignal(this.form.statusChanges, {
702
+ initialValue: this.form.status,
703
+ });
704
+ canSubmit = computed(() => this.formStatus() === 'VALID' &&
705
+ !this.facade.mutationLoading() &&
706
+ !this.result(), ...(ngDevMode ? [{ debugName: "canSubmit" }] : /* istanbul ignore next */ []));
707
+ formConfig = computed(() => ({
708
+ sections: [
709
+ {
710
+ key: 'controlled-test',
711
+ type: 'none',
712
+ columns: 12,
713
+ fields: [
714
+ new TextareaFieldConfig({
715
+ key: 'recipients',
716
+ label: this.t('test.recipients'),
717
+ placeholder: this.t('test.recipients-placeholder'),
718
+ hint: this.t('test.recipients-hint'),
719
+ colSpan: 12,
720
+ rows: 3,
721
+ validators: [
722
+ ValidatorConfig.required(),
723
+ ValidatorConfig.custom((value) => areEmailTestRecipientsValid(value, 100, 320), this.t('test.recipients-invalid')),
724
+ ],
725
+ }),
726
+ new TextFieldConfig({
727
+ key: 'subject',
728
+ label: this.t('test.subject'),
729
+ colSpan: 12,
730
+ validators: [
731
+ ValidatorConfig.required(),
732
+ ValidatorConfig.maxLength(998),
733
+ ValidatorConfig.pattern('^[^\\r\\n]*$'),
734
+ ],
735
+ }),
736
+ new TextareaFieldConfig({
737
+ key: 'body',
738
+ label: this.t('test.body'),
739
+ colSpan: 12,
740
+ rows: 8,
741
+ validators: [ValidatorConfig.maxLength(2_000_000)],
742
+ }),
743
+ new ToggleFieldConfig({
744
+ key: 'isHtml',
745
+ label: this.t('test.html'),
746
+ colSpan: 12,
747
+ }),
748
+ ],
749
+ },
750
+ ],
751
+ }), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
752
+ /**
753
+ * The provider-acceptance caveat is shown once. When the backend already
754
+ * answers with the accepted-delivery-unconfirmed code, the code message
755
+ * above carries that sentence and the extra line would just repeat it.
756
+ */
757
+ showsAcceptedCaveat(result) {
758
+ return (result.outcome === 'Accepted' &&
759
+ result.code !== ACCEPTED_DELIVERY_UNCONFIRMED_CODE);
760
+ }
761
+ submit() {
762
+ if (!this.canSubmit()) {
763
+ this.form.markAllAsTouched();
764
+ return;
765
+ }
766
+ const value = this.form.getRawValue();
767
+ this.facade
768
+ .sendConnectionTest(this.connectionId(), {
769
+ testRequestId: this.testRequestId(),
770
+ recipients: parseEmailTestRecipients(value.recipients),
771
+ subject: value.subject?.trim() ?? '',
772
+ body: value.body ?? '',
773
+ isHtml: value.isHtml,
774
+ })
775
+ .subscribe({
776
+ next: (result) => {
777
+ this.result.set(result);
778
+ this.timedOut.set(false);
779
+ },
780
+ error: (error) => {
781
+ if (isAmbiguousTimeout(error) ||
782
+ this.facade.errorAction(error) === 'poll_status') {
783
+ this.startPolling();
784
+ return;
785
+ }
786
+ this.facade.showError(error);
787
+ // The backend rejected this GUID because it already stands for a
788
+ // different send. Retrieving it would return someone else's test and
789
+ // reusing it can never succeed, so the dialog mints a fresh one and
790
+ // waits for the operator to press Send again — it never resends.
791
+ if (isTestRequestConflict(error)) {
792
+ this.testRequestId.set(createEmailTestRequestId());
793
+ }
794
+ },
795
+ });
796
+ }
797
+ retrieve() {
798
+ if (this.polling())
799
+ return;
800
+ this.retrieveStoredResult(false);
801
+ }
802
+ retrieveStoredResult(fromPoll) {
803
+ this.facade
804
+ .retrieveConnectionTest(this.connectionId(), this.testRequestId())
805
+ .subscribe({
806
+ next: (stored) => {
807
+ if (stored.result) {
808
+ this.result.set(stored.result);
809
+ this.timedOut.set(false);
810
+ this.polling.set(false);
811
+ return;
812
+ }
813
+ if (fromPoll && isEmailTestRequestPending(stored.requestState)) {
814
+ this.schedulePoll();
815
+ }
816
+ else {
817
+ this.polling.set(false);
818
+ this.timedOut.set(true);
819
+ }
820
+ },
821
+ error: (error) => {
822
+ if (fromPoll && this.facade.errorAction(error) === 'poll_status') {
823
+ this.schedulePoll();
824
+ return;
825
+ }
826
+ this.polling.set(false);
827
+ this.timedOut.set(true);
828
+ this.facade.showError(error);
829
+ },
830
+ });
831
+ }
832
+ startPolling() {
833
+ if (this.polling())
834
+ return;
835
+ this.timedOut.set(true);
836
+ this.polling.set(true);
837
+ this.pollIndex = 0;
838
+ this.schedulePoll();
839
+ }
840
+ schedulePoll() {
841
+ const delay = POLL_DELAYS_MS[this.pollIndex++];
842
+ if (delay === undefined) {
843
+ this.polling.set(false);
844
+ return;
845
+ }
846
+ timer(delay)
847
+ .pipe(takeUntilDestroyed(this.destroyRef))
848
+ .subscribe(() => this.retrieveStoredResult(true));
849
+ }
850
+ close() {
851
+ this.ref.close(this.result());
852
+ }
853
+ t(key) {
854
+ return this.transloco.translate(`email-delivery.${key}`);
855
+ }
856
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
857
+ 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 });
858
+ }
859
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailControlledTestDialog, decorators: [{
860
+ type: Component,
861
+ args: [{ selector: 'mt-email-controlled-test-dialog', imports: [
862
+ ReactiveFormsModule,
863
+ TranslocoDirective,
864
+ DynamicForm,
865
+ Button,
866
+ EmailDeliveryStatus,
867
+ ], 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" }]
868
+ }], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }], connectionName: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionName", required: true }] }] } });
869
+ const ACCEPTED_DELIVERY_UNCONFIRMED_CODE = 'email.accepted_delivery_unconfirmed';
870
+ const TEST_REQUEST_CONFLICT_CODE = 'email.delivery_test_request_conflict';
871
+ function isTestRequestConflict(error) {
872
+ const body = error instanceof HttpErrorResponse ? error.error : error;
873
+ return (typeof body === 'object' &&
874
+ body !== null &&
875
+ body.code === TEST_REQUEST_CONFLICT_CODE);
876
+ }
877
+ const POLL_DELAYS_MS = [1_000, 2_000, 4_000, 5_000, 5_000, 5_000];
878
+ function isAmbiguousTimeout(error) {
879
+ return (error instanceof HttpErrorResponse &&
880
+ (error.status === 0 || error.status === 408 || error.status === 504));
881
+ }
882
+
883
+ function getApplicableProviderFields(provider, authenticationMode) {
884
+ if (!provider || !authenticationMode) {
885
+ return [];
886
+ }
887
+ return provider.fields.filter((field) => field.authenticationModes.length === 0 ||
888
+ field.authenticationModes.includes(authenticationMode));
889
+ }
890
+ function getApplicableProviderActions(provider, authenticationMode, authorizationModel) {
891
+ if (!provider || !authenticationMode) {
892
+ return [];
893
+ }
894
+ return (provider.actions ?? []).filter((action) => {
895
+ if (!action.isExecutable) {
896
+ return false;
897
+ }
898
+ if (action.authenticationModes.length > 0 &&
899
+ !action.authenticationModes.includes(authenticationMode)) {
900
+ return false;
901
+ }
902
+ return (!action.authorizationModels?.length ||
903
+ (!!authorizationModel &&
904
+ action.authorizationModels.includes(authorizationModel)));
905
+ });
906
+ }
907
+ function parseProviderCapabilities(value) {
908
+ return value
909
+ .split(',')
910
+ .map((item) => item.trim())
911
+ .filter(Boolean);
912
+ }
913
+ function providerFieldControlKey(index) {
914
+ return `providerField${index}`;
915
+ }
916
+ function credentialOperationControlKey(index) {
917
+ return `credentialOperation${index}`;
918
+ }
919
+ function credentialStorageControlKey(index) {
920
+ return `credentialStorage${index}`;
921
+ }
922
+ function credentialValueControlKey(index) {
923
+ return `credentialValue${index}`;
924
+ }
925
+ function credentialReferenceControlKey(index) {
926
+ return `credentialReference${index}`;
927
+ }
928
+ function buildConnectionRequest(value, provider, existingConcurrencyToken) {
929
+ const mode = requiredTrimmed(value.authenticationMode);
930
+ const publicConfiguration = {};
931
+ const credentials = [];
932
+ provider.fields.forEach((field, index) => {
933
+ if (field.authenticationModes.length > 0 &&
934
+ !field.authenticationModes.includes(mode)) {
935
+ return;
936
+ }
937
+ if (field.isSecret) {
938
+ const operation = value[credentialOperationControlKey(index)];
939
+ if (!operation || operation === 'omitted') {
940
+ return;
941
+ }
942
+ const storageKind = (value[credentialStorageControlKey(index)] ??
943
+ 'protected-value');
944
+ credentials.push({
945
+ key: field.key,
946
+ operation,
947
+ storageKind,
948
+ value: operation === 'replace' && storageKind === 'protected-value'
949
+ ? optionalTrimmed(value[credentialValueControlKey(index)])
950
+ : null,
951
+ externalReference: operation === 'replace' && storageKind === 'external-reference'
952
+ ? optionalTrimmed(value[credentialReferenceControlKey(index)])
953
+ : null,
954
+ });
955
+ return;
956
+ }
957
+ const fieldValue = value[providerFieldControlKey(index)];
958
+ if (fieldValue === null || fieldValue === undefined || fieldValue === '') {
959
+ return;
960
+ }
961
+ publicConfiguration[field.key] =
962
+ typeof fieldValue === 'boolean' ? String(fieldValue) : String(fieldValue);
963
+ });
964
+ const request = {
965
+ name: requiredTrimmed(value.name),
966
+ providerKey: provider.key,
967
+ authenticationMode: mode,
968
+ senderAddress: requiredTrimmed(value.senderAddress),
969
+ senderDisplayName: optionalTrimmed(value.senderDisplayName),
970
+ publicConfiguration,
971
+ credentials,
972
+ };
973
+ return existingConcurrencyToken
974
+ ? { ...request, concurrencyToken: existingConcurrencyToken }
975
+ : request;
976
+ }
977
+ function requiredTrimmed(value) {
978
+ return String(value ?? '').trim();
979
+ }
980
+ function optionalTrimmed(value) {
981
+ const normalized = String(value ?? '').trim();
982
+ return normalized || null;
983
+ }
984
+
985
+ class EmailConnectionEditor {
986
+ editorConnectionId = input(null, ...(ngDevMode ? [{ debugName: "editorConnectionId" }] : /* istanbul ignore next */ []));
987
+ ref = inject(ModalRef);
988
+ transloco = inject(TranslocoService);
989
+ modal = inject(ModalService);
990
+ facade = inject(EmailDeliveryFacade);
991
+ connectionId = signal(null, ...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
992
+ existingDetails = signal(null, ...(ngDevMode ? [{ debugName: "existingDetails" }] : /* istanbul ignore next */ []));
993
+ editorLoading = signal(true, ...(ngDevMode ? [{ debugName: "editorLoading" }] : /* istanbul ignore next */ []));
994
+ formGeneration = signal(0, ...(ngDevMode ? [{ debugName: "formGeneration" }] : /* istanbul ignore next */ []));
995
+ formValue = signal(emptyEditorValue(), ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
996
+ form = new FormControl(emptyEditorValue(), { nonNullable: true });
997
+ formStatus = toSignal(this.form.statusChanges, {
998
+ initialValue: this.form.status,
999
+ });
1000
+ previousProviderKey = null;
1001
+ previousAuthenticationMode = null;
1002
+ structureSignature = '';
1003
+ isEdit = computed(() => !!this.connectionId(), ...(ngDevMode ? [{ debugName: "isEdit" }] : /* istanbul ignore next */ []));
1004
+ selectedProvider = computed(() => this.facade.providerByKey(this.formValue().providerKey), ...(ngDevMode ? [{ debugName: "selectedProvider" }] : /* istanbul ignore next */ []));
1005
+ selectedMode = computed(() => this.formValue().authenticationMode, ...(ngDevMode ? [{ debugName: "selectedMode" }] : /* istanbul ignore next */ []));
1006
+ applicableFields = computed(() => getApplicableProviderFields(this.selectedProvider(), this.selectedMode()), ...(ngDevMode ? [{ debugName: "applicableFields" }] : /* istanbul ignore next */ []));
1007
+ canSave = computed(() => !this.editorLoading() &&
1008
+ this.formStatus() === 'VALID' &&
1009
+ !this.facade.mutationLoading(), ...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
1010
+ configurationFields = computed(() => this.buildConfigurationFields(), ...(ngDevMode ? [{ debugName: "configurationFields" }] : /* istanbul ignore next */ []));
1011
+ /**
1012
+ * Empty whenever the selected mode authenticates without a stored secret —
1013
+ * an anonymous QC-test SMTP endpoint, for instance. That is a valid provider
1014
+ * descriptor, not a load failure.
1015
+ */
1016
+ credentialFields = computed(() => this.buildCredentialFields(), ...(ngDevMode ? [{ debugName: "credentialFields" }] : /* istanbul ignore next */ []));
1017
+ /**
1018
+ * A section header over an empty body reads as something that failed to
1019
+ * load, so a section with no applicable fields is not rendered at all.
1020
+ */
1021
+ formConfig = computed(() => ({
1022
+ layout: SECTION_MODE_LAYOUT,
1023
+ sections: [
1024
+ this.section('basics', this.buildBasicFields()),
1025
+ this.section('configuration', this.configurationFields()),
1026
+ this.section('credentials', this.credentialFields()),
1027
+ ].filter((section) => section.fields.length > 0),
1028
+ }), ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
1029
+ constructor() {
1030
+ this.form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
1031
+ this.handleFormValue(value);
1032
+ });
1033
+ }
1034
+ ngOnInit() {
1035
+ const id = this.editorConnectionId();
1036
+ this.connectionId.set(id);
1037
+ const catalog$ = this.facade.providers().length
1038
+ ? of(undefined)
1039
+ : this.facade.loadCatalog().pipe(map(() => undefined));
1040
+ catalog$
1041
+ .pipe(switchMap(() => (id ? this.facade.loadConnection(id) : of(null))), finalize(() => this.editorLoading.set(false)))
1042
+ .subscribe({
1043
+ next: (details) => {
1044
+ this.existingDetails.set(details);
1045
+ this.seedForm(details);
1046
+ },
1047
+ error: (error) => this.facade.showError(error),
1048
+ });
1049
+ }
1050
+ ngOnDestroy() {
1051
+ this.clearSecretControls();
1052
+ }
1053
+ save() {
1054
+ if (!this.canSave()) {
1055
+ this.form.markAllAsTouched();
1056
+ return;
1057
+ }
1058
+ const provider = this.selectedProvider();
1059
+ if (!provider)
1060
+ return;
1061
+ const id = this.connectionId();
1062
+ const request = buildConnectionRequest(this.form.getRawValue(), provider, this.existingDetails()?.connection.concurrencyToken);
1063
+ const operation = id
1064
+ ? this.facade.updateConnection(id, request)
1065
+ : this.facade.createConnection(request);
1066
+ operation.pipe(finalize(() => this.clearSecretControls())).subscribe({
1067
+ next: () => {
1068
+ this.ref.close(true);
1069
+ },
1070
+ error: (error) => this.handleSaveError(error),
1071
+ });
1072
+ }
1073
+ cancel() {
1074
+ this.clearSecretControls();
1075
+ this.ref.close(false);
1076
+ }
1077
+ seedForm(details) {
1078
+ const provider = details
1079
+ ? this.facade.providerByKey(details.connection.providerKey)
1080
+ : (this.facade.providers()[0] ?? null);
1081
+ const mode = details
1082
+ ? details.connection.authenticationMode
1083
+ : (provider?.authenticationModes[0]?.key ?? null);
1084
+ const value = {
1085
+ name: details?.connection.name ?? null,
1086
+ providerKey: provider?.key ?? null,
1087
+ authenticationMode: mode,
1088
+ senderAddress: details?.connection.senderAddress ?? null,
1089
+ senderDisplayName: details?.connection.senderDisplayName ?? null,
1090
+ };
1091
+ provider?.fields.forEach((field, index) => {
1092
+ if (!isApplicable(field, mode))
1093
+ return;
1094
+ if (field.isSecret) {
1095
+ const status = details?.credentials.find((credential) => credential.key === field.key);
1096
+ value[credentialOperationControlKey(index)] = details
1097
+ ? 'omitted'
1098
+ : 'replace';
1099
+ value[credentialStorageControlKey(index)] =
1100
+ status?.storageKind === 'ExternalReference'
1101
+ ? 'external-reference'
1102
+ : 'protected-value';
1103
+ value[credentialValueControlKey(index)] = null;
1104
+ value[credentialReferenceControlKey(index)] = null;
1105
+ return;
1106
+ }
1107
+ const rawValue = details?.connection.publicConfiguration[field.key] ??
1108
+ field.defaultValue;
1109
+ value[providerFieldControlKey(index)] = toEditorFieldValue(field, rawValue);
1110
+ });
1111
+ this.previousProviderKey = provider?.key ?? null;
1112
+ this.previousAuthenticationMode = mode;
1113
+ this.formValue.set(value);
1114
+ this.form.setValue(value, { emitEvent: false });
1115
+ this.structureSignature = this.getStructureSignature(value, provider);
1116
+ this.formGeneration.update((generation) => generation + 1);
1117
+ }
1118
+ handleFormValue(value) {
1119
+ let provider = this.facade.providerByKey(value.providerKey);
1120
+ const providerChanged = value.providerKey !== this.previousProviderKey;
1121
+ let mode = value.authenticationMode;
1122
+ if (provider &&
1123
+ !provider.authenticationModes.some((item) => item.key === mode)) {
1124
+ mode = provider.authenticationModes[0]?.key ?? null;
1125
+ }
1126
+ const modeChanged = mode !== this.previousAuthenticationMode;
1127
+ if (providerChanged || modeChanged) {
1128
+ const reset = this.resetProviderValues(value, provider, mode);
1129
+ this.previousProviderKey = provider?.key ?? null;
1130
+ this.previousAuthenticationMode = mode;
1131
+ this.formValue.set(reset);
1132
+ this.form.setValue(reset, { emitEvent: false });
1133
+ this.structureSignature = this.getStructureSignature(reset, provider);
1134
+ this.formGeneration.update((generation) => generation + 1);
1135
+ return;
1136
+ }
1137
+ provider = this.facade.providerByKey(value.providerKey);
1138
+ this.formValue.set(value);
1139
+ const signature = this.getStructureSignature(value, provider);
1140
+ if (signature !== this.structureSignature) {
1141
+ this.structureSignature = signature;
1142
+ this.formGeneration.update((generation) => generation + 1);
1143
+ }
1144
+ }
1145
+ resetProviderValues(value, provider, mode) {
1146
+ const reset = {
1147
+ name: value.name,
1148
+ providerKey: provider?.key ?? null,
1149
+ authenticationMode: mode,
1150
+ senderAddress: value.senderAddress,
1151
+ senderDisplayName: value.senderDisplayName,
1152
+ };
1153
+ provider?.fields.forEach((field, index) => {
1154
+ if (!isApplicable(field, mode))
1155
+ return;
1156
+ if (field.isSecret) {
1157
+ reset[credentialOperationControlKey(index)] = 'replace';
1158
+ reset[credentialStorageControlKey(index)] = 'protected-value';
1159
+ reset[credentialValueControlKey(index)] = null;
1160
+ reset[credentialReferenceControlKey(index)] = null;
1161
+ }
1162
+ else {
1163
+ reset[providerFieldControlKey(index)] = toEditorFieldValue(field, field.defaultValue);
1164
+ }
1165
+ });
1166
+ return reset;
1167
+ }
1168
+ /**
1169
+ * Every section wears the same shell mt-client-form gives its sections — a
1170
+ * bordered, flat, rounded-md card with a header strip — so the connection
1171
+ * form reads like the runtime forms in the rest of the product.
1172
+ */
1173
+ section(key, fields) {
1174
+ return {
1175
+ key,
1176
+ label: this.t(`editor.${key}`),
1177
+ type: 'header',
1178
+ columns: 12,
1179
+ headerClass: SECTION_MODE_HEADER_CLASS,
1180
+ bodyClass: SECTION_MODE_BODY_CLASS,
1181
+ fields,
1182
+ };
1183
+ }
1184
+ buildBasicFields() {
1185
+ const provider = this.selectedProvider();
1186
+ return [
1187
+ new TextFieldConfig({
1188
+ key: 'name',
1189
+ label: this.t('connection.name'),
1190
+ colSpan: 12,
1191
+ validators: [
1192
+ ValidatorConfig.required(),
1193
+ ValidatorConfig.maxLength(150),
1194
+ ],
1195
+ }),
1196
+ new SelectFieldConfig({
1197
+ key: 'providerKey',
1198
+ label: this.t('connection.provider'),
1199
+ options: this.facade.providers().map((item) => ({
1200
+ label: item.displayName,
1201
+ value: item.key,
1202
+ })),
1203
+ optionLabel: 'label',
1204
+ optionValue: 'value',
1205
+ colSpan: 6,
1206
+ validators: [ValidatorConfig.required()],
1207
+ }),
1208
+ new SelectFieldConfig({
1209
+ key: 'authenticationMode',
1210
+ label: this.t('connection.authentication-mode'),
1211
+ options: provider?.authenticationModes.map((item) => ({
1212
+ label: item.displayName,
1213
+ value: item.key,
1214
+ })) ?? [],
1215
+ optionLabel: 'label',
1216
+ optionValue: 'value',
1217
+ colSpan: 6,
1218
+ validators: [ValidatorConfig.required()],
1219
+ }),
1220
+ new TextFieldConfig({
1221
+ key: 'senderAddress',
1222
+ label: this.t('connection.sender-address'),
1223
+ inputType: 'email',
1224
+ colSpan: 6,
1225
+ validators: [ValidatorConfig.required(), ValidatorConfig.email()],
1226
+ }),
1227
+ new TextFieldConfig({
1228
+ key: 'senderDisplayName',
1229
+ label: this.t('connection.sender-display-name'),
1230
+ colSpan: 6,
1231
+ validators: [ValidatorConfig.maxLength(200)],
1232
+ }),
1233
+ ];
1234
+ }
1235
+ buildConfigurationFields() {
1236
+ const provider = this.selectedProvider();
1237
+ if (!provider)
1238
+ return [];
1239
+ return provider.fields.flatMap((field, index) => {
1240
+ if (field.isSecret || !isApplicable(field, this.selectedMode())) {
1241
+ return [];
1242
+ }
1243
+ return [this.toProviderFieldConfig(field, index)];
1244
+ });
1245
+ }
1246
+ buildCredentialFields() {
1247
+ const provider = this.selectedProvider();
1248
+ if (!provider)
1249
+ return [];
1250
+ return provider.fields.flatMap((field, index) => {
1251
+ if (!field.isSecret || !isApplicable(field, this.selectedMode())) {
1252
+ return [];
1253
+ }
1254
+ const operationKey = credentialOperationControlKey(index);
1255
+ const storageKey = credentialStorageControlKey(index);
1256
+ const operation = (this.formValue()[operationKey] ??
1257
+ (this.isEdit() ? 'omitted' : 'replace'));
1258
+ const storageKind = (this.formValue()[storageKey] ??
1259
+ 'protected-value');
1260
+ const operationOptions = [
1261
+ ...(this.isEdit()
1262
+ ? [{ label: this.t('credential.keep'), value: 'omitted' }]
1263
+ : []),
1264
+ { label: this.t('credential.replace'), value: 'replace' },
1265
+ ...(!field.required && this.isEdit()
1266
+ ? [{ label: this.t('credential.clear'), value: 'clear' }]
1267
+ : []),
1268
+ ];
1269
+ const fields = [
1270
+ new SelectFieldConfig({
1271
+ key: operationKey,
1272
+ label: `${field.displayName} — ${this.t('credential.operation')}`,
1273
+ options: operationOptions,
1274
+ optionLabel: 'label',
1275
+ optionValue: 'value',
1276
+ colSpan: 6,
1277
+ validators: [ValidatorConfig.required()],
1278
+ }),
1279
+ ];
1280
+ if (operation !== 'replace')
1281
+ return fields;
1282
+ fields.push(new RadioButtonFieldConfig({
1283
+ key: storageKey,
1284
+ label: this.t('credential.storage-choice'),
1285
+ options: [
1286
+ {
1287
+ label: this.t('credential.protected-value'),
1288
+ value: 'protected-value',
1289
+ },
1290
+ {
1291
+ label: this.t('credential.external-reference'),
1292
+ value: 'external-reference',
1293
+ },
1294
+ ],
1295
+ optionLabel: 'label',
1296
+ optionValue: 'value',
1297
+ orientation: 'vertical',
1298
+ colSpan: 6,
1299
+ validators: [ValidatorConfig.required()],
1300
+ }));
1301
+ fields.push(storageKind === 'external-reference'
1302
+ ? new TextFieldConfig({
1303
+ key: credentialReferenceControlKey(index),
1304
+ label: this.t('credential.external-reference'),
1305
+ hint: this.t('credential.external-reference-hint'),
1306
+ colSpan: 12,
1307
+ validators: [
1308
+ ValidatorConfig.required(),
1309
+ ValidatorConfig.maxLength(500),
1310
+ ValidatorConfig.pattern('^EmailSecrets:[A-Za-z0-9_.-]{1,100}$'),
1311
+ ],
1312
+ })
1313
+ : new TextFieldConfig({
1314
+ key: credentialValueControlKey(index),
1315
+ label: field.displayName,
1316
+ inputType: 'password',
1317
+ hint: this.t('credential.protected-value-hint'),
1318
+ colSpan: 12,
1319
+ validators: [
1320
+ ValidatorConfig.required(),
1321
+ ...(field.maximumLength
1322
+ ? [ValidatorConfig.maxLength(field.maximumLength)]
1323
+ : []),
1324
+ ],
1325
+ }));
1326
+ return fields;
1327
+ });
1328
+ }
1329
+ toProviderFieldConfig(field, index) {
1330
+ const config = {
1331
+ key: providerFieldControlKey(index),
1332
+ label: field.displayName,
1333
+ colSpan: 6,
1334
+ readonly: !!this.existingDetails()?.authorizationState &&
1335
+ !field.editableAfterAuthorization,
1336
+ validators: providerFieldValidators(field, this.t.bind(this)),
1337
+ };
1338
+ if (field.dataType === 'Boolean') {
1339
+ return new ToggleFieldConfig(config);
1340
+ }
1341
+ if (field.dataType === 'Integer' || field.dataType === 'Port') {
1342
+ return new NumberFieldConfig({
1343
+ ...config,
1344
+ min: field.dataType === 'Port' ? 1 : undefined,
1345
+ max: field.dataType === 'Port' ? 65_535 : undefined,
1346
+ useGrouping: false,
1347
+ });
1348
+ }
1349
+ if (field.dataType === 'Choice') {
1350
+ return new SelectFieldConfig({
1351
+ ...config,
1352
+ options: (field.allowedValues ?? []).map((value) => ({
1353
+ label: value,
1354
+ value,
1355
+ })),
1356
+ optionLabel: 'label',
1357
+ optionValue: 'value',
1358
+ });
1359
+ }
1360
+ return new TextFieldConfig({
1361
+ ...config,
1362
+ inputType: field.dataType === 'Email' ? 'email' : 'text',
1363
+ });
1364
+ }
1365
+ getStructureSignature(value, provider) {
1366
+ const credentials = provider?.fields.flatMap((field, index) => field.isSecret && isApplicable(field, value.authenticationMode)
1367
+ ? [
1368
+ `${value[credentialOperationControlKey(index)] ?? ''}:${value[credentialStorageControlKey(index)] ?? ''}`,
1369
+ ]
1370
+ : []);
1371
+ return `${value.providerKey ?? ''}|${value.authenticationMode ?? ''}|${credentials?.join('|') ?? ''}`;
1372
+ }
1373
+ clearSecretControls() {
1374
+ const provider = this.selectedProvider();
1375
+ if (!provider)
1376
+ return;
1377
+ const value = { ...this.form.getRawValue() };
1378
+ provider.fields.forEach((field, index) => {
1379
+ if (!field.isSecret)
1380
+ return;
1381
+ value[credentialValueControlKey(index)] = null;
1382
+ value[credentialReferenceControlKey(index)] = null;
1383
+ });
1384
+ this.formValue.set(value);
1385
+ this.form.setValue(value, { emitEvent: false });
1386
+ this.formGeneration.update((generation) => generation + 1);
1387
+ }
1388
+ handleSaveError(error) {
1389
+ this.facade.showError(error);
1390
+ const id = this.connectionId();
1391
+ if (!(error instanceof HttpErrorResponse) || error.status !== 409 || !id) {
1392
+ return;
1393
+ }
1394
+ this.editorLoading.set(true);
1395
+ this.facade
1396
+ .loadConnection(id)
1397
+ .pipe(finalize(() => this.editorLoading.set(false)))
1398
+ .subscribe({
1399
+ next: (details) => {
1400
+ this.existingDetails.set(details);
1401
+ this.seedForm(details);
1402
+ },
1403
+ error: (loadError) => this.facade.showError(loadError),
1404
+ });
1405
+ }
1406
+ t(key) {
1407
+ return this.transloco.translate(`email-delivery.${key}`);
1408
+ }
1409
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1410
+ 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 });
1411
+ }
1412
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionEditor, decorators: [{
1413
+ type: Component,
1414
+ args: [{ selector: 'mt-email-connection-editor', imports: [
1415
+ ReactiveFormsModule,
1416
+ TranslocoDirective,
1417
+ SkeletonModule,
1418
+ DynamicForm,
1419
+ Button,
1420
+ ], 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" }]
1421
+ }], ctorParameters: () => [], propDecorators: { editorConnectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorConnectionId", required: false }] }] } });
1422
+ // Mirrors the section-mode styling mt-client-form applies to its own sections.
1423
+ const SECTION_MODE_LAYOUT = {
1424
+ sectionClass: 'flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0',
1425
+ };
1426
+ 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';
1427
+ const SECTION_MODE_BODY_CLASS = '@container grid grid-cols-12 gap-4 items-start p-4';
1428
+ function emptyEditorValue() {
1429
+ return {
1430
+ name: null,
1431
+ providerKey: null,
1432
+ authenticationMode: null,
1433
+ senderAddress: null,
1434
+ senderDisplayName: null,
1435
+ };
1436
+ }
1437
+ function isApplicable(field, mode) {
1438
+ return (!!mode &&
1439
+ (field.authenticationModes.length === 0 ||
1440
+ field.authenticationModes.includes(mode)));
1441
+ }
1442
+ function toEditorFieldValue(field, value) {
1443
+ if (value === null || value === undefined || value === '')
1444
+ return null;
1445
+ if (field.dataType === 'Boolean')
1446
+ return value.toLowerCase() === 'true';
1447
+ if (field.dataType === 'Integer' || field.dataType === 'Port') {
1448
+ const numberValue = Number(value);
1449
+ return Number.isFinite(numberValue) ? numberValue : null;
1450
+ }
1451
+ return value;
1452
+ }
1453
+ function providerFieldValidators(field, translate) {
1454
+ const validators = [];
1455
+ if (field.required)
1456
+ validators.push(ValidatorConfig.required());
1457
+ if (field.maximumLength) {
1458
+ validators.push(ValidatorConfig.maxLength(field.maximumLength));
1459
+ }
1460
+ if (field.dataType === 'Email')
1461
+ validators.push(ValidatorConfig.email());
1462
+ if (field.dataType === 'Text') {
1463
+ validators.push(ValidatorConfig.pattern('^[^\\r\\n\\u0000]*$'));
1464
+ }
1465
+ if (field.dataType === 'Identifier') {
1466
+ validators.push(ValidatorConfig.pattern('^[A-Za-z0-9._-]+$'));
1467
+ }
1468
+ if (field.dataType === 'Host') {
1469
+ validators.push(ValidatorConfig.custom((value) => validHost(value), translate('validation.host')));
1470
+ }
1471
+ if (field.dataType === 'Url') {
1472
+ validators.push(ValidatorConfig.custom((value) => validHttpsUrl(value), translate('validation.https-url')));
1473
+ }
1474
+ if (field.dataType === 'HeaderName') {
1475
+ validators.push(ValidatorConfig.pattern('^X-[A-Za-z0-9-]+$'));
1476
+ }
1477
+ return validators;
1478
+ }
1479
+ function validHost(value) {
1480
+ const host = String(value ?? '')
1481
+ .trim()
1482
+ .toLowerCase();
1483
+ return (!!host &&
1484
+ host !== 'localhost' &&
1485
+ !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) &&
1486
+ /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,62}$/.test(host));
1487
+ }
1488
+ function validHttpsUrl(value) {
1489
+ try {
1490
+ const url = new URL(String(value ?? ''));
1491
+ return (url.protocol === 'https:' &&
1492
+ !url.username &&
1493
+ !url.password &&
1494
+ !url.hash &&
1495
+ url.hostname.toLowerCase() !== 'localhost');
1496
+ }
1497
+ catch {
1498
+ return false;
1499
+ }
1500
+ }
1501
+
1502
+ class EmailConnectionDetailsPage {
1503
+ connectionId = input.required(...(ngDevMode ? [{ debugName: "connectionId" }] : /* istanbul ignore next */ []));
1504
+ document = inject(DOCUMENT);
1505
+ ref = inject(ModalRef);
1506
+ confirmation = inject(ConfirmationService);
1507
+ transloco = inject(TranslocoService);
1508
+ modal = inject(ModalService);
1509
+ facade = inject(EmailDeliveryFacade);
1510
+ activeLanguage = toSignal(this.transloco.langChanges$, {
1511
+ initialValue: this.transloco.getActiveLang(),
1512
+ });
1513
+ actions = computed(() => {
1514
+ const details = this.facade.selectedConnection();
1515
+ if (!details || details.connection.id !== this.connectionId())
1516
+ return [];
1517
+ return getApplicableProviderActions(this.facade.providerByKey(details.connection.providerKey), details.connection.authenticationMode, details.connection.publicConfiguration['AuthorizationModel']);
1518
+ }, ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
1519
+ provider = computed(() => {
1520
+ const details = this.facade.selectedConnection();
1521
+ return details
1522
+ ? this.facade.providerByKey(details.connection.providerKey)
1523
+ : null;
1524
+ }, ...(ngDevMode ? [{ debugName: "provider" }] : /* istanbul ignore next */ []));
1525
+ /**
1526
+ * The provider descriptor's own warnings, shown here and not only while
1527
+ * editing: a connection whose mode carries a standing caveat — the QC SMTP
1528
+ * endpoint that runs without TLS or authentication, for instance — must
1529
+ * still say so to whoever opens it later.
1530
+ */
1531
+ providerWarnings = computed(() => this.provider()?.warnings ?? [], ...(ngDevMode ? [{ debugName: "providerWarnings" }] : /* istanbul ignore next */ []));
1532
+ /**
1533
+ * The contract assigns each connection state one next step. Saying it in
1534
+ * words means the chip is not the only place the operator can read what to
1535
+ * do, which is also what keeps the state legible without relying on color.
1536
+ */
1537
+ stateGuidance = computed(() => {
1538
+ this.activeLanguage();
1539
+ const details = this.facade.selectedConnection();
1540
+ if (!details)
1541
+ return '';
1542
+ return this.optionalT(`state-guidance.${toKebabCase$1(details.connection.state)}`);
1543
+ }, ...(ngDevMode ? [{ debugName: "stateGuidance" }] : /* istanbul ignore next */ []));
1544
+ /**
1545
+ * The last validation code, so an invalid connection shows the reason rather
1546
+ * than only a chip that says something went wrong.
1547
+ */
1548
+ lastValidationMessage = computed(() => {
1549
+ this.activeLanguage();
1550
+ const code = this.facade.selectedConnection()?.connection.lastValidationCode;
1551
+ return code ? this.facade.describeCode(code) : '';
1552
+ }, ...(ngDevMode ? [{ debugName: "lastValidationMessage" }] : /* istanbul ignore next */ []));
1553
+ authenticationModeLabel = computed(() => {
1554
+ const details = this.facade.selectedConnection();
1555
+ if (!details)
1556
+ return '';
1557
+ return (this.provider()?.authenticationModes.find((mode) => mode.key === details.connection.authenticationMode)?.displayName ?? details.connection.authenticationMode);
1558
+ }, ...(ngDevMode ? [{ debugName: "authenticationModeLabel" }] : /* istanbul ignore next */ []));
1559
+ publicConfiguration = computed(() => {
1560
+ this.activeLanguage();
1561
+ const details = this.facade.selectedConnection();
1562
+ if (!details)
1563
+ return [];
1564
+ const fields = this.provider()?.fields ?? [];
1565
+ const fieldOrder = new Map(fields.map((field, index) => [field.key.toLowerCase(), index]));
1566
+ return Object.entries(details.connection.publicConfiguration)
1567
+ .filter(([, value]) => value !== '')
1568
+ .sort(([left], [right]) => (fieldOrder.get(left.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
1569
+ (fieldOrder.get(right.toLowerCase()) ?? Number.MAX_SAFE_INTEGER))
1570
+ .map(([key, value]) => {
1571
+ const field = fields.find((candidate) => candidate.key.toLowerCase() === key.toLowerCase());
1572
+ return {
1573
+ key,
1574
+ label: field?.displayName ?? humanizeIdentifier(key),
1575
+ value: this.configurationValueLabel(value, field),
1576
+ };
1577
+ });
1578
+ }, ...(ngDevMode ? [{ debugName: "publicConfiguration" }] : /* istanbul ignore next */ []));
1579
+ credentialItems = computed(() => {
1580
+ this.activeLanguage();
1581
+ const details = this.facade.selectedConnection();
1582
+ if (!details)
1583
+ return [];
1584
+ const fields = this.provider()?.fields ?? [];
1585
+ return details.credentials.map((credential) => ({
1586
+ key: credential.key,
1587
+ label: fields.find((field) => field.key.toLowerCase() === credential.key.toLowerCase())?.displayName ?? humanizeIdentifier(credential.key),
1588
+ configured: credential.configured,
1589
+ storageLabel: this.credentialStorageLabel(credential.storageKind),
1590
+ lastRotatedAtUtc: credential.lastRotatedAtUtc,
1591
+ }));
1592
+ }, ...(ngDevMode ? [{ debugName: "credentialItems" }] : /* istanbul ignore next */ []));
1593
+ actionMenuItems = computed(() => {
1594
+ this.activeLanguage();
1595
+ const details = this.facade.selectedConnection();
1596
+ if (!details || !this.facade.managementAvailable())
1597
+ return [];
1598
+ const items = this.interactiveActions().map((action) => ({
1599
+ id: action.key,
1600
+ label: action.displayName,
1601
+ icon: 'security.lock-unlocked-01',
1602
+ command: () => this.authorize(action),
1603
+ }));
1604
+ if (this.hasAction('validate')) {
1605
+ items.push({
1606
+ id: 'validate',
1607
+ label: this.t('validate'),
1608
+ icon: 'general.check-circle',
1609
+ command: () => this.validate(),
1610
+ });
1611
+ }
1612
+ if (this.hasAction('test-send')) {
1613
+ items.push({
1614
+ id: 'test-send',
1615
+ label: this.t('test-send'),
1616
+ icon: 'communication.send-01',
1617
+ command: () => this.test(),
1618
+ });
1619
+ }
1620
+ if (details.connection.state === 'Ready' &&
1621
+ !details.bindings.includes('notifications')) {
1622
+ items.push({
1623
+ id: 'bind',
1624
+ label: this.t('use-for-notifications'),
1625
+ icon: 'general.link-01',
1626
+ command: () => this.bind(),
1627
+ });
1628
+ }
1629
+ if (details.bindings.includes('notifications')) {
1630
+ items.push({
1631
+ id: 'unbind',
1632
+ label: this.t('stop-using'),
1633
+ icon: 'general.link-broken-01',
1634
+ severity: 'warn',
1635
+ command: () => this.unbind(),
1636
+ });
1637
+ }
1638
+ if (details.authorizationState) {
1639
+ items.push({
1640
+ id: 'revoke',
1641
+ label: this.t('revoke'),
1642
+ icon: 'security.lock-01',
1643
+ severity: 'warn',
1644
+ command: () => this.revoke(),
1645
+ });
1646
+ }
1647
+ if (details.bindings.length === 0) {
1648
+ items.push({ id: 'destructive-separator', separator: true }, {
1649
+ id: 'delete',
1650
+ label: this.t('delete'),
1651
+ icon: 'general.trash-01',
1652
+ severity: 'danger',
1653
+ command: () => this.delete(),
1654
+ });
1655
+ }
1656
+ return items;
1657
+ }, ...(ngDevMode ? [{ debugName: "actionMenuItems" }] : /* istanbul ignore next */ []));
1658
+ ngOnInit() {
1659
+ this.reload();
1660
+ }
1661
+ edit() {
1662
+ const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
1663
+ header: this.t('editor.edit-title'),
1664
+ styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
1665
+ position: 'end',
1666
+ appendTo: 'page-content',
1667
+ dismissableMask: true,
1668
+ dismissible: true,
1669
+ inputValues: { editorConnectionId: this.connectionId() },
1670
+ });
1671
+ drawer.onClose.pipe(take(1)).subscribe((saved) => {
1672
+ if (saved)
1673
+ this.reload();
1674
+ });
1675
+ }
1676
+ validate() {
1677
+ this.facade.validateConnection(this.connectionId()).subscribe({
1678
+ error: (error) => this.facade.showError(error),
1679
+ });
1680
+ }
1681
+ test() {
1682
+ const details = this.facade.selectedConnection();
1683
+ if (!details)
1684
+ return;
1685
+ const dialog = this.modal.openModal(EmailControlledTestDialog, 'dialog', {
1686
+ header: this.t('test.title'),
1687
+ styleClass: '!w-[42rem] max-w-[96vw]',
1688
+ dismissible: true,
1689
+ inputValues: {
1690
+ connectionId: details.connection.id,
1691
+ connectionName: details.connection.name,
1692
+ },
1693
+ });
1694
+ dialog.onClose.pipe(take(1)).subscribe(() => this.reload());
1695
+ }
1696
+ authorize(action) {
1697
+ const kind = action.key === 'authorize' ? 'delegated' : 'admin-consent';
1698
+ this.facade.startAuthorization(this.connectionId(), kind).subscribe({
1699
+ next: (result) => this.document.location.assign(result.authorizationUrl),
1700
+ error: (error) => this.facade.showError(error),
1701
+ });
1702
+ }
1703
+ revoke() {
1704
+ const details = this.facade.selectedConnection();
1705
+ if (!details)
1706
+ return;
1707
+ this.confirmation.confirm({
1708
+ type: 'dialog',
1709
+ header: this.t('revoke'),
1710
+ message: this.t('confirm.revoke'),
1711
+ accept: () => this.facade.revokeAuthorization(this.connectionId()).subscribe({
1712
+ error: (error) => this.facade.showError(error),
1713
+ }),
1714
+ });
1715
+ }
1716
+ bind() {
1717
+ this.facade.bindConnection(this.connectionId()).subscribe({
1718
+ error: (error) => this.facade.showError(error),
1719
+ });
1720
+ }
1721
+ unbind() {
1722
+ this.confirmation.confirm({
1723
+ type: 'dialog',
1724
+ header: this.t('stop-using'),
1725
+ message: this.t('confirm.unbind'),
1726
+ accept: () => this.facade.unbindConnection(this.connectionId()).subscribe({
1727
+ error: (error) => this.facade.showError(error),
1728
+ }),
1729
+ });
1730
+ }
1731
+ delete() {
1732
+ const details = this.facade.selectedConnection();
1733
+ if (!details)
1734
+ return;
1735
+ this.confirmation.confirmDelete({
1736
+ type: 'dialog',
1737
+ message: this.t('confirm.delete'),
1738
+ accept: () => this.facade
1739
+ .deleteConnection(this.connectionId(), details.connection.concurrencyToken)
1740
+ .subscribe({
1741
+ next: () => this.ref.close(true),
1742
+ error: (error) => this.facade.showError(error),
1743
+ }),
1744
+ });
1745
+ }
1746
+ hasAction(key) {
1747
+ return this.actions().some((action) => action.key === key);
1748
+ }
1749
+ interactiveActions() {
1750
+ return this.actions().filter((action) => action.isInteractive);
1751
+ }
1752
+ close() {
1753
+ this.ref.close();
1754
+ }
1755
+ reload() {
1756
+ this.facade.loadConnection(this.connectionId()).subscribe({
1757
+ error: (error) => this.facade.showError(error),
1758
+ });
1759
+ }
1760
+ t(key) {
1761
+ return this.transloco.translate(`email-delivery.${key}`);
1762
+ }
1763
+ /** Empty rather than the raw key when a state has no guidance sentence. */
1764
+ optionalT(key) {
1765
+ const fullKey = `email-delivery.${key}`;
1766
+ const translated = this.transloco.translate(fullKey);
1767
+ return translated === fullKey ? '' : translated;
1768
+ }
1769
+ credentialStorageLabel(storageKind) {
1770
+ switch (storageKind.replace(/[-_\s]/g, '').toLowerCase()) {
1771
+ case 'externalreference':
1772
+ return this.t('credential.external-reference');
1773
+ case 'protectedvalue':
1774
+ return this.t('credential.protected-value');
1775
+ default:
1776
+ return humanizeIdentifier(storageKind);
1777
+ }
1778
+ }
1779
+ configurationValueLabel(value, field) {
1780
+ if (value.toLowerCase() === 'true')
1781
+ return this.t('yes');
1782
+ if (value.toLowerCase() === 'false')
1783
+ return this.t('no');
1784
+ return field?.dataType === 'Choice' ? humanizeIdentifier(value) : value;
1785
+ }
1786
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
1787
+ 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 });
1788
+ }
1789
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailConnectionDetailsPage, decorators: [{
1790
+ type: Component,
1791
+ args: [{ selector: 'mt-email-connection-details', imports: [
1792
+ DatePipe,
1793
+ TranslocoDirective,
1794
+ SkeletonModule,
1795
+ Button,
1796
+ EmptyState,
1797
+ Menu,
1798
+ EmailDeliveryStatus,
1799
+ ], 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" }]
1800
+ }], propDecorators: { connectionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "connectionId", required: true }] }] } });
1801
+ function toKebabCase$1(value) {
1802
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
1803
+ }
1804
+ function humanizeIdentifier(value) {
1805
+ return value
1806
+ .replace(/[._-]+/g, ' ')
1807
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
1808
+ .replace(/\s+/g, ' ')
1809
+ .trim()
1810
+ .replace(/^./, (character) => character.toUpperCase())
1811
+ .replace(/\bSmtp\b/g, 'SMTP')
1812
+ .replace(/\bTls\b/g, 'TLS')
1813
+ .replace(/\bOauth2?\b/gi, 'OAuth2')
1814
+ .replace(/\bHttp\b/g, 'HTTP')
1815
+ .replace(/\bApi\b/g, 'API');
1816
+ }
1817
+
1818
+ class EmailDeliveryConnections {
1819
+ transloco = inject(TranslocoService);
1820
+ modal = inject(ModalService);
1821
+ confirmationService = inject(ConfirmationService);
1822
+ facade = inject(EmailDeliveryFacade);
1823
+ activeLanguage = toSignal(this.transloco.langChanges$, {
1824
+ initialValue: this.transloco.getActiveLang(),
1825
+ });
1826
+ canConfigure = computed(() => this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel), ...(ngDevMode ? [{ debugName: "canConfigure" }] : /* istanbul ignore next */ []));
1827
+ /** One rule for both the caption action and the empty-state action. */
1828
+ canCreate = computed(() => this.canConfigure() &&
1829
+ this.facade.managementAvailable() &&
1830
+ this.facade.providers().length > 0, ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
1831
+ rows = computed(() => this.facade.connections().map((connection) => ({
1832
+ ...connection,
1833
+ providerLabel: this.facade.providerByKey(connection.providerKey)?.displayName ??
1834
+ connection.providerKey,
1835
+ usedForNotifications: connection.bindings.includes('notifications'),
1836
+ })), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
1837
+ columns = computed(() => {
1838
+ this.activeLanguage();
1839
+ return [
1840
+ {
1841
+ key: 'name',
1842
+ label: this.t('connection.name'),
1843
+ sortable: true,
1844
+ filterConfig: { type: 'text' },
1845
+ },
1846
+ {
1847
+ key: 'providerLabel',
1848
+ label: this.t('connection.provider'),
1849
+ width: '18rem',
1850
+ filterConfig: {
1851
+ type: 'select',
1852
+ options: this.facade.providers().map((provider) => ({
1853
+ label: provider.displayName,
1854
+ value: provider.displayName,
1855
+ })),
1856
+ },
1857
+ },
1858
+ {
1859
+ key: 'senderAddress',
1860
+ label: this.t('connection.sender'),
1861
+ width: '18rem',
1862
+ filterConfig: { type: 'text' },
1863
+ },
1864
+ {
1865
+ key: 'state',
1866
+ label: this.t('connection.state'),
1867
+ type: 'status',
1868
+ width: '15rem',
1869
+ filterConfig: {
1870
+ type: 'select',
1871
+ options: connectionStates.map((state) => ({
1872
+ label: this.t(`status.connection.${toKebabCase(state)}`),
1873
+ value: state,
1874
+ })),
1875
+ },
1876
+ statusMap: getEmailDeliveryStatusMap('connection', this.transloco.translate.bind(this.transloco)),
1877
+ },
1878
+ {
1879
+ key: 'usedForNotifications',
1880
+ label: this.t('in-use'),
1881
+ type: 'boolean',
1882
+ width: '10rem',
1883
+ readonly: true,
1884
+ filterConfig: { type: 'boolean' },
1885
+ },
1886
+ {
1887
+ key: 'lastValidatedAtUtc',
1888
+ label: this.t('connection.last-validation'),
1889
+ type: 'dateTime',
1890
+ width: '15rem',
1891
+ },
1892
+ ];
1893
+ }, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
1894
+ rowActions = computed(() => {
1895
+ this.activeLanguage();
1896
+ return [
1897
+ {
1898
+ icon: 'general.eye',
1899
+ label: this.t('details'),
1900
+ action: (row) => this.openDetails(row),
1901
+ },
1902
+ {
1903
+ icon: 'general.edit-01',
1904
+ label: this.t('edit'),
1905
+ hidden: () => !this.canMutate(),
1906
+ action: (row) => this.openEdit(row),
1907
+ },
1908
+ {
1909
+ icon: 'general.check-circle',
1910
+ label: this.t('validate'),
1911
+ hidden: (row) => !this.canMutate() ||
1912
+ !this.hasAction(row, 'validate'),
1913
+ loading: () => this.facade.mutationLoading() === 'validate',
1914
+ action: (row) => this.validate(row),
1915
+ },
1916
+ {
1917
+ icon: 'communication.send-01',
1918
+ label: this.t('test-send'),
1919
+ hidden: (row) => !this.canMutate() ||
1920
+ !this.hasAction(row, 'test-send'),
1921
+ action: (row) => this.openTest(row),
1922
+ },
1923
+ {
1924
+ icon: 'general.link-01',
1925
+ label: this.t('bind'),
1926
+ hidden: (row) => {
1927
+ const connection = row;
1928
+ return (!this.canMutate() ||
1929
+ connection.state !== 'Ready' ||
1930
+ connection.bindings.includes('notifications'));
1931
+ },
1932
+ action: (row) => this.bind(row),
1933
+ },
1934
+ {
1935
+ icon: 'general.link-broken-01',
1936
+ label: this.t('unbind'),
1937
+ color: 'warn',
1938
+ hidden: (row) => !this.canMutate() ||
1939
+ !row.bindings.includes('notifications'),
1940
+ action: (row) => this.confirmUnbind(row),
1941
+ },
1942
+ {
1943
+ icon: 'general.trash-01',
1944
+ label: this.t('delete'),
1945
+ color: 'danger',
1946
+ hidden: (row) => !this.canMutate() ||
1947
+ row.bindings.length > 0,
1948
+ action: (row) => this.confirmDelete(row),
1949
+ },
1950
+ ];
1951
+ }, ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
1952
+ createConnection() {
1953
+ this.openEditor(null);
1954
+ }
1955
+ openDetails(connection) {
1956
+ const drawer = this.modal.openModal(EmailConnectionDetailsPage, 'drawer', {
1957
+ header: this.t('connection-details'),
1958
+ styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
1959
+ position: 'end',
1960
+ appendTo: 'page-content',
1961
+ dismissableMask: true,
1962
+ dismissible: true,
1963
+ inputValues: { connectionId: connection.id },
1964
+ });
1965
+ drawer.onClose.subscribe(() => {
1966
+ this.facade.loadConnections().subscribe({
1967
+ error: (error) => this.facade.showError(error),
1968
+ });
1969
+ });
1970
+ }
1971
+ openEdit(connection) {
1972
+ this.openEditor(connection.id);
1973
+ }
1974
+ validate(connection) {
1975
+ this.facade.validateConnection(connection.id).subscribe({
1976
+ error: (error) => this.facade.showError(error),
1977
+ });
1978
+ }
1979
+ openTest(connection) {
1980
+ this.modal.openModal(EmailControlledTestDialog, 'dialog', {
1981
+ header: this.t('test.title'),
1982
+ styleClass: '!w-[42rem] max-w-[96vw]',
1983
+ dismissible: true,
1984
+ inputValues: {
1985
+ connectionId: connection.id,
1986
+ connectionName: connection.name,
1987
+ },
1988
+ });
1989
+ }
1990
+ bind(connection) {
1991
+ this.facade.bindConnection(connection.id).subscribe({
1992
+ error: (error) => this.facade.showError(error),
1993
+ });
1994
+ }
1995
+ confirmUnbind(connection) {
1996
+ this.confirmationService.confirm({
1997
+ type: 'dialog',
1998
+ header: this.t('unbind'),
1999
+ message: this.t('confirm.unbind'),
2000
+ accept: () => this.facade.unbindConnection(connection.id).subscribe({
2001
+ error: (error) => this.facade.showError(error),
2002
+ }),
2003
+ });
2004
+ }
2005
+ confirmDelete(connection) {
2006
+ this.confirmationService.confirmDelete({
2007
+ type: 'dialog',
2008
+ message: this.t('confirm.delete'),
2009
+ accept: () => this.facade
2010
+ .deleteConnection(connection.id, connection.concurrencyToken)
2011
+ .subscribe({ error: (error) => this.facade.showError(error) }),
2012
+ });
2013
+ }
2014
+ canMutate() {
2015
+ return this.canConfigure() && this.facade.managementAvailable();
2016
+ }
2017
+ openEditor(connectionId) {
2018
+ const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
2019
+ header: this.t(connectionId ? 'editor.edit-title' : 'editor.create-title'),
2020
+ styleClass: EMAIL_DELIVERY_DRAWER_STYLE_CLASS,
2021
+ position: 'end',
2022
+ appendTo: 'page-content',
2023
+ dismissableMask: true,
2024
+ dismissible: true,
2025
+ inputValues: { editorConnectionId: connectionId },
2026
+ });
2027
+ drawer.onClose.subscribe((saved) => {
2028
+ if (!saved)
2029
+ return;
2030
+ this.facade.loadConnections().subscribe({
2031
+ error: (error) => this.facade.showError(error),
2032
+ });
2033
+ });
2034
+ }
2035
+ hasAction(connection, actionKey) {
2036
+ return getApplicableProviderActions(this.facade.providerByKey(connection.providerKey), connection.authenticationMode).some((action) => action.key === actionKey);
2037
+ }
2038
+ t(key) {
2039
+ return this.transloco.translate(`email-delivery.${key}`);
2040
+ }
2041
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, deps: [], target: i0.ɵɵFactoryTarget.Component });
2042
+ 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 });
2043
+ }
2044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryConnections, decorators: [{
2045
+ type: Component,
2046
+ 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" }]
2047
+ }] });
2048
+ const connectionStates = [
2049
+ 'Draft',
2050
+ 'PendingAuthorization',
2051
+ 'PendingValidation',
2052
+ 'Ready',
2053
+ 'ReauthorizationRequired',
2054
+ 'ConditionalAccessRequired',
2055
+ 'Disabled',
2056
+ 'Invalid',
2057
+ ];
2058
+ function toKebabCase(value) {
2059
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
2060
+ }
2061
+
2062
+ class EmailDeliveryShell {
2063
+ route = inject(ActivatedRoute);
2064
+ router = inject(Router);
2065
+ transloco = inject(TranslocoService);
2066
+ toast = inject(ToastService);
2067
+ facade = inject(EmailDeliveryFacade);
2068
+ activeLanguage = toSignal(this.transloco.langChanges$, {
2069
+ initialValue: this.transloco.getActiveLang(),
2070
+ });
2071
+ activeTab = signal('connections', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
2072
+ canView = computed(() => this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel), ...(ngDevMode ? [{ debugName: "canView" }] : /* istanbul ignore next */ []));
2073
+ tabs = computed(() => {
2074
+ this.activeLanguage();
2075
+ return [
2076
+ { label: this.t('tabs.connections'), value: 'connections' },
2077
+ { label: this.t('tabs.activity'), value: 'activity' },
2078
+ ];
2079
+ }, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
2080
+ /**
2081
+ * Same trail the sibling control-panel pages render, so Email Delivery reads
2082
+ * as part of Product Settings rather than a standalone screen.
2083
+ */
2084
+ breadcrumbItems = computed(() => {
2085
+ this.activeLanguage();
2086
+ return [
2087
+ {
2088
+ label: '',
2089
+ icon: 'general.home-line',
2090
+ routerLink: '/control-panel/workspaces',
2091
+ },
2092
+ {
2093
+ label: this.t('breadcrumb.product-settings'),
2094
+ routerLink: PRODUCT_SETTINGS_ROUTE,
2095
+ },
2096
+ { label: this.t('title') },
2097
+ ];
2098
+ }, ...(ngDevMode ? [{ debugName: "breadcrumbItems" }] : /* istanbul ignore next */ []));
2099
+ /**
2100
+ * Readiness moved out of a page-wide banner and onto a chip beside the title.
2101
+ * The banner's explanation survives as the chip's tooltip, so the reason is
2102
+ * still one hover away without a block of text above every tab.
2103
+ */
2104
+ readiness = computed(() => {
2105
+ const status = this.facade.status();
2106
+ if (!status)
2107
+ return null;
2108
+ if (!status.managementEnabled)
2109
+ return 'unavailable';
2110
+ return status.isReady ? 'ready' : 'needs-setup';
2111
+ }, ...(ngDevMode ? [{ debugName: "readiness" }] : /* istanbul ignore next */ []));
2112
+ readinessHint = computed(() => {
2113
+ this.activeLanguage();
2114
+ const status = this.facade.status();
2115
+ return status ? this.facade.describeCode(status.code) : '';
2116
+ }, ...(ngDevMode ? [{ debugName: "readinessHint" }] : /* istanbul ignore next */ []));
2117
+ constructor() {
2118
+ this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
2119
+ this.activeTab.set(params.get('tab') === 'activity' ? 'activity' : 'connections');
2120
+ });
2121
+ }
2122
+ ngOnInit() {
2123
+ this.resolveAccess();
2124
+ }
2125
+ retryPermissions() {
2126
+ if (this.facade.permissionsLoading())
2127
+ return;
2128
+ this.resolveAccess();
2129
+ }
2130
+ resolveAccess() {
2131
+ this.facade.loadPermissions(true).subscribe({
2132
+ next: () => {
2133
+ if (!this.canView())
2134
+ return;
2135
+ this.facade.initialize();
2136
+ this.syncTabFromUrl();
2137
+ this.handleAuthorizationCallback();
2138
+ },
2139
+ // The error is already on the facade; the template turns it into the
2140
+ // retryable state rather than the access-denied one.
2141
+ error: () => undefined,
2142
+ });
2143
+ }
2144
+ onTabChange(value) {
2145
+ if (value !== 'connections' && value !== 'activity')
2146
+ return;
2147
+ void this.router.navigate([], {
2148
+ relativeTo: this.route,
2149
+ queryParams: { tab: value === 'connections' ? null : value },
2150
+ queryParamsHandling: 'merge',
2151
+ });
2152
+ }
2153
+ goBack() {
2154
+ void this.router.navigate([PRODUCT_SETTINGS_ROUTE]);
2155
+ }
2156
+ syncTabFromUrl() {
2157
+ this.activeTab.set(this.route.snapshot.queryParamMap.get('tab') === 'activity'
2158
+ ? 'activity'
2159
+ : 'connections');
2160
+ }
2161
+ handleAuthorizationCallback() {
2162
+ const query = this.route.snapshot.queryParamMap;
2163
+ const authorization = query.get('authorization');
2164
+ const code = query.get('code');
2165
+ if (!authorization && !code)
2166
+ return;
2167
+ const message = this.facade.describeCode(code);
2168
+ if (authorization === 'completed') {
2169
+ this.toast.success(message);
2170
+ }
2171
+ else {
2172
+ this.toast.error(message);
2173
+ }
2174
+ const pendingConnectionId = this.facade.takePendingAuthorizationConnection();
2175
+ void this.router
2176
+ .navigate([], {
2177
+ relativeTo: this.route,
2178
+ queryParams: { authorization: null, code: null },
2179
+ queryParamsHandling: 'merge',
2180
+ replaceUrl: true,
2181
+ })
2182
+ .then(() => {
2183
+ if (!pendingConnectionId)
2184
+ return;
2185
+ this.facade.loadConnection(pendingConnectionId).subscribe({
2186
+ next: () => this.facade.loadConnections().subscribe(),
2187
+ error: (error) => this.facade.showError(error),
2188
+ });
2189
+ });
2190
+ }
2191
+ t(key) {
2192
+ return this.transloco.translate(`email-delivery.${key}`);
2193
+ }
2194
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
2195
+ 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 (facade.permissionsLoading()) {\r\n <div class=\"p-6 text-sm text-muted-color\" aria-live=\"polite\">\r\n {{ t(\"access-checking\") }}\r\n </div>\r\n } @else 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 if (facade.permissionsError()) {\r\n <!-- A failed access read is not a denial; say so and offer the retry,\r\n otherwise a transient 500 reads as \"you do not have access\". -->\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-error-title')\"\r\n [description]=\"t('access-error-description')\"\r\n [actionLabel]=\"t('access-retry')\"\r\n actionIcon=\"arrow.refresh-cw-01\"\r\n (actionClick)=\"retryPermissions()\"\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: 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 });
2196
+ }
2197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, decorators: [{
2198
+ type: Component,
2199
+ args: [{ selector: 'mt-email-delivery-shell', imports: [
2200
+ Page,
2201
+ TranslocoDirective,
2202
+ Breadcrumb,
2203
+ EmptyState,
2204
+ Tooltip,
2205
+ EmailDeliveryStatus,
2206
+ EmailDeliveryConnections,
2207
+ EmailDeliveryActivity,
2208
+ ], 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 (facade.permissionsLoading()) {\r\n <div class=\"p-6 text-sm text-muted-color\" aria-live=\"polite\">\r\n {{ t(\"access-checking\") }}\r\n </div>\r\n } @else 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 if (facade.permissionsError()) {\r\n <!-- A failed access read is not a denial; say so and offer the retry,\r\n otherwise a transient 500 reads as \"you do not have access\". -->\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-error-title')\"\r\n [description]=\"t('access-error-description')\"\r\n [actionLabel]=\"t('access-retry')\"\r\n actionIcon=\"arrow.refresh-cw-01\"\r\n (actionClick)=\"retryPermissions()\"\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" }]
2209
+ }], ctorParameters: () => [] });
2210
+ const PRODUCT_SETTINGS_ROUTE = '/control-panel/product-settings';
2211
+
2212
+ export { EmailDeliveryShell };
2213
+ //# sourceMappingURL=masterteam-system-settings-email-delivery-shell-Bjwtj5Ic.mjs.map