@masterteam/system-settings 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, inject, computed, ChangeDetectionStrategy, Component, Injectable, signal, DestroyRef } from '@angular/core';
2
+ import { input, inject, ChangeDetectionStrategy, Component, signal, computed, DestroyRef } from '@angular/core';
3
3
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { ActivatedRoute, Router } from '@angular/router';
5
- import { TranslocoService, TranslocoDirective } from '@jsverse/transloco';
5
+ import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
6
6
  import { Breadcrumb } from '@masterteam/components/breadcrumb';
7
7
  import { EmptyState } from '@masterteam/components/empty-state';
8
8
  import { Page } from '@masterteam/components/page';
9
9
  import { ToastService } from '@masterteam/components/toast';
10
10
  import { Tooltip } from '@masterteam/components/tooltip';
11
- import { EntityStatus } from '@masterteam/components/entities';
12
- import { HttpClient, HttpErrorResponse } from '@angular/common/http';
13
- import { mergeMap, of, throwError, catchError, map, switchMap, forkJoin, tap, finalize, take, timer } from 'rxjs';
14
- import { EMAIL_DELIVERY_OPTIONS, isConnectionTestAttempt, EMAIL_DELIVERY_ATTEMPT_STATUSES, EMAIL_DELIVERY_PERMISSIONS } from './masterteam-system-settings.mjs';
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';
15
12
  import { Icon } from '@masterteam/icons';
16
13
  import { Button } from '@masterteam/components/button';
17
14
  import { ModalService } from '@masterteam/components/modal';
18
15
  import { Table } from '@masterteam/components/table';
16
+ import { take, timer, of, map, switchMap, finalize } from 'rxjs';
19
17
  import { DatePipe, DOCUMENT } from '@angular/common';
20
18
  import { ModalRef } from '@masterteam/components/dialog';
21
19
  import * as i1 from '@angular/forms';
@@ -23,724 +21,13 @@ import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
23
21
  import { TextareaField } from '@masterteam/components/textarea-field';
24
22
  import { ToggleField } from '@masterteam/components/toggle-field';
25
23
  import { ConfirmationService } from '@masterteam/components/confirmation';
24
+ import { HttpErrorResponse } from '@angular/common/http';
26
25
  import { TextareaFieldConfig, TextFieldConfig, ToggleFieldConfig, ValidatorConfig, SelectFieldConfig, RadioButtonFieldConfig, NumberFieldConfig } from '@masterteam/components';
27
26
  import { DynamicForm } from '@masterteam/forms/dynamic-form';
28
27
  import { Menu } from '@masterteam/components/menu';
29
28
  import * as i1$1 from 'primeng/skeleton';
30
29
  import { SkeletonModule } from 'primeng/skeleton';
31
30
 
