@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.
- package/README.md +23 -6
- package/assets/i18n/ar.json +439 -382
- package/assets/i18n/en.json +439 -382
- package/assets/system-settings.css +1 -1
- package/fesm2022/masterteam-system-settings-email-delivery-shell-Bjwtj5Ic.mjs +2213 -0
- package/fesm2022/masterteam-system-settings-email-delivery-shell-Bjwtj5Ic.mjs.map +1 -0
- package/fesm2022/masterteam-system-settings-masterteam-system-settings-CO-XTJOi.mjs +2707 -0
- package/fesm2022/masterteam-system-settings-masterteam-system-settings-CO-XTJOi.mjs.map +1 -0
- package/fesm2022/masterteam-system-settings.mjs +1 -1588
- package/fesm2022/masterteam-system-settings.mjs.map +1 -1
- package/package.json +4 -4
- package/types/masterteam-system-settings.d.ts +210 -2
- package/fesm2022/masterteam-system-settings-email-delivery-shell-Co2SifYp.mjs +0 -2768
- package/fesm2022/masterteam-system-settings-email-delivery-shell-Co2SifYp.mjs.map +0 -1
|
@@ -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
|