32
- const STATUS_DEFINITIONS = {
33
- // Page-level readiness, shown as a chip beside the page title instead of the
34
- // banner that used to sit above every tab.
35
- readiness: {
36
- ready: status('#059669', 'readiness.ready'),
37
- 'needs-setup': status('#D97706', 'readiness.needs-setup'),
38
- unavailable: status('#64748B', 'readiness.unavailable'),
39
- },
40
- connection: {
41
- Draft: status('#64748B', 'connection.draft'),
42
- PendingAuthorization: status('#D97706', 'connection.pending-authorization'),
43
- PendingValidation: status('#0284C7', 'connection.pending-validation'),
44
- Ready: status('#059669', 'connection.ready'),
45
- ReauthorizationRequired: status('#EA580C', 'connection.reauthorization-required'),
46
- ConditionalAccessRequired: status('#EA580C', 'connection.conditional-access-required'),
47
- Disabled: status('#64748B', 'connection.disabled'),
48
- Invalid: status('#DC2626', 'connection.invalid'),
49
- },
50
- attempt: {
51
- Reserved: status('#64748B', 'attempt.reserved'),
52
- ProviderInvocationStarted: status('#0284C7', 'attempt.provider-invocation-started'),
53
- ProviderInvocationDefinitivelyNotAccepted: status('#DC2626', 'attempt.provider-not-accepted'),
54
- ProviderResultReconciliation: status('#7C3AED', 'attempt.reconciling'),
55
- ProviderResultPending: status('#0284C7', 'attempt.pending'),
56
- Blocked: status('#DC2626', 'attempt.blocked'),
57
- RetryScheduled: status('#D97706', 'attempt.retry-scheduled'),
58
- Accepted: status('#059669', 'attempt.accepted'),
59
- OutcomeUnknown: status('#EA580C', 'attempt.outcome-unknown'),
60
- FailedPermanent: status('#DC2626', 'attempt.failed-permanent'),
61
- DeadLettered: status('#BE123C', 'attempt.dead-lettered'),
62
- },
63
- support: {
64
- active: status('#D97706', 'support.active'),
65
- 'repair-pending': status('#0284C7', 'support.repair-pending'),
66
- repaired: status('#059669', 'support.repaired'),
67
- 'acknowledgement-pending': status('#0284C7', 'support.acknowledgement-pending'),
68
- acknowledged: status('#64748B', 'support.acknowledged'),
69
- Owned: status('#059669', 'support.owned'),
70
- owned: status('#059669', 'support.owned'),
71
- Missing: status('#D97706', 'support.missing'),
72
- missing: status('#D97706', 'support.missing'),
73
- Conflict: status('#DC2626', 'support.conflict'),
74
- conflict: status('#DC2626', 'support.conflict'),
75
- IdentityIncomplete: status('#EA580C', 'support.identity-incomplete'),
76
- identityincomplete: status('#EA580C', 'support.identity-incomplete'),
77
- 'retryable-after-upgrade': status('#0284C7', 'support.retryable-after-upgrade'),
78
- 'repair-required': status('#EA580C', 'support.repair-required'),
79
- 'acknowledge-only': status('#64748B', 'support.acknowledge-only'),
80
- configured: status('#059669', 'support.configured'),
81
- 'not-configured': status('#64748B', 'support.not-configured'),
82
- },
83
- };
84
- class EmailDeliveryStatus {
85
- value = input.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
86
- kind = input('support', ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
87
- transloco = inject(TranslocoService);
88
- activeLanguage = toSignal(this.transloco.langChanges$, {
89
- initialValue: this.transloco.getActiveLang(),
90
- });
91
- statusData = computed(() => {
92
- this.activeLanguage();
93
- return {
94
- name: '',
95
- viewType: 'Status',
96
- value: getEmailDeliveryStatusValue(this.kind(), this.value(), (key) => this.transloco.translate(key)),
97
- configuration: { hideName: true },
98
- };
99
- }, ...(ngDevMode ? [{ debugName: "statusData" }] : /* istanbul ignore next */ []));
100
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, deps: [], target: i0.ɵɵFactoryTarget.Component });
101
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: EmailDeliveryStatus, isStandalone: true, selector: "mt-email-delivery-status", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, kind: { classPropertyName: "kind", publicName: "kind", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: `<mt-entity-status [data]="statusData()" />`, isInline: true, dependencies: [{ kind: "component", type: EntityStatus, selector: "mt-entity-status", inputs: ["data", "name", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
102
- }
103
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryStatus, decorators: [{
104
- type: Component,
105
- args: [{
106
- selector: 'mt-email-delivery-status',
107
- imports: [EntityStatus],
108
- template: `<mt-entity-status [data]="statusData()" />`,
109
- host: { class: 'inline-flex' },
110
- changeDetection: ChangeDetectionStrategy.OnPush,
111
- }]
112
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], kind: [{ type: i0.Input, args: [{ isSignal: true, alias: "kind", required: false }] }] } });
113
- function getEmailDeliveryStatusMap(kind, translate) {
114
- return Object.fromEntries(Object.entries(STATUS_DEFINITIONS[kind]).map(([key, definition]) => [
115
- key,
116
- {
117
- color: definition.color,
118
- display: translate(`email-delivery.status.${definition.translationKey}`),
119
- },
120
- ]));
121
- }
122
- function getEmailDeliveryStatusValue(kind, value, translate) {
123
- const definition = STATUS_DEFINITIONS[kind][value];
124
- return {
125
- key: value,
126
- display: definition
127
- ? translate(`email-delivery.status.${definition.translationKey}`)
128
- : value,
129
- color: definition?.color ?? '#64748B',
130
- };
131
- }
132
- function status(color, translationKey) {
133
- return { color, translationKey };
134
- }
135
-
136
- class EmailDeliveryApi {
137
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
138
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi });
139
- }
140
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryApi, decorators: [{
141
- type: Injectable
142
- }] });
143
-
144
- class EmailDeliveryHttpApi extends EmailDeliveryApi {
145
- http = inject(HttpClient);
146
- options = inject(EMAIL_DELIVERY_OPTIONS);
147
- getStatus() {
148
- return this.http.get(this.url('status'));
149
- }
150
- getProviders() {
151
- return this.http.get(this.url('providers'));
152
- }
153
- getErrorCatalog() {
154
- return this.http.get(this.url('error-catalog'));
155
- }
156
- getConnections() {
157
- return this.http.get(this.url('connections'));
158
- }
159
- getConnection(id) {
160
- return this.http.get(this.url(`connections/${encodeURIComponent(id)}`));
161
- }
162
- createConnection(request) {
163
- return this.http.post(this.url('connections'), request);
164
- }
165
- updateConnection(id, request) {
166
- return this.http.put(this.url(`connections/${encodeURIComponent(id)}`), request);
167
- }
168
- deleteConnection(id, concurrencyToken) {
169
- return this.http.delete(this.url(`connections/${encodeURIComponent(id)}`), { params: { concurrencyToken } });
170
- }
171
- bindConnection(purpose, connectionId) {
172
- return this.http.put(this.url(`bindings/${encodeURIComponent(purpose)}`), { connectionId });
173
- }
174
- unbindConnection(purpose) {
175
- return this.http.delete(this.url(`bindings/${encodeURIComponent(purpose)}`));
176
- }
177
- validateConnection(id) {
178
- return this.http.post(this.url(`connections/${encodeURIComponent(id)}/validate`), null);
179
- }
180
- sendConnectionTest(id, request) {
181
- return this.http
182
- .post(this.url(`connections/${encodeURIComponent(id)}/test`), request, { observe: 'response' })
183
- .pipe(mergeMap((response) => {
184
- const body = response.body;
185
- if (isEmailSendResult(body))
186
- return of(body);
187
- // 202 means the send was accepted without a result on this response.
188
- // The caller must read the outcome back with the same testRequestId;
189
- // posting again would deliver a second real email.
190
- if (response.status === 202) {
191
- return throwError(() => body ?? PENDING_CONTROLLED_TEST);
192
- }
193
- return throwError(() => body);
194
- }), catchError((error) => {
195
- const body = error instanceof HttpErrorResponse ? error.error : error;
196
- return isEmailSendResult(body) ? of(body) : throwError(() => error);
197
- }));
198
- }
199
- getConnectionTest(connectionId, testRequestId) {
200
- return this.http.get(this.url(`connections/${encodeURIComponent(connectionId)}/test-requests/${encodeURIComponent(testRequestId)}`));
201
- }
202
- startDelegatedAuthorization(id) {
203
- return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/delegated/start`), null);
204
- }
205
- startAdminConsent(id) {
206
- return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/admin-consent/start`), null);
207
- }
208
- revokeAuthorization(id) {
209
- return this.http.post(this.url(`connections/${encodeURIComponent(id)}/authorization/revoke`), null);
210
- }
211
- getAttempts(status, cursor, take) {
212
- return this.http.get(this.url('attempts'), {
213
- params: withoutNulls({ status, cursor, take }),
214
- });
215
- }
216
- replayAttempt(id, request) {
217
- return this.http.post(this.url(`attempts/${encodeURIComponent(id)}/replay`), request);
218
- }
219
- getTestRequest(testRequestId) {
220
- return this.http.get(this.url(`test-requests/${encodeURIComponent(testRequestId)}`));
221
- }
222
- sendTemplateTest(eventId, templateId, request) {
223
- return this.http
224
- .post(this.notificationUrl(`events/${eventId}/templates/${templateId}/test-email`), request)
225
- .pipe(map((response) => response.data));
226
- }
227
- sendAlertTest(alertId, request) {
228
- return this.http
229
- .post(this.notificationUrl(`alerts/${alertId}/test`), request)
230
- .pipe(map((response) => response.data));
231
- }
232
- getFinalizationQuarantine(cursor, take) {
233
- return this.http.get(this.url('finalization-quarantine'), { params: withoutNulls({ cursor, take }) });
234
- }
235
- retryFinalizationQuarantine(itemId, reason) {
236
- return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/retry`), { reason });
237
- }
238
- acknowledgeFinalizationQuarantine(itemId, reason) {
239
- return this.http.post(this.url(`finalization-quarantine/${encodeURIComponent(itemId)}/acknowledge`), { reason });
240
- }
241
- getHostEvidence(cursor, take) {
242
- return this.http.get(this.url('orphan-evidence'), {
243
- params: withoutNulls({ cursor, take }),
244
- });
245
- }
246
- exportHostEvidence(evidenceOccurrenceId) {
247
- return this.http
248
- .get(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/export`), {
249
- observe: 'response',
250
- responseType: 'blob',
251
- })
252
- .pipe(map((response) => toEvidenceDownload(response)));
253
- }
254
- repairHostEvidence(evidenceOccurrenceId, request) {
255
- return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/repair`), request);
256
- }
257
- acknowledgeHostEvidence(evidenceOccurrenceId, request) {
258
- return this.http.post(this.url(`orphan-evidence/${encodeURIComponent(evidenceOccurrenceId)}/acknowledge`), request);
259
- }
260
- rebuildEvidenceIndex(take) {
261
- return this.http.post(this.url('orphan-evidence/rebuild-index'), null, { params: { take } });
262
- }
263
- url(path) {
264
- return joinPath(this.options.apiBasePath, path);
265
- }
266
- notificationUrl(path) {
267
- return joinPath(this.options.notificationApiBasePath, path);
268
- }
269
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
270
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, providedIn: 'root' });
271
- }
272
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryHttpApi, decorators: [{
273
- type: Injectable,
274
- args: [{ providedIn: 'root' }]
275
- }] });
276
- /**
277
- * Stand-in problem for a bare `202 Accepted` that carries no body of its own —
278
- * it routes the dialog to retrieval by GUID, the same as the catalog problem
279
- * the backend normally returns for an in-flight test.
280
- */
281
- const PENDING_CONTROLLED_TEST = {
282
- code: 'email.delivery_test_request_in_progress',
283
- category: 'pending',
284
- retryable: true,
285
- action: 'poll_status',
286
- readinessImpact: 'unchanged',
287
- correlationId: '',
288
- retryAfterSeconds: null,
289
- };
290
- function isEmailSendResult(value) {
291
- return (typeof value === 'object' &&
292
- value !== null &&
293
- typeof value.outcome === 'string' &&
294
- typeof value.code === 'string');
295
- }
296
- function withoutNulls(values) {
297
- return Object.fromEntries(Object.entries(values).filter((entry) => entry[1] !== null));
298
- }
299
- function joinPath(base, path) {
300
- return `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
301
- }
302
- function toEvidenceDownload(response) {
303
- const contentDisposition = response.headers.get('content-disposition') ?? '';
304
- const fileNameMatch = /filename\*?=(?:UTF-8''|\")?([^\";]+)/i.exec(contentDisposition);
305
- return {
306
- blob: response.body ?? new Blob(),
307
- fileName: fileNameMatch
308
- ? decodeURIComponent(fileNameMatch[1].replace(/\"$/, ''))
309
- : 'email-delivery-evidence.json',
310
- contentType: response.headers.get('content-type') ?? 'application/octet-stream',
311
- };
312
- }
313
-
314
- const PENDING_AUTHORIZATION_STORAGE_KEY = 'mt.email-delivery.pending-authorization-connection';
315
- class EmailDeliveryFacade {
316
- options = inject(EMAIL_DELIVERY_OPTIONS);
317
- api = inject(EmailDeliveryHttpApi);
318
- transloco = inject(TranslocoService);
319
- toast = inject(ToastService);
320
- initialized = false;
321
- attemptsLoaded = false;
322
- status = signal(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
323
- providers = signal([], ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
324
- errorCatalog = signal([], ...(ngDevMode ? [{ debugName: "errorCatalog" }] : /* istanbul ignore next */ []));
325
- connections = signal([], ...(ngDevMode ? [{ debugName: "connections" }] : /* istanbul ignore next */ []));
326
- selectedConnection = signal(null, ...(ngDevMode ? [{ debugName: "selectedConnection" }] : /* istanbul ignore next */ []));
327
- attempts = signal([], ...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
328
- attemptsNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "attemptsNextCursor" }] : /* istanbul ignore next */ []));
329
- attemptStatusFilter = signal(null, ...(ngDevMode ? [{ debugName: "attemptStatusFilter" }] : /* istanbul ignore next */ []));
330
- quarantine = signal([], ...(ngDevMode ? [{ debugName: "quarantine" }] : /* istanbul ignore next */ []));
331
- quarantineNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "quarantineNextCursor" }] : /* istanbul ignore next */ []));
332
- hostEvidence = signal([], ...(ngDevMode ? [{ debugName: "hostEvidence" }] : /* istanbul ignore next */ []));
333
- hostEvidenceNextCursor = signal(null, ...(ngDevMode ? [{ debugName: "hostEvidenceNextCursor" }] : /* istanbul ignore next */ []));
334
- hostEvidenceScanBudgetExhausted = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceScanBudgetExhausted" }] : /* istanbul ignore next */ []));
335
- statusLoading = signal(false, ...(ngDevMode ? [{ debugName: "statusLoading" }] : /* istanbul ignore next */ []));
336
- catalogLoading = signal(false, ...(ngDevMode ? [{ debugName: "catalogLoading" }] : /* istanbul ignore next */ []));
337
- connectionsLoading = signal(false, ...(ngDevMode ? [{ debugName: "connectionsLoading" }] : /* istanbul ignore next */ []));
338
- detailsLoading = signal(false, ...(ngDevMode ? [{ debugName: "detailsLoading" }] : /* istanbul ignore next */ []));
339
- attemptsLoading = signal(false, ...(ngDevMode ? [{ debugName: "attemptsLoading" }] : /* istanbul ignore next */ []));
340
- quarantineLoading = signal(false, ...(ngDevMode ? [{ debugName: "quarantineLoading" }] : /* istanbul ignore next */ []));
341
- hostEvidenceLoading = signal(false, ...(ngDevMode ? [{ debugName: "hostEvidenceLoading" }] : /* istanbul ignore next */ []));
342
- mutationLoading = signal(null, ...(ngDevMode ? [{ debugName: "mutationLoading" }] : /* istanbul ignore next */ []));
343
- lastConnectionTest = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTest" }] : /* istanbul ignore next */ []));
344
- lastConnectionTestRequestId = signal(null, ...(ngDevMode ? [{ debugName: "lastConnectionTestRequestId" }] : /* istanbul ignore next */ []));
345
- activeConnection = computed(() => this.connections().find((connection) => connection.bindings.includes('notifications')), ...(ngDevMode ? [{ debugName: "activeConnection" }] : /* istanbul ignore next */ []));
346
- managementAvailable = computed(() => this.status()?.managementEnabled === true, ...(ngDevMode ? [{ debugName: "managementAvailable" }] : /* istanbul ignore next */ []));
347
- initialize() {
348
- if (this.initialized)
349
- return;
350
- this.initialized = true;
351
- this.loadStatus()
352
- .pipe(switchMap(() => forkJoin({
353
- catalog: this.loadCatalog(),
354
- connections: this.loadConnections(),
355
- })))
356
- .subscribe({ error: (error) => this.showError(error) });
357
- }
358
- hasPermission(permission) {
359
- return this.options.permissionKeys.includes(permission);
360
- }
361
- loadStatus() {
362
- this.statusLoading.set(true);
363
- return this.api.getStatus().pipe(tap((status) => this.status.set(status)), finalize(() => this.statusLoading.set(false)));
364
- }
365
- loadCatalog() {
366
- this.catalogLoading.set(true);
367
- return forkJoin({
368
- providers: this.api.getProviders(),
369
- errors: this.api.getErrorCatalog(),
370
- }).pipe(tap(({ providers, errors }) => {
371
- this.providers.set(providers);
372
- this.errorCatalog.set(errors);
373
- }), finalize(() => this.catalogLoading.set(false)));
374
- }
375
- loadConnections() {
376
- this.connectionsLoading.set(true);
377
- return this.api.getConnections().pipe(tap((connections) => this.connections.set(connections)), finalize(() => this.connectionsLoading.set(false)));
378
- }
379
- loadConnection(id) {
380
- this.detailsLoading.set(true);
381
- return this.api.getConnection(id).pipe(tap((details) => this.selectedConnection.set(details)), finalize(() => this.detailsLoading.set(false)));
382
- }
383
- createConnection(request) {
384
- this.mutationLoading.set('create');
385
- return this.api.createConnection(request).pipe(switchMap((details) => this.refreshConnection(details.connection.id)), tap(() => this.showSuccess('connection-created')), finalize(() => this.mutationLoading.set(null)));
386
- }
387
- updateConnection(id, request) {
388
- this.mutationLoading.set('update');
389
- return this.api.updateConnection(id, request).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-updated')), finalize(() => this.mutationLoading.set(null)));
390
- }
391
- deleteConnection(id, concurrencyToken) {
392
- this.mutationLoading.set('delete');
393
- return this.api.deleteConnection(id, concurrencyToken).pipe(this.reloadOnConflict(id), switchMap(() => forkJoin({
394
- connections: this.loadConnections(),
395
- status: this.loadStatus(),
396
- })), tap(() => {
397
- this.selectedConnection.set(null);
398
- this.showSuccess('connection-deleted');
399
- }), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
400
- }
401
- validateConnection(id) {
402
- this.mutationLoading.set('validate');
403
- return this.api.validateConnection(id).pipe(this.reloadOnConflict(id), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), tap((result) => result.isValid
404
- ? this.toast.success(this.describeCode(result.code))
405
- : this.toast.warn(this.describeCode(result.code))), finalize(() => this.mutationLoading.set(null)));
406
- }
407
- sendConnectionTest(id, request) {
408
- this.mutationLoading.set('test');
409
- this.lastConnectionTestRequestId.set(request.testRequestId);
410
- return this.api.sendConnectionTest(id, request).pipe(tap((result) => {
411
- this.lastConnectionTest.set(result);
412
- this.refreshActivity();
413
- }), switchMap((result) => this.refreshConnection(id).pipe(map(() => result))), finalize(() => this.mutationLoading.set(null)));
414
- }
415
- retrieveConnectionTest(connectionId, testRequestId) {
416
- this.mutationLoading.set('retrieve-test');
417
- return this.api.getConnectionTest(connectionId, testRequestId).pipe(tap((stored) => {
418
- this.lastConnectionTest.set(stored.result);
419
- if (stored.result)
420
- this.refreshActivity();
421
- }), finalize(() => this.mutationLoading.set(null)));
422
- }
423
- bindConnection(id, purpose = 'notifications') {
424
- this.mutationLoading.set('bind');
425
- return this.api.bindConnection(purpose, id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-bound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
426
- }
427
- unbindConnection(id, purpose = 'notifications') {
428
- this.mutationLoading.set('unbind');
429
- return this.api.unbindConnection(purpose).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('connection-unbound')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
430
- }
431
- startAuthorization(id, kind) {
432
- this.mutationLoading.set(kind);
433
- const request = kind === 'delegated'
434
- ? this.api.startDelegatedAuthorization(id)
435
- : this.api.startAdminConsent(id);
436
- return request.pipe(tap(() => this.rememberAuthorizationConnection(id)), finalize(() => this.mutationLoading.set(null)));
437
- }
438
- revokeAuthorization(id) {
439
- this.mutationLoading.set('revoke');
440
- return this.api.revokeAuthorization(id).pipe(this.reloadOnConflict(id), switchMap(() => this.refreshConnection(id)), tap(() => this.showSuccess('authorization-revoked')), map(() => undefined), finalize(() => this.mutationLoading.set(null)));
441
- }
442
- loadAttempts(reset = true, status = this.attemptStatusFilter()) {
443
- this.attemptsLoading.set(true);
444
- const cursor = reset ? null : this.attemptsNextCursor();
445
- if (reset)
446
- this.attemptStatusFilter.set(status);
447
- return this.api.getAttempts(status, cursor, 50).pipe(tap((page) => {
448
- this.attemptsLoaded = true;
449
- this.attempts.set(reset ? page.items : mergeById(this.attempts(), page.items, 'id'));
450
- this.attemptsNextCursor.set(page.nextCursor);
451
- }), finalize(() => this.attemptsLoading.set(false)));
452
- }
453
- replayAttempt(attempt, request) {
454
- this.mutationLoading.set(`replay-${attempt.id}`);
455
- return this.api.replayAttempt(attempt.id, request).pipe(tap((result) => {
456
- this.attempts.update((items) => replaceById(items, result.attempt, 'id'));
457
- this.showSuccess('attempt-replay-scheduled');
458
- }), finalize(() => this.mutationLoading.set(null)));
459
- }
460
- loadQuarantine(reset = true) {
461
- this.quarantineLoading.set(true);
462
- const cursor = reset ? null : this.quarantineNextCursor();
463
- return this.api.getFinalizationQuarantine(cursor, 100).pipe(tap((page) => {
464
- this.quarantine.set(reset
465
- ? page.items
466
- : mergeById(this.quarantine(), page.items, 'itemId'));
467
- this.quarantineNextCursor.set(page.nextCursor);
468
- }), finalize(() => this.quarantineLoading.set(false)));
469
- }
470
- decideQuarantine(item, action, reason) {
471
- this.mutationLoading.set(`${action}-${item.itemId}`);
472
- const request = action === 'retry'
473
- ? this.api.retryFinalizationQuarantine(item.itemId, reason)
474
- : this.api.acknowledgeFinalizationQuarantine(item.itemId, reason);
475
- return request.pipe(switchMap((updated) => this.loadQuarantine(true).pipe(map(() => updated))), tap(() => {
476
- this.showSuccess(action === 'retry'
477
- ? 'quarantine-retry-submitted'
478
- : 'quarantine-acknowledged');
479
- }), finalize(() => this.mutationLoading.set(null)));
480
- }
481
- loadHostEvidence(reset = true) {
482
- this.hostEvidenceLoading.set(true);
483
- const cursor = reset ? null : this.hostEvidenceNextCursor();
484
- return this.api.getHostEvidence(cursor, 100).pipe(tap((page) => {
485
- this.hostEvidence.set(reset
486
- ? page.items
487
- : mergeHostEvidence(this.hostEvidence(), page.items));
488
- this.hostEvidenceNextCursor.set(page.nextCursor);
489
- this.hostEvidenceScanBudgetExhausted.set(page.scanBudgetExhausted);
490
- }), finalize(() => this.hostEvidenceLoading.set(false)));
491
- }
492
- exportHostEvidence(evidenceOccurrenceId) {
493
- this.mutationLoading.set(`export-${evidenceOccurrenceId}`);
494
- return this.api
495
- .exportHostEvidence(evidenceOccurrenceId)
496
- .pipe(finalize(() => this.mutationLoading.set(null)));
497
- }
498
- decideHostEvidence(item, action, reason) {
499
- if (!item.evidenceOccurrenceId || !item.decisionToken) {
500
- return throwError(() => new Error('Incomplete evidence identity'));
501
- }
502
- this.mutationLoading.set(`${action}-${item.evidenceOccurrenceId}`);
503
- const request = {
504
- reason,
505
- decisionToken: item.decisionToken,
506
- };
507
- const operation = action === 'repair'
508
- ? this.api.repairHostEvidence(item.evidenceOccurrenceId, request)
509
- : this.api.acknowledgeHostEvidence(item.evidenceOccurrenceId, request);
510
- return operation.pipe(switchMap((updated) => this.loadHostEvidence(true).pipe(map(() => updated))), tap(() => {
511
- this.showSuccess(action === 'repair' ? 'evidence-repaired' : 'evidence-acknowledged');
512
- }), finalize(() => this.mutationLoading.set(null)));
513
- }
514
- rebuildEvidenceIndex(take) {
515
- this.mutationLoading.set('rebuild-evidence');
516
- return this.api.rebuildEvidenceIndex(take).pipe(switchMap((result) => this.loadHostEvidence(true).pipe(map(() => result))), tap(() => this.showSuccess('evidence-index-rebuilt')), finalize(() => this.mutationLoading.set(null)));
517
- }
518
- providerByKey(key) {
519
- return this.providers().find((provider) => provider.key === key) ?? null;
520
- }
521
- describeCode(code) {
522
- if (!code)
523
- return this.transloco.translate('email-delivery.unknown-code');
524
- const key = `email-delivery.codes.${code}`;
525
- const translated = this.transloco.translate(key);
526
- if (translated !== key)
527
- return translated;
528
- const contract = this.errorCatalog().find((entry) => entry.code === code);
529
- if (!contract || contract.frontendAction === 'none')
530
- return code;
531
- const actionKey = `email-delivery.error-actions.${contract.frontendAction}`;
532
- const action = this.transloco.translate(actionKey);
533
- return action === actionKey ? code : `${action} (${code})`;
534
- }
535
- /**
536
- * The one sentence every failure path shows. It prefers the backend's stable
537
- * code, falls back to the transport status the contract assigns a required
538
- * behaviour to (401/403/409/422/428/429/503), and always appends the
539
- * correlation ID when one is available — that identifier is what support asks
540
- * for, and it is the only part of a failure this page is allowed to keep.
541
- */
542
- describeError(error) {
543
- const failure = extractFailure(error);
544
- const parts = [this.describeFailure(failure)];
545
- if (failure.retryAfterSeconds !== null) {
546
- parts.push(this.transloco.translate('email-delivery.retry-after', {
547
- seconds: failure.retryAfterSeconds,
548
- }));
549
- }
550
- if (failure.correlationId) {
551
- parts.push(`${this.transloco.translate('email-delivery.correlation-id')}: ${failure.correlationId}`);
552
- }
553
- return parts.join(' ');
554
- }
555
- errorAction(error) {
556
- const failure = extractFailure(error);
557
- return (failure.action ||
558
- this.errorCatalog().find((entry) => entry.code === failure.code)
559
- ?.frontendAction ||
560
- statusAction(failure.status));
561
- }
562
- showError(error) {
563
- this.toast.error(this.describeError(error));
564
- }
565
- /**
566
- * A `409` means the server state moved under this page. The contract requires
567
- * reloading it rather than re-submitting over it, so every mutation that can
568
- * conflict routes through here: the latest connection, list and readiness are
569
- * pulled back before the caller sees the rejection.
570
- */
571
- reloadOnConflict(id) {
572
- return catchError((error) => {
573
- if (statusOf(error) !== 409)
574
- return throwError(() => error);
575
- const reload = id
576
- ? this.refreshConnection(id).pipe(map(() => undefined))
577
- : forkJoin({
578
- connections: this.loadConnections(),
579
- status: this.loadStatus(),
580
- }).pipe(map(() => undefined));
581
- reload.subscribe({ error: () => undefined });
582
- return throwError(() => error);
583
- });
584
- }
585
- describeFailure(failure) {
586
- const translated = failure.code ? this.translateCode(failure.code) : null;
587
- if (translated)
588
- return translated;
589
- const statusMessage = this.translateStatus(failure.status);
590
- if (statusMessage) {
591
- return failure.code
592
- ? `${statusMessage} (${failure.code})`
593
- : statusMessage;
594
- }
595
- return this.describeCode(failure.code);
596
- }
597
- translateCode(code) {
598
- const key = `email-delivery.codes.${code}`;
599
- const translated = this.transloco.translate(key);
600
- return translated === key ? null : translated;
601
- }
602
- translateStatus(status) {
603
- const key = status === null ? null : HTTP_STATUS_KEYS[status];
604
- if (!key)
605
- return null;
606
- const fullKey = `email-delivery.http.${key}`;
607
- const translated = this.transloco.translate(fullKey);
608
- return translated === fullKey ? null : translated;
609
- }
610
- takePendingAuthorizationConnection() {
611
- if (typeof sessionStorage === 'undefined')
612
- return null;
613
- const id = sessionStorage.getItem(PENDING_AUTHORIZATION_STORAGE_KEY);
614
- sessionStorage.removeItem(PENDING_AUTHORIZATION_STORAGE_KEY);
615
- return id;
616
- }
617
- /**
618
- * A completed controlled test writes its own Activity row, so the loaded
619
- * Activity list is stale the moment the test returns. Only refresh once
620
- * Activity has been opened — before that its own load covers it, and the
621
- * status filter in effect there is preserved.
622
- */
623
- refreshActivity() {
624
- if (!this.attemptsLoaded)
625
- return;
626
- this.loadAttempts(true).subscribe({ error: () => undefined });
627
- }
628
- refreshConnection(id) {
629
- return forkJoin({
630
- details: this.api.getConnection(id),
631
- connections: this.api.getConnections(),
632
- status: this.api.getStatus(),
633
- }).pipe(tap(({ details, connections, status }) => {
634
- this.selectedConnection.set(details);
635
- this.connections.set(connections);
636
- this.status.set(status);
637
- }), map(({ details }) => details));
638
- }
639
- rememberAuthorizationConnection(id) {
640
- if (typeof sessionStorage === 'undefined')
641
- return;
642
- sessionStorage.setItem(PENDING_AUTHORIZATION_STORAGE_KEY, id);
643
- }
644
- showSuccess(key) {
645
- this.toast.success(this.transloco.translate(`email-delivery.${key}`));
646
- }
647
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
648
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, providedIn: 'root' });
649
- }
650
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryFacade, decorators: [{
651
- type: Injectable,
652
- args: [{ providedIn: 'root' }]
653
- }] });
654
- const HTTP_STATUS_KEYS = {
655
- 401: 'unauthorized',
656
- 403: 'forbidden',
657
- 409: 'conflict',
658
- 422: 'unprocessable',
659
- 428: 'authorization-required',
660
- 429: 'too-many-requests',
661
- 503: 'unavailable',
662
- };
663
- const HTTP_STATUS_ACTIONS = {
664
- 403: 'request_permission',
665
- 409: 'refresh',
666
- 422: 'correct_configuration',
667
- 428: 'authorize',
668
- 429: 'retry_later',
669
- 503: 'retry_later',
670
- };
671
- function statusAction(status) {
672
- return status === null ? null : (HTTP_STATUS_ACTIONS[status] ?? null);
673
- }
674
- function statusOf(error) {
675
- return error instanceof HttpErrorResponse ? error.status : null;
676
- }
677
- function extractFailure(error) {
678
- const body = error instanceof HttpErrorResponse ? error.error : error;
679
- const problem = readProblem(body);
680
- const headers = error instanceof HttpErrorResponse ? error.headers : null;
681
- return {
682
- code: problem?.code ?? null,
683
- action: problem?.action || null,
684
- correlationId: problem?.correlationId ||
685
- headers?.get('x-correlation-id') ||
686
- headers?.get('correlation-id') ||
687
- null,
688
- retryAfterSeconds: problem?.retryAfterSeconds ??
689
- parseRetryAfter(headers?.get('retry-after') ?? null),
690
- status: statusOf(error),
691
- };
692
- }
693
- /**
694
- * Two envelopes reach this page. The email contract answers with the problem
695
- * shape itself; Gateway rejects a request before it ever gets there and answers
696
- * with `{ errors: { code, message } }` — that is how `REQ_001` arrives, and
697
- * reading only the first shape lost it.
698
- */
699
- function readProblem(body) {
700
- if (typeof body !== 'object' || body === null)
701
- return null;
702
- const problem = body;
703
- if (typeof problem.code === 'string')
704
- return problem;
705
- const gatewayCode = problem.errors?.code;
706
- if (typeof gatewayCode === 'string') {
707
- return { ...problem, code: gatewayCode };
708
- }
709
- return null;
710
- }
711
- /**
712
- * `Retry-After` is either a delay in seconds or an HTTP date. Both become a
713
- * whole number of seconds so the message can say when to try again; anything
714
- * unparseable or already in the past is simply not offered.
715
- */
716
- function parseRetryAfter(value) {
717
- if (!value)
718
- return null;
719
- const seconds = Number(value);
720
- if (Number.isFinite(seconds)) {
721
- return seconds > 0 ? Math.ceil(seconds) : null;
722
- }
723
- const date = Date.parse(value);
724
- if (Number.isNaN(date))
725
- return null;
726
- const delta = Math.ceil((date - Date.now()) / 1000);
727
- return delta > 0 ? delta : null;
728
- }
729
- function mergeById(current, incoming, key) {
730
- const values = new Map(current.map((item) => [item[key], item]));
731
- incoming.forEach((item) => values.set(item[key], item));
732
- return Array.from(values.values());
733
- }
734
- function replaceById(current, replacement, key) {
735
- return current.map((item) => item[key] === replacement[key] ? replacement : item);
736
- }
737
- function mergeHostEvidence(current, incoming) {
738
- const identity = (item) => item.evidenceOccurrenceId ?? item.itemId;
739
- const values = new Map(current.map((item) => [identity(item), item]));
740
- incoming.forEach((item) => values.set(identity(item), item));
741
- return Array.from(values.values());
742
- }
743
-
744
31
  class EmailOperatorReasonDialog {
745
32
  itemName = input.required(...(ngDevMode ? [{ debugName: "itemName" }] : /* istanbul ignore next */ []));
746
33
  consequence = input.required(...(ngDevMode ? [{ debugName: "consequence" }] : /* istanbul ignore next */ []));
@@ -1400,7 +687,7 @@ class EmailControlledTestDialog {
1400
687
  destroyRef = inject(DestroyRef);
1401
688
  transloco = inject(TranslocoService);
1402
689
  facade = inject(EmailDeliveryFacade);
1403
- testRequestId = signal(createId(), ...(ngDevMode ? [{ debugName: "testRequestId" }] : /* istanbul ignore next */ []));
690
+ testRequestId = signal(createEmailTestRequestId(), ...(ngDevMode ? [{ debugName: "testRequestId" }] : /* istanbul ignore next */ []));
1404
691
  timedOut = signal(false, ...(ngDevMode ? [{ debugName: "timedOut" }] : /* istanbul ignore next */ []));
1405
692
  polling = signal(false, ...(ngDevMode ? [{ debugName: "polling" }] : /* istanbul ignore next */ []));
1406
693
  result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []));
@@ -1433,7 +720,7 @@ class EmailControlledTestDialog {
1433
720
  rows: 3,
1434
721
  validators: [
1435
722
  ValidatorConfig.required(),
1436
- ValidatorConfig.custom((value) => validRecipients(value), this.t('test.recipients-invalid')),
723
+ ValidatorConfig.custom((value) => areEmailTestRecipientsValid(value, 100, 320), this.t('test.recipients-invalid')),
1437
724
  ],
1438
725
  }),
1439
726
  new TextFieldConfig({
@@ -1480,7 +767,7 @@ class EmailControlledTestDialog {
1480
767
  this.facade
1481
768
  .sendConnectionTest(this.connectionId(), {
1482
769
  testRequestId: this.testRequestId(),
1483
- recipients: parseRecipients(value.recipients),
770
+ recipients: parseEmailTestRecipients(value.recipients),
1484
771
  subject: value.subject?.trim() ?? '',
1485
772
  body: value.body ?? '',
1486
773
  isHtml: value.isHtml,
@@ -1502,7 +789,7 @@ class EmailControlledTestDialog {
1502
789
  // reusing it can never succeed, so the dialog mints a fresh one and
1503
790
  // waits for the operator to press Send again — it never resends.
1504
791
  if (isTestRequestConflict(error)) {
1505
- this.testRequestId.set(createId());
792
+ this.testRequestId.set(createEmailTestRequestId());
1506
793
  }
1507
794
  },
1508
795
  });
@@ -1523,7 +810,7 @@ class EmailControlledTestDialog {
1523
810
  this.polling.set(false);
1524
811
  return;
1525
812
  }
1526
- if (fromPoll && isPendingRequestState(stored.requestState)) {
813
+ if (fromPoll && isEmailTestRequestPending(stored.requestState)) {
1527
814
  this.schedulePoll();
1528
815
  }
1529
816
  else {
@@ -1588,35 +875,10 @@ function isTestRequestConflict(error) {
1588
875
  body.code === TEST_REQUEST_CONFLICT_CODE);
1589
876
  }
1590
877
  const POLL_DELAYS_MS = [1_000, 2_000, 4_000, 5_000, 5_000, 5_000];
1591
- function isPendingRequestState(value) {
1592
- return !['Completed', 'Failed', 'Cancelled', 'Canceled'].includes(value);
1593
- }
1594
- function parseRecipients(value) {
1595
- return Array.from(new Set((value ?? '')
1596
- .split(/[\s,;]+/)
1597
- .map((item) => item.trim())
1598
- .filter(Boolean)));
1599
- }
1600
- function validRecipients(value) {
1601
- const recipients = parseRecipients(String(value ?? ''));
1602
- return (recipients.length >= 1 &&
1603
- recipients.length <= 100 &&
1604
- recipients.every((recipient) => recipient.length <= 320 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient)));
1605
- }
1606
878
  function isAmbiguousTimeout(error) {
1607
879
  return (error instanceof HttpErrorResponse &&
1608
880
  (error.status === 0 || error.status === 408 || error.status === 504));
1609
881
  }
1610
- function createId() {
1611
- if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
1612
- return crypto.randomUUID();
1613
- }
1614
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => {
1615
- const random = Math.floor(Math.random() * 16);
1616
- const value = character === 'x' ? random : (random & 0x3) | 0x8;
1617
- return value.toString(16);
1618
- });
1619
- }
1620
882
 
1621
883
  function getApplicableProviderFields(provider, authenticationMode) {
1622
884
  if (!provider || !authenticationMode) {
@@ -2561,9 +1823,9 @@ class EmailDeliveryConnections {
2561
1823
  activeLanguage = toSignal(this.transloco.langChanges$, {
2562
1824
  initialValue: this.transloco.getActiveLang(),
2563
1825
  });
2564
- canConfigure = this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel);
1826
+ canConfigure = computed(() => this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel), ...(ngDevMode ? [{ debugName: "canConfigure" }] : /* istanbul ignore next */ []));
2565
1827
  /** One rule for both the caption action and the empty-state action. */
2566
- canCreate = computed(() => this.canConfigure &&
1828
+ canCreate = computed(() => this.canConfigure() &&
2567
1829
  this.facade.managementAvailable() &&
2568
1830
  this.facade.providers().length > 0, ...(ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []));
2569
1831
  rows = computed(() => this.facade.connections().map((connection) => ({
@@ -2750,7 +2012,7 @@ class EmailDeliveryConnections {
2750
2012
  });
2751
2013
  }
2752
2014
  canMutate() {
2753
- return this.canConfigure && this.facade.managementAvailable();
2015
+ return this.canConfigure() && this.facade.managementAvailable();
2754
2016
  }
2755
2017
  openEditor(connectionId) {
2756
2018
  const drawer = this.modal.openModal(EmailConnectionEditor, 'drawer', {
@@ -2807,7 +2069,7 @@ class EmailDeliveryShell {
2807
2069
  initialValue: this.transloco.getActiveLang(),
2808
2070
  });
2809
2071
  activeTab = signal('connections', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
2810
- canView = this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel);
2072
+ canView = computed(() => this.facade.hasPermission(EMAIL_DELIVERY_PERMISSIONS.controlPanel), ...(ngDevMode ? [{ debugName: "canView" }] : /* istanbul ignore next */ []));
2811
2073
  tabs = computed(() => {
2812
2074
  this.activeLanguage();
2813
2075
  return [
@@ -2858,11 +2120,26 @@ class EmailDeliveryShell {
2858
2120
  });
2859
2121
  }
2860
2122
  ngOnInit() {
2861
- if (!this.canView)
2123
+ this.resolveAccess();
2124
+ }
2125
+ retryPermissions() {
2126
+ if (this.facade.permissionsLoading())
2862
2127
  return;
2863
- this.facade.initialize();
2864
- this.syncTabFromUrl();
2865
- this.handleAuthorizationCallback();
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
+ });
2866
2143
  }
2867
2144
  onTabChange(value) {
2868
2145
  if (value !== 'connections' && value !== 'activity')
@@ -2915,7 +2192,7 @@ class EmailDeliveryShell {
2915
2192
  return this.transloco.translate(`email-delivery.${key}`);
2916
2193
  }
2917
2194
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
2918
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: EmailDeliveryShell, isStandalone: true, selector: "mt-email-delivery-shell", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n <!-- Declared unconditionally: a content query cannot reach a template that\r\n only exists inside a control-flow block, so the condition lives here. -->\r\n <ng-template #titleEnd>\r\n @if (canView && readiness(); as state) {\r\n <span class=\"shrink-0\" [mtTooltip]=\"readinessHint()\">\r\n <mt-email-delivery-status kind=\"readiness\" [value]=\"state\" />\r\n </span>\r\n }\r\n </ng-template>\r\n\r\n @if (canView) {\r\n <div\r\n class=\"flex min-h-0 flex-col gap-4 [&_.mt-actions-column]:w-[5rem] [&_mt-card]:rounded-md [&_mt-table_:has(>.p-datatable)]:rounded-md\"\r\n >\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems()\"\r\n styleClass=\"mx-1 flex justify-start\"\r\n />\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n </div>\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n", dependencies: [{ kind: "component", type: Page, selector: "mt-page", inputs: ["backButton", "backButtonIcon", "avatarIcon", "avatarStyle", "avatarShape", "title", "tabs", "activeTab", "contentClass", "contentId"], outputs: ["backButtonClick", "tabChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }, { kind: "component", type: EmptyState, selector: "mt-empty-state", inputs: ["variant", "illustration", "title", "description", "actionLabel", "actionIcon"], outputs: ["actionClick"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: EmailDeliveryStatus, selector: "mt-email-delivery-status", inputs: ["value", "kind"] }, { kind: "component", type: EmailDeliveryConnections, selector: "mt-email-delivery-connections" }, { kind: "component", type: EmailDeliveryActivity, selector: "mt-email-delivery-activity" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
2919
2196
  }
2920
2197
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: EmailDeliveryShell, decorators: [{
2921
2198
  type: Component,
@@ -2928,9 +2205,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
2928
2205
  EmailDeliveryStatus,
2929
2206
  EmailDeliveryConnections,
2930
2207
  EmailDeliveryActivity,
2931
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'email-delivery'\">\r\n <mt-page\r\n [title]=\"t('title')\"\r\n avatarIcon=\"communication.mail-01\"\r\n [avatarStyle]=\"{\r\n '--p-avatar-background': 'var(--p-blue-100)',\r\n '--p-avatar-color': 'var(--p-blue-700)',\r\n }\"\r\n [tabs]=\"canView ? tabs() : []\"\r\n [activeTab]=\"activeTab()\"\r\n contentClass=\"min-h-0 max-[1025px]:p-4 max-[640px]:p-3\"\r\n backButton\r\n (backButtonClick)=\"goBack()\"\r\n (tabChange)=\"onTabChange($event)\"\r\n >\r\n <!-- Declared unconditionally: a content query cannot reach a template that\r\n only exists inside a control-flow block, so the condition lives here. -->\r\n <ng-template #titleEnd>\r\n @if (canView && readiness(); as state) {\r\n <span class=\"shrink-0\" [mtTooltip]=\"readinessHint()\">\r\n <mt-email-delivery-status kind=\"readiness\" [value]=\"state\" />\r\n </span>\r\n }\r\n </ng-template>\r\n\r\n @if (canView) {\r\n <div\r\n class=\"flex min-h-0 flex-col gap-4 [&_.mt-actions-column]:w-[5rem] [&_mt-card]:rounded-md [&_mt-table_:has(>.p-datatable)]:rounded-md\"\r\n >\r\n <mt-breadcrumb\r\n [items]=\"breadcrumbItems()\"\r\n styleClass=\"mx-1 flex justify-start\"\r\n />\r\n\r\n @if (activeTab() === \"activity\") {\r\n <mt-email-delivery-activity />\r\n } @else {\r\n <mt-email-delivery-connections />\r\n }\r\n </div>\r\n } @else {\r\n <mt-empty-state\r\n illustration=\"none\"\r\n [title]=\"t('access-denied-title')\"\r\n [description]=\"t('access-denied-description')\"\r\n />\r\n }\r\n </mt-page>\r\n</ng-container>\r\n" }]
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" }]
2932
2209
  }], ctorParameters: () => [] });
2933
2210
  const PRODUCT_SETTINGS_ROUTE = '/control-panel/product-settings';
2934
2211
 
2935
2212
  export { EmailDeliveryShell };
2936
- //# sourceMappingURL=masterteam-system-settings-email-delivery-shell-C_AhRF1f.mjs.map
2213
+ //# sourceMappingURL=masterteam-system-settings-email-delivery-shell-Bjwtj5Ic.mjs.map