@masterteam/delegations 0.0.12 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/delegations.css +1 -1
- package/assets/i18n/ar.json +93 -4
- package/assets/i18n/en.json +92 -3
- package/fesm2022/masterteam-delegations.mjs +1605 -415
- package/fesm2022/masterteam-delegations.mjs.map +1 -1
- package/package.json +4 -4
- package/types/masterteam-delegations.d.ts +772 -153
|
@@ -1,26 +1,577 @@
|
|
|
1
|
+
import * as i1 from '@angular/common';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
1
3
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import { Router, RouterOutlet } from '@angular/router';
|
|
4
|
+
import { input, computed, Component, inject, Injectable, ChangeDetectionStrategy, viewChild, model, signal, effect, untracked, linkedSignal } from '@angular/core';
|
|
5
|
+
import { RouterLink, Router, RouterOutlet } from '@angular/router';
|
|
4
6
|
import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
|
|
5
|
-
import { Page } from '@masterteam/components/page';
|
|
6
|
-
import { CommonModule } from '@angular/common';
|
|
7
|
-
import { Table } from '@masterteam/components/table';
|
|
8
|
-
import * as i1 from 'primeng/skeleton';
|
|
9
|
-
import { SkeletonModule } from 'primeng/skeleton';
|
|
10
|
-
import { Action, Selector, State, Store, select } from '@ngxs/store';
|
|
11
|
-
import { HttpClient, HttpContext } from '@angular/common/http';
|
|
12
|
-
import { CrudStateBase, handleApiRequest, REQUEST_CONTEXT, UserSearchFieldConfig, TextareaFieldConfig, DateFieldConfig, RadioButtonFieldConfig, MultiSelectFieldConfig, ToggleFieldConfig, ValidatorConfig } from '@masterteam/components';
|
|
13
7
|
import { Avatar } from '@masterteam/components/avatar';
|
|
8
|
+
import { Button } from '@masterteam/components/button';
|
|
14
9
|
import { ModalService } from '@masterteam/components/modal';
|
|
15
|
-
import
|
|
10
|
+
import { Icon } from '@masterteam/icons';
|
|
11
|
+
import { Popover } from 'primeng/popover';
|
|
12
|
+
import { Chip } from '@masterteam/components/chip';
|
|
13
|
+
import { Actions, Store, ofActionSuccessful, ofActionDispatched, Action, Selector, State, select } from '@ngxs/store';
|
|
14
|
+
import { HttpClient, HttpParams, HttpContext } from '@angular/common/http';
|
|
15
|
+
import { handleApiRequest, REQUEST_CONTEXT, UserSearchFieldConfig, TextareaFieldConfig, DateFieldConfig, RadioButtonFieldConfig, MultiSelectFieldConfig, ToggleFieldConfig, ValidatorConfig } from '@masterteam/components';
|
|
16
|
+
import { EMPTY, catchError, switchMap, finalize } from 'rxjs';
|
|
17
|
+
import { ModalRef } from '@masterteam/components/dialog';
|
|
18
|
+
import { Page } from '@masterteam/components/page';
|
|
19
|
+
import { Breadcrumb } from '@masterteam/components/breadcrumb';
|
|
20
|
+
import { Table } from '@masterteam/components/table';
|
|
21
|
+
import * as i1$2 from '@angular/forms';
|
|
16
22
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
17
23
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
18
24
|
import { DynamicForm } from '@masterteam/forms/dynamic-form';
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
|
|
25
|
+
import * as i1$1 from 'primeng/skeleton';
|
|
26
|
+
import { SkeletonModule } from 'primeng/skeleton';
|
|
27
|
+
import * as i2 from 'primeng/tooltip';
|
|
28
|
+
import { TooltipModule } from 'primeng/tooltip';
|
|
29
|
+
|
|
30
|
+
const STATUS_VISUAL = {
|
|
31
|
+
Active: {
|
|
32
|
+
i18nKey: 'delegations.active',
|
|
33
|
+
styleClass: 'mt-status-chip mt-status-chip--active',
|
|
34
|
+
},
|
|
35
|
+
Scheduled: {
|
|
36
|
+
i18nKey: 'delegations.scheduled',
|
|
37
|
+
styleClass: 'mt-status-chip mt-status-chip--scheduled',
|
|
38
|
+
},
|
|
39
|
+
PendingApproval: {
|
|
40
|
+
i18nKey: 'delegations.pending-approval',
|
|
41
|
+
styleClass: 'mt-status-chip mt-status-chip--pending',
|
|
42
|
+
},
|
|
43
|
+
Expired: {
|
|
44
|
+
i18nKey: 'delegations.expired',
|
|
45
|
+
styleClass: 'mt-status-chip mt-status-chip--expired',
|
|
46
|
+
},
|
|
47
|
+
Rejected: {
|
|
48
|
+
i18nKey: 'delegations.rejected',
|
|
49
|
+
styleClass: 'mt-status-chip mt-status-chip--rejected',
|
|
50
|
+
},
|
|
51
|
+
Cancelled: {
|
|
52
|
+
i18nKey: 'delegations.cancelled',
|
|
53
|
+
styleClass: 'mt-status-chip mt-status-chip--cancelled',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
class DelegationStatusChip {
|
|
57
|
+
status = input.required(...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
58
|
+
visual = computed(() => STATUS_VISUAL[this.status()], ...(ngDevMode ? [{ debugName: "visual" }] : /* istanbul ignore next */ []));
|
|
59
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationStatusChip, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
60
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.8", type: DelegationStatusChip, isStandalone: true, selector: "mt-delegation-status-chip", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
61
|
+
<ng-container *transloco="let t">
|
|
62
|
+
@let v = visual();
|
|
63
|
+
<mt-chip [label]="t(v.i18nKey)" [styleClass]="v.styleClass"></mt-chip>
|
|
64
|
+
</ng-container>
|
|
65
|
+
`, isInline: true, styles: [":host{display:inline-flex}:host ::ng-deep .mt-status-chip{font-weight:500;font-size:.75rem;border-radius:9999px;padding-inline:.625rem;padding-block:.125rem}:host ::ng-deep .mt-status-chip--active{background-color:#dcfce7;color:#166534}:host ::ng-deep .mt-status-chip--scheduled{background-color:#dbeafe;color:#1e40af}:host ::ng-deep .mt-status-chip--pending{background-color:#fef9c3;color:#854d0e}:host ::ng-deep .mt-status-chip--expired{background-color:#f3f4f6;color:#4b5563}:host ::ng-deep .mt-status-chip--rejected{background-color:#fee2e2;color:#991b1b}:host ::ng-deep .mt-status-chip--cancelled{background-color:#e5e7eb;color:#374151}:host-context(.dark) ::ng-deep .mt-status-chip--active{background-color:#22c55e33;color:#86efac}:host-context(.dark) ::ng-deep .mt-status-chip--scheduled{background-color:#3b82f633;color:#93c5fd}:host-context(.dark) ::ng-deep .mt-status-chip--pending{background-color:#eab30833;color:#fde047}:host-context(.dark) ::ng-deep .mt-status-chip--expired{background-color:#94a3b833;color:#cbd5e1}:host-context(.dark) ::ng-deep .mt-status-chip--rejected{background-color:#ef444433;color:#fca5a5}:host-context(.dark) ::ng-deep .mt-status-chip--cancelled{background-color:#94a3b826;color:#cbd5e1}\n"], dependencies: [{ kind: "component", type: Chip, selector: "mt-chip", inputs: ["label", "icon", "image", "removable", "removeIcon", "styleClass"], outputs: ["onRemove", "onImageError"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] });
|
|
66
|
+
}
|
|
67
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationStatusChip, decorators: [{
|
|
68
|
+
type: Component,
|
|
69
|
+
args: [{ selector: 'mt-delegation-status-chip', standalone: true, imports: [Chip, TranslocoDirective], template: `
|
|
70
|
+
<ng-container *transloco="let t">
|
|
71
|
+
@let v = visual();
|
|
72
|
+
<mt-chip [label]="t(v.i18nKey)" [styleClass]="v.styleClass"></mt-chip>
|
|
73
|
+
</ng-container>
|
|
74
|
+
`, styles: [":host{display:inline-flex}:host ::ng-deep .mt-status-chip{font-weight:500;font-size:.75rem;border-radius:9999px;padding-inline:.625rem;padding-block:.125rem}:host ::ng-deep .mt-status-chip--active{background-color:#dcfce7;color:#166534}:host ::ng-deep .mt-status-chip--scheduled{background-color:#dbeafe;color:#1e40af}:host ::ng-deep .mt-status-chip--pending{background-color:#fef9c3;color:#854d0e}:host ::ng-deep .mt-status-chip--expired{background-color:#f3f4f6;color:#4b5563}:host ::ng-deep .mt-status-chip--rejected{background-color:#fee2e2;color:#991b1b}:host ::ng-deep .mt-status-chip--cancelled{background-color:#e5e7eb;color:#374151}:host-context(.dark) ::ng-deep .mt-status-chip--active{background-color:#22c55e33;color:#86efac}:host-context(.dark) ::ng-deep .mt-status-chip--scheduled{background-color:#3b82f633;color:#93c5fd}:host-context(.dark) ::ng-deep .mt-status-chip--pending{background-color:#eab30833;color:#fde047}:host-context(.dark) ::ng-deep .mt-status-chip--expired{background-color:#94a3b833;color:#cbd5e1}:host-context(.dark) ::ng-deep .mt-status-chip--rejected{background-color:#ef444433;color:#fca5a5}:host-context(.dark) ::ng-deep .mt-status-chip--cancelled{background-color:#94a3b826;color:#cbd5e1}\n"] }]
|
|
75
|
+
}], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: true }] }] } });
|
|
76
|
+
|
|
77
|
+
/** Reads sessionStorage + login candidates, restores any tab-local active session. */
|
|
78
|
+
class BootstrapDelegationSession {
|
|
79
|
+
static type = '[DelegationSession] Bootstrap';
|
|
80
|
+
}
|
|
81
|
+
/** Replaces the candidate pool (called after LoginSuccess or when user.delegations changes). */
|
|
82
|
+
class SetDelegationCandidates {
|
|
83
|
+
candidates;
|
|
84
|
+
static type = '[DelegationSession] Set Candidates';
|
|
85
|
+
constructor(candidates) {
|
|
86
|
+
this.candidates = candidates;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Start a delegated session for the given assignment. */
|
|
90
|
+
class StartDelegationSession {
|
|
91
|
+
delegationId;
|
|
92
|
+
static type = '[DelegationSession] Start';
|
|
93
|
+
constructor(delegationId) {
|
|
94
|
+
this.delegationId = delegationId;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** End the current delegated session (server + local). */
|
|
98
|
+
class EndDelegationSession {
|
|
99
|
+
options;
|
|
100
|
+
static type = '[DelegationSession] End';
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Atomically end current + start another. */
|
|
106
|
+
class SwitchDelegationSession {
|
|
107
|
+
delegationId;
|
|
108
|
+
static type = '[DelegationSession] Switch';
|
|
109
|
+
constructor(delegationId) {
|
|
110
|
+
this.delegationId = delegationId;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Refresh the delegation access token via refresh-token rotation. */
|
|
114
|
+
class RefreshDelegationSession {
|
|
115
|
+
static type = '[DelegationSession] Refresh';
|
|
116
|
+
}
|
|
117
|
+
/** Verify the current session against the server (used after bootstrap). */
|
|
118
|
+
class VerifyDelegationSession {
|
|
119
|
+
static type = '[DelegationSession] Verify';
|
|
120
|
+
}
|
|
121
|
+
/** Surface a recoverable session error from any pipeline (e.g. 403 codes). */
|
|
122
|
+
class DelegationSessionInvalidated {
|
|
123
|
+
reason;
|
|
124
|
+
static type = '[DelegationSession] Invalidated';
|
|
125
|
+
constructor(reason) {
|
|
126
|
+
this.reason = reason;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const DELEGATION_SESSION_STORAGE_KEY = 'mt.delegation.session.v1';
|
|
131
|
+
function toActiveSession(pair) {
|
|
132
|
+
return {
|
|
133
|
+
accessToken: pair.accessToken,
|
|
134
|
+
accessTokenExpiresAtUtc: pair.accessTokenExpiresAtUtc,
|
|
135
|
+
refreshToken: pair.refreshToken,
|
|
136
|
+
refreshTokenExpiresAtUtc: pair.refreshTokenExpiresAtUtc,
|
|
137
|
+
delegation: pair.delegation,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var DelegationSessionActionKey;
|
|
141
|
+
(function (DelegationSessionActionKey) {
|
|
142
|
+
DelegationSessionActionKey["Bootstrap"] = "bootstrap";
|
|
143
|
+
DelegationSessionActionKey["StartSession"] = "startSession";
|
|
144
|
+
DelegationSessionActionKey["EndSession"] = "endSession";
|
|
145
|
+
DelegationSessionActionKey["RefreshSession"] = "refreshSession";
|
|
146
|
+
DelegationSessionActionKey["GetSession"] = "getSession";
|
|
147
|
+
})(DelegationSessionActionKey || (DelegationSessionActionKey = {}));
|
|
148
|
+
|
|
149
|
+
var __decorate$1 = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
150
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
151
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
152
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
153
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
154
|
+
};
|
|
155
|
+
const BASE$1 = 'identity/delegations';
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Action shells for loose coupling with gateway-auth (matched by `type` string).
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
class GatewayLoginSuccessShell {
|
|
160
|
+
static type = '[Auth] Login Success';
|
|
161
|
+
}
|
|
162
|
+
class GatewayLogoutShell {
|
|
163
|
+
static type = '[Auth] Logout';
|
|
164
|
+
}
|
|
165
|
+
function readStored() {
|
|
166
|
+
try {
|
|
167
|
+
const raw = sessionStorage.getItem(DELEGATION_SESSION_STORAGE_KEY);
|
|
168
|
+
if (!raw)
|
|
169
|
+
return null;
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
if (!parsed?.accessToken || !parsed?.accessTokenExpiresAtUtc)
|
|
172
|
+
return null;
|
|
173
|
+
if (new Date(parsed.accessTokenExpiresAtUtc).getTime() < Date.now()) {
|
|
174
|
+
sessionStorage.removeItem(DELEGATION_SESSION_STORAGE_KEY);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function writeStored(value) {
|
|
184
|
+
try {
|
|
185
|
+
if (value) {
|
|
186
|
+
sessionStorage.setItem(DELEGATION_SESSION_STORAGE_KEY, JSON.stringify(value));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
sessionStorage.removeItem(DELEGATION_SESSION_STORAGE_KEY);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* sessionStorage may be unavailable in some embeds; ignore */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Filters a delegation list to "candidates the user could activate".
|
|
198
|
+
* Active + StartSession in allowedActions is the runtime gate.
|
|
199
|
+
*/
|
|
200
|
+
function pickCandidates(delegations) {
|
|
201
|
+
if (!Array.isArray(delegations))
|
|
202
|
+
return [];
|
|
203
|
+
return delegations.filter((d) => d.status === 'Active' && d.allowedActions?.includes('StartSession'));
|
|
204
|
+
}
|
|
205
|
+
let DelegationSessionState = class DelegationSessionState {
|
|
206
|
+
http = inject(HttpClient);
|
|
207
|
+
actions$ = inject(Actions);
|
|
208
|
+
store = inject(Store);
|
|
209
|
+
constructor() {
|
|
210
|
+
// ---- Bridge to gateway-auth lifecycle (loose coupling via Actions stream) ----
|
|
211
|
+
// We use local action *shells* matching gateway-auth's type strings to avoid
|
|
212
|
+
// a hard project dependency on @masterteam/gateway-auth. NGXS matches actions
|
|
213
|
+
// by the static `type` field, so any class with the same type string works.
|
|
214
|
+
this.actions$
|
|
215
|
+
.pipe(ofActionSuccessful(GatewayLoginSuccessShell))
|
|
216
|
+
.subscribe(() => {
|
|
217
|
+
this.store.dispatch(new BootstrapDelegationSession());
|
|
218
|
+
});
|
|
219
|
+
this.actions$
|
|
220
|
+
.pipe(ofActionDispatched(GatewayLogoutShell))
|
|
221
|
+
.subscribe(() => {
|
|
222
|
+
this.store.dispatch(new EndDelegationSession({ silent: true }));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Selectors
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
static getActive(state) {
|
|
229
|
+
return state.active;
|
|
230
|
+
}
|
|
231
|
+
static getCandidates(state) {
|
|
232
|
+
return state.candidates;
|
|
233
|
+
}
|
|
234
|
+
static isDelegated(state) {
|
|
235
|
+
return !!state.active;
|
|
236
|
+
}
|
|
237
|
+
static getLoadingActive(state) {
|
|
238
|
+
return state.loadingActive;
|
|
239
|
+
}
|
|
240
|
+
static getErrors(state) {
|
|
241
|
+
return state.errors;
|
|
242
|
+
}
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Actions
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
bootstrap(ctx) {
|
|
247
|
+
// 1. Hydrate active session from sessionStorage (tab-local).
|
|
248
|
+
const stored = readStored();
|
|
249
|
+
ctx.patchState({ active: stored });
|
|
250
|
+
// 2. Hydrate candidates from gateway-auth user.delegations (loose lookup).
|
|
251
|
+
const authUser = this.store.snapshot()['auth'];
|
|
252
|
+
const userDelegations = authUser?.user
|
|
253
|
+
?.delegations ?? [];
|
|
254
|
+
ctx.patchState({ candidates: pickCandidates(userDelegations) });
|
|
255
|
+
// 3. If we had a stored session, verify it against the server.
|
|
256
|
+
if (stored) {
|
|
257
|
+
this.store.dispatch(new VerifyDelegationSession());
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
setCandidates(ctx, { candidates }) {
|
|
261
|
+
ctx.patchState({ candidates });
|
|
262
|
+
}
|
|
263
|
+
verify(ctx) {
|
|
264
|
+
const req$ = this.http.get(`${BASE$1}/session`);
|
|
265
|
+
return handleApiRequest({
|
|
266
|
+
ctx,
|
|
267
|
+
key: DelegationSessionActionKey.GetSession,
|
|
268
|
+
request$: req$,
|
|
269
|
+
onSuccess: (response) => {
|
|
270
|
+
if (!response.data) {
|
|
271
|
+
writeStored(null);
|
|
272
|
+
return { active: null };
|
|
273
|
+
}
|
|
274
|
+
const current = ctx.getState().active;
|
|
275
|
+
if (current && response.data.id === current.delegation.id) {
|
|
276
|
+
// Server confirms the same delegation — keep token, just refresh summary.
|
|
277
|
+
const updated = {
|
|
278
|
+
...current,
|
|
279
|
+
delegation: response.data,
|
|
280
|
+
};
|
|
281
|
+
writeStored(updated);
|
|
282
|
+
return { active: updated };
|
|
283
|
+
}
|
|
284
|
+
// Mismatch — drop local session.
|
|
285
|
+
writeStored(null);
|
|
286
|
+
return { active: null };
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
start(ctx, { delegationId }) {
|
|
291
|
+
const req$ = this.http.post(`${BASE$1}/${delegationId}/token`, {});
|
|
292
|
+
return handleApiRequest({
|
|
293
|
+
ctx,
|
|
294
|
+
key: DelegationSessionActionKey.StartSession,
|
|
295
|
+
request$: req$,
|
|
296
|
+
onSuccess: (response) => {
|
|
297
|
+
const active = toActiveSession(response.data);
|
|
298
|
+
writeStored(active);
|
|
299
|
+
return { active };
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
end(ctx, { options }) {
|
|
304
|
+
const state = ctx.getState();
|
|
305
|
+
// Always clear local state immediately.
|
|
306
|
+
writeStored(null);
|
|
307
|
+
ctx.patchState({ active: null });
|
|
308
|
+
if (!state.active || options.silent) {
|
|
309
|
+
return EMPTY;
|
|
310
|
+
}
|
|
311
|
+
// Fire-and-forget the server end call; failures don't roll back local clear.
|
|
312
|
+
return this.http
|
|
313
|
+
.post(`${BASE$1}/session/end`, {})
|
|
314
|
+
.pipe(catchError(() => EMPTY));
|
|
315
|
+
}
|
|
316
|
+
switch(ctx, { delegationId }) {
|
|
317
|
+
return this.store
|
|
318
|
+
.dispatch(new EndDelegationSession({ silent: true }))
|
|
319
|
+
.pipe(switchMap(() => this.store.dispatch(new StartDelegationSession(delegationId))));
|
|
320
|
+
}
|
|
321
|
+
refresh(ctx) {
|
|
322
|
+
const current = ctx.getState().active;
|
|
323
|
+
if (!current)
|
|
324
|
+
return EMPTY;
|
|
325
|
+
const req$ = this.http.post(`${BASE$1}/token/refresh`, { refreshToken: current.refreshToken });
|
|
326
|
+
return handleApiRequest({
|
|
327
|
+
ctx,
|
|
328
|
+
key: DelegationSessionActionKey.RefreshSession,
|
|
329
|
+
request$: req$,
|
|
330
|
+
onSuccess: (response) => {
|
|
331
|
+
const active = toActiveSession(response.data);
|
|
332
|
+
writeStored(active);
|
|
333
|
+
return { active };
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
invalidated(ctx) {
|
|
338
|
+
// Same effect as EndDelegationSession({ silent: true }), but as a distinct
|
|
339
|
+
// signal so UI can react with a toast or modal.
|
|
340
|
+
writeStored(null);
|
|
341
|
+
ctx.patchState({ active: null });
|
|
342
|
+
return EMPTY;
|
|
343
|
+
}
|
|
344
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
345
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionState });
|
|
346
|
+
};
|
|
347
|
+
__decorate$1([
|
|
348
|
+
Action(BootstrapDelegationSession)
|
|
349
|
+
], DelegationSessionState.prototype, "bootstrap", null);
|
|
350
|
+
__decorate$1([
|
|
351
|
+
Action(SetDelegationCandidates)
|
|
352
|
+
], DelegationSessionState.prototype, "setCandidates", null);
|
|
353
|
+
__decorate$1([
|
|
354
|
+
Action(VerifyDelegationSession)
|
|
355
|
+
], DelegationSessionState.prototype, "verify", null);
|
|
356
|
+
__decorate$1([
|
|
357
|
+
Action(StartDelegationSession)
|
|
358
|
+
], DelegationSessionState.prototype, "start", null);
|
|
359
|
+
__decorate$1([
|
|
360
|
+
Action(EndDelegationSession)
|
|
361
|
+
], DelegationSessionState.prototype, "end", null);
|
|
362
|
+
__decorate$1([
|
|
363
|
+
Action(SwitchDelegationSession)
|
|
364
|
+
], DelegationSessionState.prototype, "switch", null);
|
|
365
|
+
__decorate$1([
|
|
366
|
+
Action(RefreshDelegationSession)
|
|
367
|
+
], DelegationSessionState.prototype, "refresh", null);
|
|
368
|
+
__decorate$1([
|
|
369
|
+
Action(DelegationSessionInvalidated)
|
|
370
|
+
], DelegationSessionState.prototype, "invalidated", null);
|
|
371
|
+
__decorate$1([
|
|
372
|
+
Selector()
|
|
373
|
+
], DelegationSessionState, "getActive", null);
|
|
374
|
+
__decorate$1([
|
|
375
|
+
Selector()
|
|
376
|
+
], DelegationSessionState, "getCandidates", null);
|
|
377
|
+
__decorate$1([
|
|
378
|
+
Selector()
|
|
379
|
+
], DelegationSessionState, "isDelegated", null);
|
|
380
|
+
__decorate$1([
|
|
381
|
+
Selector()
|
|
382
|
+
], DelegationSessionState, "getLoadingActive", null);
|
|
383
|
+
__decorate$1([
|
|
384
|
+
Selector()
|
|
385
|
+
], DelegationSessionState, "getErrors", null);
|
|
386
|
+
DelegationSessionState = __decorate$1([
|
|
387
|
+
State({
|
|
388
|
+
name: 'delegationSession',
|
|
389
|
+
defaults: {
|
|
390
|
+
active: null,
|
|
391
|
+
candidates: [],
|
|
392
|
+
loadingActive: [],
|
|
393
|
+
errors: {},
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
], DelegationSessionState);
|
|
397
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionState, decorators: [{
|
|
398
|
+
type: Injectable
|
|
399
|
+
}], ctorParameters: () => [], propDecorators: { bootstrap: [], setCandidates: [], verify: [], start: [], end: [], switch: [], refresh: [], invalidated: [] } });
|
|
400
|
+
|
|
401
|
+
class DelegationSessionFacade {
|
|
402
|
+
store = inject(Store);
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Data slices
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
active = select(DelegationSessionState.getActive);
|
|
407
|
+
candidates = select(DelegationSessionState.getCandidates);
|
|
408
|
+
isDelegated = select(DelegationSessionState.isDelegated);
|
|
409
|
+
loadingActive = select(DelegationSessionState.getLoadingActive);
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Derived (used by interceptor + topbar menu)
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
delegator = computed(() => this.active()?.delegation.delegator ?? null, ...(ngDevMode ? [{ debugName: "delegator" }] : /* istanbul ignore next */ []));
|
|
414
|
+
accessToken = computed(() => this.active()?.accessToken ?? null, ...(ngDevMode ? [{ debugName: "accessToken" }] : /* istanbul ignore next */ []));
|
|
415
|
+
accessTokenExpiresAtUtc = computed(() => this.active()?.accessTokenExpiresAtUtc ?? null, ...(ngDevMode ? [{ debugName: "accessTokenExpiresAtUtc" }] : /* istanbul ignore next */ []));
|
|
416
|
+
hasCandidates = computed(() => this.candidates().length > 0, ...(ngDevMode ? [{ debugName: "hasCandidates" }] : /* istanbul ignore next */ []));
|
|
417
|
+
isStartingSession = computed(() => this.loadingActive().includes(DelegationSessionActionKey.StartSession), ...(ngDevMode ? [{ debugName: "isStartingSession" }] : /* istanbul ignore next */ []));
|
|
418
|
+
isEndingSession = computed(() => this.loadingActive().includes(DelegationSessionActionKey.EndSession), ...(ngDevMode ? [{ debugName: "isEndingSession" }] : /* istanbul ignore next */ []));
|
|
419
|
+
isRefreshingSession = computed(() => this.loadingActive().includes(DelegationSessionActionKey.RefreshSession), ...(ngDevMode ? [{ debugName: "isRefreshingSession" }] : /* istanbul ignore next */ []));
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Dispatchers
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
bootstrap() {
|
|
424
|
+
return this.store.dispatch(new BootstrapDelegationSession());
|
|
425
|
+
}
|
|
426
|
+
setCandidates(candidates) {
|
|
427
|
+
return this.store.dispatch(new SetDelegationCandidates(candidates));
|
|
428
|
+
}
|
|
429
|
+
startSession(delegationId) {
|
|
430
|
+
return this.store.dispatch(new StartDelegationSession(delegationId));
|
|
431
|
+
}
|
|
432
|
+
endSession(opts = {}) {
|
|
433
|
+
return this.store.dispatch(new EndDelegationSession(opts));
|
|
434
|
+
}
|
|
435
|
+
switchSession(delegationId) {
|
|
436
|
+
return this.store.dispatch(new SwitchDelegationSession(delegationId));
|
|
437
|
+
}
|
|
438
|
+
refreshSession() {
|
|
439
|
+
return this.store.dispatch(new RefreshDelegationSession());
|
|
440
|
+
}
|
|
441
|
+
verifySession() {
|
|
442
|
+
return this.store.dispatch(new VerifyDelegationSession());
|
|
443
|
+
}
|
|
444
|
+
invalidate(reason = 'Unknown') {
|
|
445
|
+
return this.store.dispatch(new DelegationSessionInvalidated(reason));
|
|
446
|
+
}
|
|
447
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
448
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionFacade, providedIn: 'root' });
|
|
449
|
+
}
|
|
450
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationSessionFacade, decorators: [{
|
|
451
|
+
type: Injectable,
|
|
452
|
+
args: [{ providedIn: 'root' }]
|
|
453
|
+
}] });
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Confirmation dialog opened by the topbar menu before starting (or switching to)
|
|
457
|
+
* a delegated session. Calls the facade and closes itself on success.
|
|
458
|
+
*/
|
|
459
|
+
class StartSessionDialog {
|
|
460
|
+
candidate = input.required(...(ngDevMode ? [{ debugName: "candidate" }] : /* istanbul ignore next */ []));
|
|
461
|
+
intent = input('start', ...(ngDevMode ? [{ debugName: "intent" }] : /* istanbul ignore next */ []));
|
|
462
|
+
ref = inject(ModalRef);
|
|
463
|
+
facade = inject(DelegationSessionFacade);
|
|
464
|
+
isBusy = computed(() => this.facade.isStartingSession() || this.facade.isEndingSession(), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
465
|
+
title = computed(() => this.intent() === 'switch'
|
|
466
|
+
? 'delegations.switch-session-confirm-title'
|
|
467
|
+
: 'delegations.start-session-confirm-title', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
468
|
+
body = computed(() => this.intent() === 'switch'
|
|
469
|
+
? 'delegations.switch-session-confirm-body'
|
|
470
|
+
: 'delegations.start-session-confirm-body', ...(ngDevMode ? [{ debugName: "body" }] : /* istanbul ignore next */ []));
|
|
471
|
+
confirm() {
|
|
472
|
+
const id = this.candidate().id;
|
|
473
|
+
const op$ = this.intent() === 'switch'
|
|
474
|
+
? this.facade.switchSession(id)
|
|
475
|
+
: this.facade.startSession(id);
|
|
476
|
+
op$.subscribe({
|
|
477
|
+
next: () => this.ref.close(true),
|
|
478
|
+
error: () => {
|
|
479
|
+
/* facade publishes the error to the toast pipeline; just stay open */
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
cancel() {
|
|
484
|
+
this.ref.close(false);
|
|
485
|
+
}
|
|
486
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StartSessionDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
487
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: StartSessionDialog, isStandalone: true, selector: "mt-start-session-dialog", inputs: { candidate: { classPropertyName: "candidate", publicName: "candidate", isSignal: true, isRequired: true, transformFunction: null }, intent: { classPropertyName: "intent", publicName: "intent", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col gap-4 p-2\">\r\n <div class=\"flex items-start gap-3\">\r\n <mt-avatar\r\n [image]=\"candidate().delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-12! h-12! text-lg! bg-surface-200!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <div class=\"text-base font-medium text-gray-900 truncate\">\r\n {{ candidate().delegator.displayName }}\r\n </div>\r\n @if (candidate().delegator.email) {\r\n <div class=\"text-xs text-gray-500 truncate\">\r\n {{ candidate().delegator.email }}\r\n </div>\r\n }\r\n @if (candidate().scopeSummary) {\r\n <div class=\"text-xs text-gray-500 mt-1\">\r\n {{ t(\"delegations.scope\") }}: {{ candidate().scopeSummary }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <p class=\"text-sm text-gray-600 leading-relaxed\">\r\n {{ t(body()) }}\r\n </p>\r\n\r\n <div class=\"flex justify-end gap-2 pt-2 border-t border-gray-200\">\r\n <mt-button\r\n variant=\"outlined\"\r\n color=\"secondary\"\r\n [label]=\"t('cancel')\"\r\n [disabled]=\"isBusy()\"\r\n (click)=\"cancel()\"\r\n ></mt-button>\r\n <mt-button\r\n color=\"primary\"\r\n icon=\"user.users-check\"\r\n [label]=\"\r\n intent() === 'switch'\r\n ? t('delegations.switch-session')\r\n : t('delegations.start-session')\r\n \"\r\n [loading]=\"isBusy()\"\r\n (click)=\"confirm()\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { 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"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
488
|
+
}
|
|
489
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: StartSessionDialog, decorators: [{
|
|
490
|
+
type: Component,
|
|
491
|
+
args: [{ selector: 'mt-start-session-dialog', standalone: true, imports: [CommonModule, Avatar, Button, TranslocoDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col gap-4 p-2\">\r\n <div class=\"flex items-start gap-3\">\r\n <mt-avatar\r\n [image]=\"candidate().delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-12! h-12! text-lg! bg-surface-200!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <div class=\"text-base font-medium text-gray-900 truncate\">\r\n {{ candidate().delegator.displayName }}\r\n </div>\r\n @if (candidate().delegator.email) {\r\n <div class=\"text-xs text-gray-500 truncate\">\r\n {{ candidate().delegator.email }}\r\n </div>\r\n }\r\n @if (candidate().scopeSummary) {\r\n <div class=\"text-xs text-gray-500 mt-1\">\r\n {{ t(\"delegations.scope\") }}: {{ candidate().scopeSummary }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <p class=\"text-sm text-gray-600 leading-relaxed\">\r\n {{ t(body()) }}\r\n </p>\r\n\r\n <div class=\"flex justify-end gap-2 pt-2 border-t border-gray-200\">\r\n <mt-button\r\n variant=\"outlined\"\r\n color=\"secondary\"\r\n [label]=\"t('cancel')\"\r\n [disabled]=\"isBusy()\"\r\n (click)=\"cancel()\"\r\n ></mt-button>\r\n <mt-button\r\n color=\"primary\"\r\n icon=\"user.users-check\"\r\n [label]=\"\r\n intent() === 'switch'\r\n ? t('delegations.switch-session')\r\n : t('delegations.start-session')\r\n \"\r\n [loading]=\"isBusy()\"\r\n (click)=\"confirm()\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n</ng-container>\r\n" }]
|
|
492
|
+
}], propDecorators: { candidate: [{ type: i0.Input, args: [{ isSignal: true, alias: "candidate", required: true }] }], intent: [{ type: i0.Input, args: [{ isSignal: true, alias: "intent", required: false }] }] } });
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Topbar surface for the delegation runtime.
|
|
496
|
+
*
|
|
497
|
+
* State A — no candidates, no active session: renders nothing.
|
|
498
|
+
* State B — candidates exist, no active session: shows a "Switch identity" affordance.
|
|
499
|
+
* State C — active delegation session: shows the delegator name + back/switch.
|
|
500
|
+
*
|
|
501
|
+
* Designed to be projected into a host topbar's `[actions]` slot (alongside the
|
|
502
|
+
* existing user popover) — it does NOT replace the user dropdown.
|
|
503
|
+
*/
|
|
504
|
+
class TopbarDelegationMenu {
|
|
505
|
+
/** Path to the management page, e.g. `/control-panel/delegations` or `/delegations`. */
|
|
506
|
+
managePath = input('/delegations', ...(ngDevMode ? [{ debugName: "managePath" }] : /* istanbul ignore next */ []));
|
|
507
|
+
/** Compact mode hides the inline delegator name next to the button. */
|
|
508
|
+
compact = input(false, ...(ngDevMode ? [{ debugName: "compact" }] : /* istanbul ignore next */ []));
|
|
509
|
+
facade = inject(DelegationSessionFacade);
|
|
510
|
+
modal = inject(ModalService);
|
|
511
|
+
transloco = inject(TranslocoService);
|
|
512
|
+
popover = viewChild.required('popover');
|
|
513
|
+
active = this.facade.active;
|
|
514
|
+
candidates = this.facade.candidates;
|
|
515
|
+
hasCandidates = this.facade.hasCandidates;
|
|
516
|
+
isStarting = this.facade.isStartingSession;
|
|
517
|
+
isEnding = this.facade.isEndingSession;
|
|
518
|
+
delegatorName = computed(() => this.active()?.delegation.delegator.displayName ?? '', ...(ngDevMode ? [{ debugName: "delegatorName" }] : /* istanbul ignore next */ []));
|
|
519
|
+
/** State machine: which UI to render. */
|
|
520
|
+
mode = computed(() => {
|
|
521
|
+
if (this.active())
|
|
522
|
+
return 'active';
|
|
523
|
+
if (this.hasCandidates())
|
|
524
|
+
return 'candidates';
|
|
525
|
+
return 'hidden';
|
|
526
|
+
}, ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
527
|
+
togglePopover(event) {
|
|
528
|
+
this.popover().toggle(event);
|
|
529
|
+
}
|
|
530
|
+
startCandidate(candidate, event) {
|
|
531
|
+
event.stopPropagation();
|
|
532
|
+
this.popover().hide();
|
|
533
|
+
this.openConfirmation(candidate, 'start');
|
|
534
|
+
}
|
|
535
|
+
switchTo(candidate, event) {
|
|
536
|
+
event.stopPropagation();
|
|
537
|
+
this.popover().hide();
|
|
538
|
+
this.openConfirmation(candidate, 'switch');
|
|
539
|
+
}
|
|
540
|
+
endSession(event) {
|
|
541
|
+
event.stopPropagation();
|
|
542
|
+
this.popover().hide();
|
|
543
|
+
this.facade.endSession().subscribe();
|
|
544
|
+
}
|
|
545
|
+
openConfirmation(candidate, intent) {
|
|
546
|
+
this.modal.openModal(StartSessionDialog, 'dialog', {
|
|
547
|
+
header: this.transloco.translate(intent === 'switch'
|
|
548
|
+
? 'delegations.switch-session'
|
|
549
|
+
: 'delegations.start-session'),
|
|
550
|
+
styleClass: '!w-[min(96vw,28rem)] !max-w-[96vw]',
|
|
551
|
+
dismissableMask: true,
|
|
552
|
+
dismissible: true,
|
|
553
|
+
inputValues: {
|
|
554
|
+
candidate,
|
|
555
|
+
intent,
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TopbarDelegationMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
560
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: TopbarDelegationMenu, isStandalone: true, selector: "mt-topbar-delegation-menu", inputs: { managePath: { classPropertyName: "managePath", publicName: "managePath", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "popover", first: true, predicate: ["popover"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n @if (mode() !== \"hidden\") {\r\n <div class=\"mt-delegation-trigger flex items-center gap-2\">\r\n <button\r\n type=\"button\"\r\n class=\"mt-delegation-trigger__button flex items-center gap-2 px-2 py-1 rounded-full cursor-pointer hover:bg-surface-100\"\r\n [attr.aria-label]=\"t('delegations.delegation-menu')\"\r\n (click)=\"togglePopover($event)\"\r\n >\r\n @if (mode() === \"active\") {\r\n <mt-icon\r\n icon=\"user.users-check\"\r\n styleClass=\"text-lg text-primary\"\r\n ></mt-icon>\r\n @if (!compact()) {\r\n <span class=\"mt-delegation-trigger__label text-sm font-medium\">\r\n {{ t(\"delegations.acting-on-behalf-of\") }}: {{ delegatorName() }}\r\n </span>\r\n }\r\n <mt-delegation-status-chip\r\n status=\"Active\"\r\n ></mt-delegation-status-chip>\r\n } @else {\r\n <mt-icon\r\n icon=\"user.users-plus\"\r\n styleClass=\"text-lg text-primary\"\r\n ></mt-icon>\r\n @if (!compact()) {\r\n <span class=\"mt-delegation-trigger__label text-sm font-medium\">\r\n {{ t(\"delegations.delegations-available\") }}\r\n </span>\r\n }\r\n }\r\n </button>\r\n </div>\r\n\r\n <p-popover #popover [pt]=\"{ content: { class: 'p-0!' } }\">\r\n <div class=\"flex flex-col min-w-80 max-w-96 p-0\">\r\n @if (mode() === \"active\" && active(); as session) {\r\n <!-- ============================================================ -->\r\n <!-- STATE C \u2014 active session -->\r\n <!-- ============================================================ -->\r\n <div\r\n class=\"flex items-start gap-3 border-b border-gray-200 px-4 py-4\"\r\n >\r\n <mt-avatar\r\n [image]=\"session.delegation.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-11! h-11! text-xl! text-gray-600! bg-surface-300!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0 flex-1\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.acting-on-behalf-of\") }}\r\n </div>\r\n <div class=\"text-sm font-medium text-gray-900 truncate\">\r\n {{ session.delegation.delegator.displayName }}\r\n </div>\r\n @if (session.delegation.scopeSummary) {\r\n <div\r\n class=\"text-xs text-gray-500 mt-1 truncate\"\r\n [title]=\"session.delegation.scopeSummary\"\r\n >\r\n {{ t(\"delegations.scope\") }}:\r\n {{ session.delegation.scopeSummary }}\r\n </div>\r\n }\r\n <div class=\"text-xs text-gray-400 mt-1\">\r\n {{ t(\"delegations.session-ends-at\") }}:\r\n {{ session.accessTokenExpiresAtUtc | date: \"short\" }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-1 px-2 py-2\">\r\n <mt-button\r\n variant=\"text\"\r\n icon=\"arrow.arrow-left\"\r\n [label]=\"t('delegations.back-to-my-session')\"\r\n [loading]=\"isEnding()\"\r\n styleClass=\"w-full justify-start\"\r\n (click)=\"endSession($event)\"\r\n ></mt-button>\r\n </div>\r\n\r\n @if (candidates().length > 1) {\r\n <div class=\"border-t border-gray-200 pt-2 px-2 pb-2\">\r\n <div class=\"text-xs text-gray-500 px-2 pb-1\">\r\n {{ t(\"delegations.switch-to-another\") }}\r\n </div>\r\n @for (cand of candidates(); track cand.id) {\r\n @if (cand.id !== session.delegation.id) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between gap-2 w-full px-2 py-2 rounded-md hover:bg-surface-100 cursor-pointer\"\r\n (click)=\"switchTo(cand, $event)\"\r\n >\r\n <div class=\"flex items-center gap-2 min-w-0\">\r\n <mt-avatar\r\n [image]=\"cand.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-7! h-7! text-sm! bg-surface-200!\"\r\n ></mt-avatar>\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ cand.delegator.displayName }}\r\n </span>\r\n </div>\r\n <mt-icon\r\n icon=\"arrow.switch-horizontal-01\"\r\n styleClass=\"text-base text-gray-400\"\r\n ></mt-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n }\r\n } @else if (mode() === \"candidates\") {\r\n <!-- ============================================================ -->\r\n <!-- STATE B \u2014 candidates available, no active session -->\r\n <!-- ============================================================ -->\r\n <div class=\"border-b border-gray-200 px-4 py-3\">\r\n <div class=\"text-sm font-medium text-gray-900\">\r\n {{ t(\"delegations.you-can-act-as\") }}\r\n </div>\r\n <div class=\"text-xs text-gray-500 mt-1\">\r\n {{ t(\"delegations.choose-delegator-hint\") }}\r\n </div>\r\n </div>\r\n <div class=\"flex flex-col gap-1 px-2 py-2 max-h-72 overflow-y-auto\">\r\n @for (cand of candidates(); track cand.id) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between gap-2 w-full px-2 py-2 rounded-md hover:bg-surface-100 cursor-pointer text-start\"\r\n (click)=\"startCandidate(cand, $event)\"\r\n >\r\n <div class=\"flex items-center gap-2 min-w-0 flex-1\">\r\n <mt-avatar\r\n [image]=\"cand.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-9! h-9! text-base! bg-surface-200!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"text-sm font-medium text-gray-900 truncate\">\r\n {{ cand.delegator.displayName }}\r\n </span>\r\n @if (cand.scopeSummary) {\r\n <span\r\n class=\"text-xs text-gray-500 truncate\"\r\n [title]=\"cand.scopeSummary\"\r\n >\r\n {{ cand.scopeSummary }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n <mt-icon\r\n icon=\"arrow.arrow-right\"\r\n styleClass=\"text-base text-gray-400\"\r\n ></mt-icon>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Footer (always shown when popover renders) -->\r\n <div class=\"border-t border-gray-200 px-2 py-2\">\r\n <a\r\n [routerLink]=\"managePath()\"\r\n (click)=\"popover().hide()\"\r\n class=\"flex items-center gap-2 px-3 py-2 rounded-md text-gray-700 hover:bg-surface-100 text-sm cursor-pointer\"\r\n >\r\n <mt-icon\r\n icon=\"general.settings-02\"\r\n styleClass=\"text-base\"\r\n ></mt-icon>\r\n <span>{{ t(\"delegations.manage-delegations\") }}</span>\r\n </a>\r\n </div>\r\n </div>\r\n </p-popover>\r\n }\r\n</ng-container>\r\n", styles: [":host{display:inline-flex;align-items:center}.mt-delegation-trigger__button{transition:background-color .2s ease-out}.mt-delegation-trigger__label{max-width:16rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host-context(.dark) .mt-delegation-trigger__button:hover{background-color:#94a3b826}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { 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"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DelegationStatusChip, selector: "mt-delegation-status-chip", inputs: ["status"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
561
|
+
}
|
|
562
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: TopbarDelegationMenu, decorators: [{
|
|
563
|
+
type: Component,
|
|
564
|
+
args: [{ selector: 'mt-topbar-delegation-menu', standalone: true, imports: [
|
|
565
|
+
CommonModule,
|
|
566
|
+
Avatar,
|
|
567
|
+
Button,
|
|
568
|
+
Icon,
|
|
569
|
+
Popover,
|
|
570
|
+
RouterLink,
|
|
571
|
+
TranslocoDirective,
|
|
572
|
+
DelegationStatusChip,
|
|
573
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\r\n @if (mode() !== \"hidden\") {\r\n <div class=\"mt-delegation-trigger flex items-center gap-2\">\r\n <button\r\n type=\"button\"\r\n class=\"mt-delegation-trigger__button flex items-center gap-2 px-2 py-1 rounded-full cursor-pointer hover:bg-surface-100\"\r\n [attr.aria-label]=\"t('delegations.delegation-menu')\"\r\n (click)=\"togglePopover($event)\"\r\n >\r\n @if (mode() === \"active\") {\r\n <mt-icon\r\n icon=\"user.users-check\"\r\n styleClass=\"text-lg text-primary\"\r\n ></mt-icon>\r\n @if (!compact()) {\r\n <span class=\"mt-delegation-trigger__label text-sm font-medium\">\r\n {{ t(\"delegations.acting-on-behalf-of\") }}: {{ delegatorName() }}\r\n </span>\r\n }\r\n <mt-delegation-status-chip\r\n status=\"Active\"\r\n ></mt-delegation-status-chip>\r\n } @else {\r\n <mt-icon\r\n icon=\"user.users-plus\"\r\n styleClass=\"text-lg text-primary\"\r\n ></mt-icon>\r\n @if (!compact()) {\r\n <span class=\"mt-delegation-trigger__label text-sm font-medium\">\r\n {{ t(\"delegations.delegations-available\") }}\r\n </span>\r\n }\r\n }\r\n </button>\r\n </div>\r\n\r\n <p-popover #popover [pt]=\"{ content: { class: 'p-0!' } }\">\r\n <div class=\"flex flex-col min-w-80 max-w-96 p-0\">\r\n @if (mode() === \"active\" && active(); as session) {\r\n <!-- ============================================================ -->\r\n <!-- STATE C \u2014 active session -->\r\n <!-- ============================================================ -->\r\n <div\r\n class=\"flex items-start gap-3 border-b border-gray-200 px-4 py-4\"\r\n >\r\n <mt-avatar\r\n [image]=\"session.delegation.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-11! h-11! text-xl! text-gray-600! bg-surface-300!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0 flex-1\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.acting-on-behalf-of\") }}\r\n </div>\r\n <div class=\"text-sm font-medium text-gray-900 truncate\">\r\n {{ session.delegation.delegator.displayName }}\r\n </div>\r\n @if (session.delegation.scopeSummary) {\r\n <div\r\n class=\"text-xs text-gray-500 mt-1 truncate\"\r\n [title]=\"session.delegation.scopeSummary\"\r\n >\r\n {{ t(\"delegations.scope\") }}:\r\n {{ session.delegation.scopeSummary }}\r\n </div>\r\n }\r\n <div class=\"text-xs text-gray-400 mt-1\">\r\n {{ t(\"delegations.session-ends-at\") }}:\r\n {{ session.accessTokenExpiresAtUtc | date: \"short\" }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex flex-col gap-1 px-2 py-2\">\r\n <mt-button\r\n variant=\"text\"\r\n icon=\"arrow.arrow-left\"\r\n [label]=\"t('delegations.back-to-my-session')\"\r\n [loading]=\"isEnding()\"\r\n styleClass=\"w-full justify-start\"\r\n (click)=\"endSession($event)\"\r\n ></mt-button>\r\n </div>\r\n\r\n @if (candidates().length > 1) {\r\n <div class=\"border-t border-gray-200 pt-2 px-2 pb-2\">\r\n <div class=\"text-xs text-gray-500 px-2 pb-1\">\r\n {{ t(\"delegations.switch-to-another\") }}\r\n </div>\r\n @for (cand of candidates(); track cand.id) {\r\n @if (cand.id !== session.delegation.id) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between gap-2 w-full px-2 py-2 rounded-md hover:bg-surface-100 cursor-pointer\"\r\n (click)=\"switchTo(cand, $event)\"\r\n >\r\n <div class=\"flex items-center gap-2 min-w-0\">\r\n <mt-avatar\r\n [image]=\"cand.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-7! h-7! text-sm! bg-surface-200!\"\r\n ></mt-avatar>\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ cand.delegator.displayName }}\r\n </span>\r\n </div>\r\n <mt-icon\r\n icon=\"arrow.switch-horizontal-01\"\r\n styleClass=\"text-base text-gray-400\"\r\n ></mt-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n }\r\n } @else if (mode() === \"candidates\") {\r\n <!-- ============================================================ -->\r\n <!-- STATE B \u2014 candidates available, no active session -->\r\n <!-- ============================================================ -->\r\n <div class=\"border-b border-gray-200 px-4 py-3\">\r\n <div class=\"text-sm font-medium text-gray-900\">\r\n {{ t(\"delegations.you-can-act-as\") }}\r\n </div>\r\n <div class=\"text-xs text-gray-500 mt-1\">\r\n {{ t(\"delegations.choose-delegator-hint\") }}\r\n </div>\r\n </div>\r\n <div class=\"flex flex-col gap-1 px-2 py-2 max-h-72 overflow-y-auto\">\r\n @for (cand of candidates(); track cand.id) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between gap-2 w-full px-2 py-2 rounded-md hover:bg-surface-100 cursor-pointer text-start\"\r\n (click)=\"startCandidate(cand, $event)\"\r\n >\r\n <div class=\"flex items-center gap-2 min-w-0 flex-1\">\r\n <mt-avatar\r\n [image]=\"cand.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-9! h-9! text-base! bg-surface-200!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"text-sm font-medium text-gray-900 truncate\">\r\n {{ cand.delegator.displayName }}\r\n </span>\r\n @if (cand.scopeSummary) {\r\n <span\r\n class=\"text-xs text-gray-500 truncate\"\r\n [title]=\"cand.scopeSummary\"\r\n >\r\n {{ cand.scopeSummary }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n <mt-icon\r\n icon=\"arrow.arrow-right\"\r\n styleClass=\"text-base text-gray-400\"\r\n ></mt-icon>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Footer (always shown when popover renders) -->\r\n <div class=\"border-t border-gray-200 px-2 py-2\">\r\n <a\r\n [routerLink]=\"managePath()\"\r\n (click)=\"popover().hide()\"\r\n class=\"flex items-center gap-2 px-3 py-2 rounded-md text-gray-700 hover:bg-surface-100 text-sm cursor-pointer\"\r\n >\r\n <mt-icon\r\n icon=\"general.settings-02\"\r\n styleClass=\"text-base\"\r\n ></mt-icon>\r\n <span>{{ t(\"delegations.manage-delegations\") }}</span>\r\n </a>\r\n </div>\r\n </div>\r\n </p-popover>\r\n }\r\n</ng-container>\r\n", styles: [":host{display:inline-flex;align-items:center}.mt-delegation-trigger__button{transition:background-color .2s ease-out}.mt-delegation-trigger__label{max-width:16rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host-context(.dark) .mt-delegation-trigger__button:hover{background-color:#94a3b826}\n"] }]
|
|
574
|
+
}], propDecorators: { managePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "managePath", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], popover: [{ type: i0.ViewChild, args: ['popover', { isSignal: true }] }] } });
|
|
24
575
|
|
|
25
576
|
class Delegations {
|
|
26
577
|
router = inject(Router);
|
|
@@ -28,79 +579,199 @@ class Delegations {
|
|
|
28
579
|
this.router.navigate(['control-panel/product-settings']);
|
|
29
580
|
}
|
|
30
581
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: Delegations, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
31
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: Delegations, isStandalone: true, selector: "mt-delegations", ngImport: i0, template: "<ng-container *transloco=\"let t\">\
|
|
582
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.8", type: Delegations, isStandalone: true, selector: "mt-delegations", ngImport: i0, template: "<ng-container *transloco=\"let t\">\n <mt-page\n [title]=\"t('delegations.delegations')\"\n [avatarIcon]=\"'custom.hierarchy-structure'\"\n [contentClass]=\"'max-[1025px]:p-4 max-[640px]:p-3'\"\n [avatarStyle]=\"{\n '--p-avatar-background': 'var(--p-indigo-50)',\n '--p-avatar-color': 'var(--p-indigo-700)',\n }\"\n (backButtonClick)=\"goBack()\"\n backButton\n >\n <router-outlet />\n </mt-page>\n</ng-container>\n", styles: [""], 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: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }] });
|
|
32
583
|
}
|
|
33
584
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: Delegations, decorators: [{
|
|
34
585
|
type: Component,
|
|
35
|
-
args: [{ selector: 'mt-delegations', imports: [Page, RouterOutlet, TranslocoDirective], template: "<ng-container *transloco=\"let t\">\
|
|
586
|
+
args: [{ selector: 'mt-delegations', imports: [Page, RouterOutlet, TranslocoDirective], template: "<ng-container *transloco=\"let t\">\n <mt-page\n [title]=\"t('delegations.delegations')\"\n [avatarIcon]=\"'custom.hierarchy-structure'\"\n [contentClass]=\"'max-[1025px]:p-4 max-[640px]:p-3'\"\n [avatarStyle]=\"{\n '--p-avatar-background': 'var(--p-indigo-50)',\n '--p-avatar-color': 'var(--p-indigo-700)',\n }\"\n (backButtonClick)=\"goBack()\"\n backButton\n >\n <router-outlet />\n </mt-page>\n</ng-container>\n" }]
|
|
36
587
|
}] });
|
|
37
588
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// List / detail
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
class GetMyDelegations {
|
|
593
|
+
query;
|
|
594
|
+
static type = '[Delegations] Get My Delegations';
|
|
595
|
+
constructor(query = {}) {
|
|
596
|
+
this.query = query;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
class GetAssignedDelegations {
|
|
600
|
+
query;
|
|
601
|
+
static type = '[Delegations] Get Assigned Delegations';
|
|
602
|
+
constructor(query = {}) {
|
|
603
|
+
this.query = query;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
class GetDelegation {
|
|
607
|
+
id;
|
|
608
|
+
includeStatusHistory;
|
|
609
|
+
static type = '[Delegations] Get Delegation';
|
|
610
|
+
constructor(id, includeStatusHistory = false) {
|
|
611
|
+
this.id = id;
|
|
612
|
+
this.includeStatusHistory = includeStatusHistory;
|
|
613
|
+
}
|
|
49
614
|
}
|
|
50
|
-
class
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
615
|
+
class ClearSelectedDelegation {
|
|
616
|
+
static type = '[Delegations] Clear Selected Delegation';
|
|
617
|
+
}
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Lifecycle
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
class CreateDelegation {
|
|
622
|
+
request;
|
|
623
|
+
static type = '[Delegations] Create Delegation';
|
|
624
|
+
constructor(request) {
|
|
625
|
+
this.request = request;
|
|
55
626
|
}
|
|
56
627
|
}
|
|
57
628
|
class UpdateDelegation {
|
|
58
629
|
id;
|
|
59
|
-
|
|
630
|
+
request;
|
|
60
631
|
static type = '[Delegations] Update Delegation';
|
|
61
|
-
constructor(id,
|
|
632
|
+
constructor(id, request) {
|
|
62
633
|
this.id = id;
|
|
63
|
-
this.
|
|
634
|
+
this.request = request;
|
|
64
635
|
}
|
|
65
636
|
}
|
|
66
|
-
class
|
|
637
|
+
class ApproveDelegation {
|
|
67
638
|
id;
|
|
68
|
-
|
|
69
|
-
|
|
639
|
+
request;
|
|
640
|
+
static type = '[Delegations] Approve Delegation';
|
|
641
|
+
constructor(id, request) {
|
|
70
642
|
this.id = id;
|
|
643
|
+
this.request = request;
|
|
71
644
|
}
|
|
72
645
|
}
|
|
73
|
-
class
|
|
646
|
+
class RejectDelegation {
|
|
74
647
|
id;
|
|
75
|
-
|
|
76
|
-
|
|
648
|
+
request;
|
|
649
|
+
static type = '[Delegations] Reject Delegation';
|
|
650
|
+
constructor(id, request) {
|
|
77
651
|
this.id = id;
|
|
652
|
+
this.request = request;
|
|
78
653
|
}
|
|
79
654
|
}
|
|
80
|
-
class
|
|
81
|
-
|
|
655
|
+
class CancelDelegation {
|
|
656
|
+
id;
|
|
657
|
+
request;
|
|
658
|
+
static type = '[Delegations] Cancel Delegation';
|
|
659
|
+
constructor(id, request) {
|
|
660
|
+
this.id = id;
|
|
661
|
+
this.request = request;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// Scope picker (form support)
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
class GetScopeOptions {
|
|
668
|
+
delegatorUserId;
|
|
669
|
+
static type = '[Delegations] Get Scope Options';
|
|
670
|
+
/** Admin-only override; ignored for normal users. */
|
|
671
|
+
constructor(delegatorUserId) {
|
|
672
|
+
this.delegatorUserId = delegatorUserId;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
class PreviewScope {
|
|
676
|
+
scope;
|
|
677
|
+
static type = '[Delegations] Preview Scope';
|
|
678
|
+
constructor(scope) {
|
|
679
|
+
this.scope = scope;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
class ClearScopePreview {
|
|
683
|
+
static type = '[Delegations] Clear Scope Preview';
|
|
82
684
|
}
|
|
83
685
|
|
|
686
|
+
// ============================================================================
|
|
687
|
+
// NGXS state shape + action keys
|
|
688
|
+
// ============================================================================
|
|
689
|
+
var DelegationsActionKey;
|
|
690
|
+
(function (DelegationsActionKey) {
|
|
691
|
+
DelegationsActionKey["GetMyDelegations"] = "getMyDelegations";
|
|
692
|
+
DelegationsActionKey["GetAssignedDelegations"] = "getAssignedDelegations";
|
|
693
|
+
DelegationsActionKey["GetDelegation"] = "getDelegation";
|
|
694
|
+
DelegationsActionKey["CreateDelegation"] = "createDelegation";
|
|
695
|
+
DelegationsActionKey["UpdateDelegation"] = "updateDelegation";
|
|
696
|
+
DelegationsActionKey["ApproveDelegation"] = "approveDelegation";
|
|
697
|
+
DelegationsActionKey["RejectDelegation"] = "rejectDelegation";
|
|
698
|
+
DelegationsActionKey["CancelDelegation"] = "cancelDelegation";
|
|
699
|
+
DelegationsActionKey["GetScopeOptions"] = "getScopeOptions";
|
|
700
|
+
DelegationsActionKey["PreviewScope"] = "previewScope";
|
|
701
|
+
})(DelegationsActionKey || (DelegationsActionKey = {}));
|
|
702
|
+
|
|
84
703
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
85
704
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
86
705
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
87
706
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
88
707
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
89
708
|
};
|
|
90
|
-
|
|
709
|
+
const BASE = 'identity/delegations';
|
|
710
|
+
function buildListParams(query) {
|
|
711
|
+
let params = new HttpParams();
|
|
712
|
+
if (query.page != null)
|
|
713
|
+
params = params.set('page', query.page);
|
|
714
|
+
if (query.pageSize != null)
|
|
715
|
+
params = params.set('pageSize', query.pageSize);
|
|
716
|
+
if (query.q)
|
|
717
|
+
params = params.set('q', query.q);
|
|
718
|
+
if (query.startFrom)
|
|
719
|
+
params = params.set('startFrom', query.startFrom);
|
|
720
|
+
if (query.startTo)
|
|
721
|
+
params = params.set('startTo', query.startTo);
|
|
722
|
+
if (query.endFrom)
|
|
723
|
+
params = params.set('endFrom', query.endFrom);
|
|
724
|
+
if (query.endTo)
|
|
725
|
+
params = params.set('endTo', query.endTo);
|
|
726
|
+
if (query.activeOn)
|
|
727
|
+
params = params.set('activeOn', query.activeOn);
|
|
728
|
+
if (query.approvalStatus)
|
|
729
|
+
params = params.set('approvalStatus', query.approvalStatus);
|
|
730
|
+
if (query.moduleId != null)
|
|
731
|
+
params = params.set('moduleId', query.moduleId);
|
|
732
|
+
if (query.accessibilityGroupId != null)
|
|
733
|
+
params = params.set('accessibilityGroupId', query.accessibilityGroupId);
|
|
734
|
+
if (query.sortBy)
|
|
735
|
+
params = params.set('sortBy', query.sortBy);
|
|
736
|
+
if (query.sortDir)
|
|
737
|
+
params = params.set('sortDir', query.sortDir);
|
|
738
|
+
if (query.status?.length) {
|
|
739
|
+
for (const s of query.status) {
|
|
740
|
+
params = params.append('status', s);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return params;
|
|
744
|
+
}
|
|
745
|
+
function replaceInPage(page, updated) {
|
|
746
|
+
if (!page)
|
|
747
|
+
return page;
|
|
748
|
+
const idx = page.items.findIndex((it) => it.id === updated.id);
|
|
749
|
+
if (idx === -1)
|
|
750
|
+
return page;
|
|
751
|
+
const items = page.items.slice();
|
|
752
|
+
items[idx] = updated;
|
|
753
|
+
return { ...page, items };
|
|
754
|
+
}
|
|
755
|
+
let DelegationsState = class DelegationsState {
|
|
91
756
|
http = inject(HttpClient);
|
|
92
757
|
// ============================================================================
|
|
93
|
-
// Selectors
|
|
758
|
+
// Selectors
|
|
94
759
|
// ============================================================================
|
|
95
|
-
static
|
|
96
|
-
return state.
|
|
760
|
+
static getMyPage(state) {
|
|
761
|
+
return state.my;
|
|
97
762
|
}
|
|
98
|
-
static
|
|
99
|
-
return state.
|
|
763
|
+
static getAssignedPage(state) {
|
|
764
|
+
return state.assigned;
|
|
765
|
+
}
|
|
766
|
+
static getSelected(state) {
|
|
767
|
+
return state.selected;
|
|
768
|
+
}
|
|
769
|
+
static getScopeOptions(state) {
|
|
770
|
+
return state.scopeOptions;
|
|
771
|
+
}
|
|
772
|
+
static getScopePreview(state) {
|
|
773
|
+
return state.scopePreview;
|
|
100
774
|
}
|
|
101
|
-
// ============================================================================
|
|
102
|
-
// Loading/Error Slice Selectors - REQUIRED for optimal performance
|
|
103
|
-
// ============================================================================
|
|
104
775
|
static getLoadingActive(state) {
|
|
105
776
|
return state.loadingActive;
|
|
106
777
|
}
|
|
@@ -108,88 +779,189 @@ let DelegationsState = class DelegationsState extends CrudStateBase {
|
|
|
108
779
|
return state.errors;
|
|
109
780
|
}
|
|
110
781
|
// ============================================================================
|
|
111
|
-
//
|
|
782
|
+
// List
|
|
112
783
|
// ============================================================================
|
|
113
|
-
|
|
114
|
-
const req$ = this.http.get(
|
|
784
|
+
getMyDelegations(ctx, { query }) {
|
|
785
|
+
const req$ = this.http.get(`${BASE}/my`, {
|
|
786
|
+
params: buildListParams(query),
|
|
787
|
+
});
|
|
115
788
|
return handleApiRequest({
|
|
116
789
|
ctx,
|
|
117
|
-
key: DelegationsActionKey.
|
|
790
|
+
key: DelegationsActionKey.GetMyDelegations,
|
|
118
791
|
request$: req$,
|
|
119
|
-
onSuccess: (response) => ({
|
|
120
|
-
allDelegations: response.data ?? [],
|
|
121
|
-
}),
|
|
792
|
+
onSuccess: (response) => ({ my: response.data }),
|
|
122
793
|
});
|
|
123
794
|
}
|
|
124
|
-
|
|
125
|
-
const req$ = this.http.get(
|
|
795
|
+
getAssignedDelegations(ctx, { query }) {
|
|
796
|
+
const req$ = this.http.get(`${BASE}/assigned`, { params: buildListParams(query) });
|
|
126
797
|
return handleApiRequest({
|
|
127
798
|
ctx,
|
|
128
|
-
key: DelegationsActionKey.
|
|
799
|
+
key: DelegationsActionKey.GetAssignedDelegations,
|
|
129
800
|
request$: req$,
|
|
130
|
-
onSuccess: (response) => ({
|
|
131
|
-
selectedDelegation: response.data ?? null,
|
|
132
|
-
}),
|
|
801
|
+
onSuccess: (response) => ({ assigned: response.data }),
|
|
133
802
|
});
|
|
134
803
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
804
|
+
getDelegation(ctx, { id, includeStatusHistory }) {
|
|
805
|
+
let params = new HttpParams();
|
|
806
|
+
if (includeStatusHistory) {
|
|
807
|
+
params = params.set('include', 'statusHistory');
|
|
808
|
+
}
|
|
809
|
+
const req$ = this.http.get(`${BASE}/${id}`, {
|
|
810
|
+
params,
|
|
811
|
+
});
|
|
812
|
+
return handleApiRequest({
|
|
813
|
+
ctx,
|
|
814
|
+
key: DelegationsActionKey.GetDelegation,
|
|
815
|
+
request$: req$,
|
|
816
|
+
onSuccess: (response) => ({ selected: response.data ?? null }),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
clearSelectedDelegation(ctx) {
|
|
820
|
+
ctx.patchState({ selected: null });
|
|
821
|
+
}
|
|
822
|
+
// ============================================================================
|
|
823
|
+
// Lifecycle
|
|
824
|
+
// ============================================================================
|
|
825
|
+
createDelegation(ctx, { request }) {
|
|
826
|
+
const req$ = this.http.post(BASE, request);
|
|
827
|
+
return handleApiRequest({
|
|
828
|
+
ctx,
|
|
829
|
+
key: DelegationsActionKey.CreateDelegation,
|
|
139
830
|
request$: req$,
|
|
140
|
-
|
|
831
|
+
onSuccess: (response) => {
|
|
832
|
+
const created = response.data;
|
|
833
|
+
const state = ctx.getState();
|
|
834
|
+
return {
|
|
835
|
+
my: state.my
|
|
836
|
+
? { ...state.my, items: [created, ...state.my.items] }
|
|
837
|
+
: state.my,
|
|
838
|
+
};
|
|
839
|
+
},
|
|
141
840
|
});
|
|
142
841
|
}
|
|
143
|
-
updateDelegation(ctx, { id,
|
|
144
|
-
const req$ = this.http.put(
|
|
145
|
-
return this.
|
|
146
|
-
|
|
842
|
+
updateDelegation(ctx, { id, request }) {
|
|
843
|
+
const req$ = this.http.put(`${BASE}/${id}`, request);
|
|
844
|
+
return this.applyLifecycleResult(ctx, DelegationsActionKey.UpdateDelegation, req$);
|
|
845
|
+
}
|
|
846
|
+
approveDelegation(ctx, { id, request }) {
|
|
847
|
+
const req$ = this.http.post(`${BASE}/${id}/approve`, request);
|
|
848
|
+
return this.applyLifecycleResult(ctx, DelegationsActionKey.ApproveDelegation, req$);
|
|
849
|
+
}
|
|
850
|
+
rejectDelegation(ctx, { id, request }) {
|
|
851
|
+
const req$ = this.http.post(`${BASE}/${id}/reject`, request);
|
|
852
|
+
return this.applyLifecycleResult(ctx, DelegationsActionKey.RejectDelegation, req$);
|
|
853
|
+
}
|
|
854
|
+
cancelDelegation(ctx, { id, request }) {
|
|
855
|
+
const req$ = this.http.post(`${BASE}/${id}/cancel`, request);
|
|
856
|
+
return this.applyLifecycleResult(ctx, DelegationsActionKey.CancelDelegation, req$);
|
|
857
|
+
}
|
|
858
|
+
// ============================================================================
|
|
859
|
+
// Scope (options + preview)
|
|
860
|
+
// ============================================================================
|
|
861
|
+
getScopeOptions(ctx, { delegatorUserId }) {
|
|
862
|
+
let params = new HttpParams();
|
|
863
|
+
if (delegatorUserId)
|
|
864
|
+
params = params.set('delegatorUserId', delegatorUserId);
|
|
865
|
+
const url = delegatorUserId
|
|
866
|
+
? `identity/admin/delegations/scope/options`
|
|
867
|
+
: `${BASE}/scope/options`;
|
|
868
|
+
const req$ = this.http.get(url, {
|
|
869
|
+
params,
|
|
870
|
+
});
|
|
871
|
+
return handleApiRequest({
|
|
872
|
+
ctx,
|
|
873
|
+
key: DelegationsActionKey.GetScopeOptions,
|
|
147
874
|
request$: req$,
|
|
148
|
-
|
|
149
|
-
id,
|
|
150
|
-
stateProperty: (state) => state.allDelegations,
|
|
875
|
+
onSuccess: (response) => ({ scopeOptions: response.data ?? null }),
|
|
151
876
|
});
|
|
152
877
|
}
|
|
153
|
-
|
|
154
|
-
const req$ = this.http.
|
|
155
|
-
return
|
|
156
|
-
|
|
878
|
+
previewScope(ctx, { scope }) {
|
|
879
|
+
const req$ = this.http.post(`${BASE}/scope/preview`, scope);
|
|
880
|
+
return handleApiRequest({
|
|
881
|
+
ctx,
|
|
882
|
+
key: DelegationsActionKey.PreviewScope,
|
|
157
883
|
request$: req$,
|
|
158
|
-
|
|
159
|
-
id,
|
|
160
|
-
stateProperty: (state) => state.allDelegations,
|
|
884
|
+
onSuccess: (response) => ({ scopePreview: response.data ?? null }),
|
|
161
885
|
});
|
|
162
886
|
}
|
|
163
|
-
|
|
164
|
-
ctx.patchState({
|
|
887
|
+
clearScopePreview(ctx) {
|
|
888
|
+
ctx.patchState({ scopePreview: null });
|
|
889
|
+
}
|
|
890
|
+
// ============================================================================
|
|
891
|
+
// Helpers
|
|
892
|
+
// ============================================================================
|
|
893
|
+
applyLifecycleResult(ctx, key, req$) {
|
|
894
|
+
return handleApiRequest({
|
|
895
|
+
ctx,
|
|
896
|
+
key,
|
|
897
|
+
request$: req$,
|
|
898
|
+
onSuccess: (response) => {
|
|
899
|
+
const updated = response.data;
|
|
900
|
+
const state = ctx.getState();
|
|
901
|
+
return {
|
|
902
|
+
my: replaceInPage(state.my, updated),
|
|
903
|
+
assigned: replaceInPage(state.assigned, updated),
|
|
904
|
+
selected: state.selected && state.selected.id === updated.id
|
|
905
|
+
? { ...state.selected, ...updated }
|
|
906
|
+
: state.selected,
|
|
907
|
+
};
|
|
908
|
+
},
|
|
909
|
+
});
|
|
165
910
|
}
|
|
166
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsState, deps:
|
|
911
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
167
912
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsState });
|
|
168
913
|
};
|
|
169
914
|
__decorate([
|
|
170
|
-
Action(
|
|
171
|
-
], DelegationsState.prototype, "
|
|
915
|
+
Action(GetMyDelegations)
|
|
916
|
+
], DelegationsState.prototype, "getMyDelegations", null);
|
|
917
|
+
__decorate([
|
|
918
|
+
Action(GetAssignedDelegations)
|
|
919
|
+
], DelegationsState.prototype, "getAssignedDelegations", null);
|
|
172
920
|
__decorate([
|
|
173
921
|
Action(GetDelegation)
|
|
174
922
|
], DelegationsState.prototype, "getDelegation", null);
|
|
175
923
|
__decorate([
|
|
176
|
-
Action(
|
|
177
|
-
], DelegationsState.prototype, "
|
|
924
|
+
Action(ClearSelectedDelegation)
|
|
925
|
+
], DelegationsState.prototype, "clearSelectedDelegation", null);
|
|
926
|
+
__decorate([
|
|
927
|
+
Action(CreateDelegation)
|
|
928
|
+
], DelegationsState.prototype, "createDelegation", null);
|
|
178
929
|
__decorate([
|
|
179
930
|
Action(UpdateDelegation)
|
|
180
931
|
], DelegationsState.prototype, "updateDelegation", null);
|
|
181
932
|
__decorate([
|
|
182
|
-
Action(
|
|
183
|
-
], DelegationsState.prototype, "
|
|
933
|
+
Action(ApproveDelegation)
|
|
934
|
+
], DelegationsState.prototype, "approveDelegation", null);
|
|
184
935
|
__decorate([
|
|
185
|
-
Action(
|
|
186
|
-
], DelegationsState.prototype, "
|
|
936
|
+
Action(RejectDelegation)
|
|
937
|
+
], DelegationsState.prototype, "rejectDelegation", null);
|
|
938
|
+
__decorate([
|
|
939
|
+
Action(CancelDelegation)
|
|
940
|
+
], DelegationsState.prototype, "cancelDelegation", null);
|
|
941
|
+
__decorate([
|
|
942
|
+
Action(GetScopeOptions)
|
|
943
|
+
], DelegationsState.prototype, "getScopeOptions", null);
|
|
944
|
+
__decorate([
|
|
945
|
+
Action(PreviewScope)
|
|
946
|
+
], DelegationsState.prototype, "previewScope", null);
|
|
947
|
+
__decorate([
|
|
948
|
+
Action(ClearScopePreview)
|
|
949
|
+
], DelegationsState.prototype, "clearScopePreview", null);
|
|
187
950
|
__decorate([
|
|
188
951
|
Selector()
|
|
189
|
-
], DelegationsState, "
|
|
952
|
+
], DelegationsState, "getMyPage", null);
|
|
190
953
|
__decorate([
|
|
191
954
|
Selector()
|
|
192
|
-
], DelegationsState, "
|
|
955
|
+
], DelegationsState, "getAssignedPage", null);
|
|
956
|
+
__decorate([
|
|
957
|
+
Selector()
|
|
958
|
+
], DelegationsState, "getSelected", null);
|
|
959
|
+
__decorate([
|
|
960
|
+
Selector()
|
|
961
|
+
], DelegationsState, "getScopeOptions", null);
|
|
962
|
+
__decorate([
|
|
963
|
+
Selector()
|
|
964
|
+
], DelegationsState, "getScopePreview", null);
|
|
193
965
|
__decorate([
|
|
194
966
|
Selector()
|
|
195
967
|
], DelegationsState, "getLoadingActive", null);
|
|
@@ -200,8 +972,11 @@ DelegationsState = __decorate([
|
|
|
200
972
|
State({
|
|
201
973
|
name: 'delegations',
|
|
202
974
|
defaults: {
|
|
203
|
-
|
|
204
|
-
|
|
975
|
+
my: null,
|
|
976
|
+
assigned: null,
|
|
977
|
+
selected: null,
|
|
978
|
+
scopeOptions: null,
|
|
979
|
+
scopePreview: null,
|
|
205
980
|
loadingActive: [],
|
|
206
981
|
errors: {},
|
|
207
982
|
},
|
|
@@ -209,61 +984,87 @@ DelegationsState = __decorate([
|
|
|
209
984
|
], DelegationsState);
|
|
210
985
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsState, decorators: [{
|
|
211
986
|
type: Injectable
|
|
212
|
-
}], propDecorators: {
|
|
987
|
+
}], propDecorators: { getMyDelegations: [], getAssignedDelegations: [], getDelegation: [], clearSelectedDelegation: [], createDelegation: [], updateDelegation: [], approveDelegation: [], rejectDelegation: [], cancelDelegation: [], getScopeOptions: [], previewScope: [], clearScopePreview: [] } });
|
|
213
988
|
|
|
214
989
|
class DelegationsFacade {
|
|
215
990
|
store = inject(Store);
|
|
216
|
-
//
|
|
217
|
-
// Data
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
991
|
+
// ---------------------------------------------------------------------------
|
|
992
|
+
// Data slices
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
myPage = select(DelegationsState.getMyPage);
|
|
995
|
+
assignedPage = select(DelegationsState.getAssignedPage);
|
|
996
|
+
selected = select(DelegationsState.getSelected);
|
|
997
|
+
scopeOptions = select(DelegationsState.getScopeOptions);
|
|
998
|
+
scopePreview = select(DelegationsState.getScopePreview);
|
|
224
999
|
loadingActive = select(DelegationsState.getLoadingActive);
|
|
225
1000
|
errors = select(DelegationsState.getErrors);
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1001
|
+
// ---------------------------------------------------------------------------
|
|
1002
|
+
// Derived
|
|
1003
|
+
// ---------------------------------------------------------------------------
|
|
1004
|
+
myItems = computed(() => this.myPage()?.items ?? [], ...(ngDevMode ? [{ debugName: "myItems" }] : /* istanbul ignore next */ []));
|
|
1005
|
+
assignedItems = computed(() => this.assignedPage()?.items ?? [], ...(ngDevMode ? [{ debugName: "assignedItems" }] : /* istanbul ignore next */ []));
|
|
1006
|
+
// ---------------------------------------------------------------------------
|
|
1007
|
+
// Loading
|
|
1008
|
+
// ---------------------------------------------------------------------------
|
|
1009
|
+
isLoadingMy = computed(() => this.loadingActive().includes(DelegationsActionKey.GetMyDelegations), ...(ngDevMode ? [{ debugName: "isLoadingMy" }] : /* istanbul ignore next */ []));
|
|
1010
|
+
isLoadingAssigned = computed(() => this.loadingActive().includes(DelegationsActionKey.GetAssignedDelegations), ...(ngDevMode ? [{ debugName: "isLoadingAssigned" }] : /* istanbul ignore next */ []));
|
|
1011
|
+
isLoadingDetail = computed(() => this.loadingActive().includes(DelegationsActionKey.GetDelegation), ...(ngDevMode ? [{ debugName: "isLoadingDetail" }] : /* istanbul ignore next */ []));
|
|
1012
|
+
isCreating = computed(() => this.loadingActive().includes(DelegationsActionKey.CreateDelegation), ...(ngDevMode ? [{ debugName: "isCreating" }] : /* istanbul ignore next */ []));
|
|
1013
|
+
isUpdating = computed(() => this.loadingActive().includes(DelegationsActionKey.UpdateDelegation), ...(ngDevMode ? [{ debugName: "isUpdating" }] : /* istanbul ignore next */ []));
|
|
1014
|
+
isApproving = computed(() => this.loadingActive().includes(DelegationsActionKey.ApproveDelegation), ...(ngDevMode ? [{ debugName: "isApproving" }] : /* istanbul ignore next */ []));
|
|
1015
|
+
isRejecting = computed(() => this.loadingActive().includes(DelegationsActionKey.RejectDelegation), ...(ngDevMode ? [{ debugName: "isRejecting" }] : /* istanbul ignore next */ []));
|
|
1016
|
+
isCancelling = computed(() => this.loadingActive().includes(DelegationsActionKey.CancelDelegation), ...(ngDevMode ? [{ debugName: "isCancelling" }] : /* istanbul ignore next */ []));
|
|
1017
|
+
isLoadingScopeOptions = computed(() => this.loadingActive().includes(DelegationsActionKey.GetScopeOptions), ...(ngDevMode ? [{ debugName: "isLoadingScopeOptions" }] : /* istanbul ignore next */ []));
|
|
1018
|
+
isPreviewingScope = computed(() => this.loadingActive().includes(DelegationsActionKey.PreviewScope), ...(ngDevMode ? [{ debugName: "isPreviewingScope" }] : /* istanbul ignore next */ []));
|
|
1019
|
+
// ---------------------------------------------------------------------------
|
|
1020
|
+
// Errors
|
|
1021
|
+
// ---------------------------------------------------------------------------
|
|
1022
|
+
errorMy = computed(() => this.errors()[DelegationsActionKey.GetMyDelegations] ?? null, ...(ngDevMode ? [{ debugName: "errorMy" }] : /* istanbul ignore next */ []));
|
|
1023
|
+
errorAssigned = computed(() => this.errors()[DelegationsActionKey.GetAssignedDelegations] ?? null, ...(ngDevMode ? [{ debugName: "errorAssigned" }] : /* istanbul ignore next */ []));
|
|
1024
|
+
errorDetail = computed(() => this.errors()[DelegationsActionKey.GetDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorDetail" }] : /* istanbul ignore next */ []));
|
|
1025
|
+
errorCreate = computed(() => this.errors()[DelegationsActionKey.CreateDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorCreate" }] : /* istanbul ignore next */ []));
|
|
1026
|
+
errorUpdate = computed(() => this.errors()[DelegationsActionKey.UpdateDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorUpdate" }] : /* istanbul ignore next */ []));
|
|
1027
|
+
errorApprove = computed(() => this.errors()[DelegationsActionKey.ApproveDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorApprove" }] : /* istanbul ignore next */ []));
|
|
1028
|
+
errorReject = computed(() => this.errors()[DelegationsActionKey.RejectDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorReject" }] : /* istanbul ignore next */ []));
|
|
1029
|
+
errorCancel = computed(() => this.errors()[DelegationsActionKey.CancelDelegation] ?? null, ...(ngDevMode ? [{ debugName: "errorCancel" }] : /* istanbul ignore next */ []));
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
// Dispatchers
|
|
1032
|
+
// ---------------------------------------------------------------------------
|
|
1033
|
+
getMy(query = {}) {
|
|
1034
|
+
return this.store.dispatch(new GetMyDelegations(query));
|
|
252
1035
|
}
|
|
253
|
-
|
|
254
|
-
return this.store.dispatch(new
|
|
1036
|
+
getAssigned(query = {}) {
|
|
1037
|
+
return this.store.dispatch(new GetAssignedDelegations(query));
|
|
255
1038
|
}
|
|
256
|
-
|
|
257
|
-
return this.store.dispatch(new
|
|
1039
|
+
getOne(id, includeStatusHistory = false) {
|
|
1040
|
+
return this.store.dispatch(new GetDelegation(id, includeStatusHistory));
|
|
258
1041
|
}
|
|
259
|
-
|
|
260
|
-
return this.store.dispatch(new
|
|
1042
|
+
clearSelected() {
|
|
1043
|
+
return this.store.dispatch(new ClearSelectedDelegation());
|
|
261
1044
|
}
|
|
262
|
-
|
|
263
|
-
return this.store.dispatch(new
|
|
1045
|
+
create(request) {
|
|
1046
|
+
return this.store.dispatch(new CreateDelegation(request));
|
|
264
1047
|
}
|
|
265
|
-
|
|
266
|
-
return this.store.dispatch(new
|
|
1048
|
+
update(id, request) {
|
|
1049
|
+
return this.store.dispatch(new UpdateDelegation(id, request));
|
|
1050
|
+
}
|
|
1051
|
+
approve(id, request) {
|
|
1052
|
+
return this.store.dispatch(new ApproveDelegation(id, request));
|
|
1053
|
+
}
|
|
1054
|
+
reject(id, request) {
|
|
1055
|
+
return this.store.dispatch(new RejectDelegation(id, request));
|
|
1056
|
+
}
|
|
1057
|
+
cancel(id, request) {
|
|
1058
|
+
return this.store.dispatch(new CancelDelegation(id, request));
|
|
1059
|
+
}
|
|
1060
|
+
loadScopeOptions(delegatorUserId) {
|
|
1061
|
+
return this.store.dispatch(new GetScopeOptions(delegatorUserId));
|
|
1062
|
+
}
|
|
1063
|
+
previewScope(scope) {
|
|
1064
|
+
return this.store.dispatch(new PreviewScope(scope));
|
|
1065
|
+
}
|
|
1066
|
+
clearScopePreview() {
|
|
1067
|
+
return this.store.dispatch(new ClearScopePreview());
|
|
267
1068
|
}
|
|
268
1069
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
269
1070
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsFacade, providedIn: 'root' });
|
|
@@ -273,58 +1074,272 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
273
1074
|
args: [{ providedIn: 'root' }]
|
|
274
1075
|
}] });
|
|
275
1076
|
|
|
1077
|
+
const EMPTY_SCOPE$1 = {
|
|
1078
|
+
mode: 'Explicit',
|
|
1079
|
+
readPolicy: 'ImplicitReadForSelectedActions',
|
|
1080
|
+
accessibilityGroupIds: [],
|
|
1081
|
+
targets: [],
|
|
1082
|
+
};
|
|
1083
|
+
function targetKey(t) {
|
|
1084
|
+
return `${t.targetType}:${t.targetId ?? ''}:${t.permissionTargetId ?? ''}:${t.moduleKey}`;
|
|
1085
|
+
}
|
|
1086
|
+
function requestTargetKey(t) {
|
|
1087
|
+
return `${t.targetType}:${t.targetId ?? ''}:${t.permissionTargetId ?? ''}:${t.moduleKey}`;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Scope picker driven entirely by Identity-provided options.
|
|
1091
|
+
*
|
|
1092
|
+
* - `[(scope)]` two-way binds a normalized `DelegationScopeRequest`.
|
|
1093
|
+
* - Loads options via `DelegationsFacade.loadScopeOptions()` on init.
|
|
1094
|
+
* - Calls `/scope/preview` (debounced) so the user sees the server-built summary
|
|
1095
|
+
* and denied items before they save.
|
|
1096
|
+
* - Non-delegable operations render disabled with the BE-supplied `reason`.
|
|
1097
|
+
*/
|
|
1098
|
+
class ScopePicker {
|
|
1099
|
+
/** Two-way bound — the form drives initial value and reads updates back. */
|
|
1100
|
+
scope = model(EMPTY_SCOPE$1, ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
|
|
1101
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
1102
|
+
/** Admin scope-as-another-user — passed through to the options endpoint. */
|
|
1103
|
+
delegatorUserId = input(undefined, ...(ngDevMode ? [{ debugName: "delegatorUserId" }] : /* istanbul ignore next */ []));
|
|
1104
|
+
facade = inject(DelegationsFacade);
|
|
1105
|
+
transloco = inject(TranslocoService);
|
|
1106
|
+
options = this.facade.scopeOptions;
|
|
1107
|
+
preview = this.facade.scopePreview;
|
|
1108
|
+
isLoadingOptions = this.facade.isLoadingScopeOptions;
|
|
1109
|
+
isPreviewing = this.facade.isPreviewingScope;
|
|
1110
|
+
/** Set of accordion target keys currently expanded. */
|
|
1111
|
+
expanded = signal(new Set(), ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
1112
|
+
readPolicies = computed(() => this.options()?.readPolicies ?? [], ...(ngDevMode ? [{ debugName: "readPolicies" }] : /* istanbul ignore next */ []));
|
|
1113
|
+
/** Flat list of accessibility group options (across modules). */
|
|
1114
|
+
accessibilityGroups = computed(() => {
|
|
1115
|
+
const acc = this.options()?.accessibilities ?? [];
|
|
1116
|
+
return acc.flatMap((a) => a.groups.map((g) => ({
|
|
1117
|
+
appAccessibilityId: a.appAccessibilityId,
|
|
1118
|
+
accessibilityGroupId: g.accessibilityGroupId,
|
|
1119
|
+
moduleName: a.moduleName,
|
|
1120
|
+
groupName: g.name,
|
|
1121
|
+
})));
|
|
1122
|
+
}, ...(ngDevMode ? [{ debugName: "accessibilityGroups" }] : /* istanbul ignore next */ []));
|
|
1123
|
+
/** Targets grouped by `targetType` for the accordion UI. */
|
|
1124
|
+
groupedTargets = computed(() => {
|
|
1125
|
+
const targets = this.options()?.targets ?? [];
|
|
1126
|
+
const map = new Map();
|
|
1127
|
+
for (const t of targets) {
|
|
1128
|
+
const list = map.get(t.targetType) ?? [];
|
|
1129
|
+
list.push(t);
|
|
1130
|
+
map.set(t.targetType, list);
|
|
1131
|
+
}
|
|
1132
|
+
return Array.from(map.entries()).map(([type, items]) => ({
|
|
1133
|
+
type,
|
|
1134
|
+
items,
|
|
1135
|
+
}));
|
|
1136
|
+
}, ...(ngDevMode ? [{ debugName: "groupedTargets" }] : /* istanbul ignore next */ []));
|
|
1137
|
+
/** O(1) lookup: is this accessibility group selected? */
|
|
1138
|
+
selectedAccessibilityIds = computed(() => new Set(this.scope().accessibilityGroupIds ?? []), ...(ngDevMode ? [{ debugName: "selectedAccessibilityIds" }] : /* istanbul ignore next */ []));
|
|
1139
|
+
/** O(1) lookup: which targets are picked, and which ops per target? */
|
|
1140
|
+
selectedTargetMap = computed(() => {
|
|
1141
|
+
const map = new Map();
|
|
1142
|
+
for (const t of this.scope().targets) {
|
|
1143
|
+
const key = requestTargetKey(t);
|
|
1144
|
+
map.set(key, new Set(t.operations.map((op) => op.permissionCommand)));
|
|
1145
|
+
}
|
|
1146
|
+
return map;
|
|
1147
|
+
}, ...(ngDevMode ? [{ debugName: "selectedTargetMap" }] : /* istanbul ignore next */ []));
|
|
1148
|
+
// -------------------------------------------------------------------------
|
|
1149
|
+
// Lifecycle
|
|
1150
|
+
// -------------------------------------------------------------------------
|
|
1151
|
+
ngOnInit() {
|
|
1152
|
+
this.facade.loadScopeOptions(this.delegatorUserId());
|
|
1153
|
+
}
|
|
1154
|
+
constructor() {
|
|
1155
|
+
// Debounced preview: whenever the scope changes, call /scope/preview.
|
|
1156
|
+
// (Skips the empty initial value.)
|
|
1157
|
+
let timer = null;
|
|
1158
|
+
effect(() => {
|
|
1159
|
+
const s = this.scope();
|
|
1160
|
+
const hasContent = s.accessibilityGroupIds.length > 0 || s.targets.length > 0;
|
|
1161
|
+
untracked(() => {
|
|
1162
|
+
if (!hasContent) {
|
|
1163
|
+
this.facade.clearScopePreview();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (timer)
|
|
1167
|
+
clearTimeout(timer);
|
|
1168
|
+
timer = setTimeout(() => this.facade.previewScope(s), 300);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
// -------------------------------------------------------------------------
|
|
1173
|
+
// UI handlers
|
|
1174
|
+
// -------------------------------------------------------------------------
|
|
1175
|
+
setReadPolicy(policy) {
|
|
1176
|
+
if (this.readonly())
|
|
1177
|
+
return;
|
|
1178
|
+
this.scope.update((s) => ({ ...s, readPolicy: policy }));
|
|
1179
|
+
}
|
|
1180
|
+
toggleAccessibility(group) {
|
|
1181
|
+
if (this.readonly())
|
|
1182
|
+
return;
|
|
1183
|
+
this.scope.update((s) => {
|
|
1184
|
+
const ids = new Set(s.accessibilityGroupIds);
|
|
1185
|
+
if (ids.has(group.accessibilityGroupId)) {
|
|
1186
|
+
ids.delete(group.accessibilityGroupId);
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
ids.add(group.accessibilityGroupId);
|
|
1190
|
+
}
|
|
1191
|
+
return { ...s, accessibilityGroupIds: Array.from(ids) };
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
isAccessibilitySelected(group) {
|
|
1195
|
+
return this.selectedAccessibilityIds().has(group.accessibilityGroupId);
|
|
1196
|
+
}
|
|
1197
|
+
toggleAccordion(target) {
|
|
1198
|
+
const key = targetKey(target);
|
|
1199
|
+
const next = new Set(this.expanded());
|
|
1200
|
+
if (next.has(key))
|
|
1201
|
+
next.delete(key);
|
|
1202
|
+
else
|
|
1203
|
+
next.add(key);
|
|
1204
|
+
this.expanded.set(next);
|
|
1205
|
+
}
|
|
1206
|
+
isExpanded(target) {
|
|
1207
|
+
return this.expanded().has(targetKey(target));
|
|
1208
|
+
}
|
|
1209
|
+
toggleOperation(target, op) {
|
|
1210
|
+
if (this.readonly() || !op.isDelegable)
|
|
1211
|
+
return;
|
|
1212
|
+
this.scope.update((s) => {
|
|
1213
|
+
const reqKey = `${target.targetType}:${target.targetId ?? ''}:${target.permissionTargetId ?? ''}:${target.moduleKey}`;
|
|
1214
|
+
const targets = [...s.targets];
|
|
1215
|
+
const idx = targets.findIndex((t) => requestTargetKey(t) === reqKey);
|
|
1216
|
+
const baseRequest = {
|
|
1217
|
+
targetType: target.targetType,
|
|
1218
|
+
targetId: target.targetId,
|
|
1219
|
+
levelId: target.levelId,
|
|
1220
|
+
levelModuleId: target.levelModuleId,
|
|
1221
|
+
domainModuleId: target.domainModuleId,
|
|
1222
|
+
moduleKey: target.moduleKey,
|
|
1223
|
+
permissionModuleType: target.permissionModuleType,
|
|
1224
|
+
permissionTargetId: target.permissionTargetId,
|
|
1225
|
+
operations: [],
|
|
1226
|
+
};
|
|
1227
|
+
if (idx === -1) {
|
|
1228
|
+
targets.push({
|
|
1229
|
+
...baseRequest,
|
|
1230
|
+
operations: [
|
|
1231
|
+
{
|
|
1232
|
+
operationKey: op.operationKey,
|
|
1233
|
+
permissionCommand: op.permissionCommand,
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
const current = targets[idx];
|
|
1240
|
+
const opIdx = current.operations.findIndex((o) => o.permissionCommand === op.permissionCommand);
|
|
1241
|
+
if (opIdx === -1) {
|
|
1242
|
+
targets[idx] = {
|
|
1243
|
+
...current,
|
|
1244
|
+
operations: [
|
|
1245
|
+
...current.operations,
|
|
1246
|
+
{
|
|
1247
|
+
operationKey: op.operationKey,
|
|
1248
|
+
permissionCommand: op.permissionCommand,
|
|
1249
|
+
},
|
|
1250
|
+
],
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
const nextOps = current.operations.slice();
|
|
1255
|
+
nextOps.splice(opIdx, 1);
|
|
1256
|
+
if (nextOps.length === 0) {
|
|
1257
|
+
// last op removed — drop the target entirely
|
|
1258
|
+
targets.splice(idx, 1);
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
targets[idx] = { ...current, operations: nextOps };
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return { ...s, targets };
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
isOperationSelected(target, op) {
|
|
1269
|
+
const key = `${target.targetType}:${target.targetId ?? ''}:${target.permissionTargetId ?? ''}:${target.moduleKey}`;
|
|
1270
|
+
return (this.selectedTargetMap().get(key)?.has(op.permissionCommand) ?? false);
|
|
1271
|
+
}
|
|
1272
|
+
selectedOpsCount(target) {
|
|
1273
|
+
const key = `${target.targetType}:${target.targetId ?? ''}:${target.permissionTargetId ?? ''}:${target.moduleKey}`;
|
|
1274
|
+
return this.selectedTargetMap().get(key)?.size ?? 0;
|
|
1275
|
+
}
|
|
1276
|
+
reasonLabel(op) {
|
|
1277
|
+
return op.reason ?? this.transloco.translate('delegations.non-delegable-reason');
|
|
1278
|
+
}
|
|
1279
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ScopePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1280
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: ScopePicker, isStandalone: true, selector: "mt-scope-picker", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, delegatorUserId: { classPropertyName: "delegatorUserId", publicName: "delegatorUserId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { scope: "scopeChange" }, ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"mt-scope-picker flex flex-col gap-4\">\r\n <!-- Loading skeleton -->\r\n @if (isLoadingOptions() && !options()) {\r\n <p-skeleton height=\"2rem\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n } @else if (options(); as opts) {\r\n <!-- ============================================================ -->\r\n <!-- Read policy -->\r\n <!-- ============================================================ -->\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.read-policy') }}\r\n </h4>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (rp of readPolicies(); track rp.key) {\r\n <button\r\n type=\"button\"\r\n class=\"px-3 py-1.5 rounded-full text-xs border transition-colors cursor-pointer\"\r\n [class.border-primary]=\"scope().readPolicy === rp.key\"\r\n [class.bg-primary-50]=\"scope().readPolicy === rp.key\"\r\n [class.text-primary]=\"scope().readPolicy === rp.key\"\r\n [class.border-gray-300]=\"scope().readPolicy !== rp.key\"\r\n [class.text-gray-700]=\"scope().readPolicy !== rp.key\"\r\n [disabled]=\"readonly()\"\r\n (click)=\"setReadPolicy(rp.key)\">\r\n {{ t('delegations.' + (rp.key === 'ImplicitReadForSelectedActions'\r\n ? 'implicit-read'\r\n : rp.key === 'ExplicitReadOnly'\r\n ? 'explicit-read-only'\r\n : 'no-read-unless-own-permission')) }}\r\n </button>\r\n }\r\n </div>\r\n </section>\r\n\r\n <!-- ============================================================ -->\r\n <!-- Accessibilities -->\r\n <!-- ============================================================ -->\r\n @if (accessibilityGroups().length > 0) {\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.scope') }} \u2014\r\n {{ t('delegations.delegated-to') }}\r\n </h4>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (g of accessibilityGroups(); track g.accessibilityGroupId) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center gap-2 px-3 py-1.5 rounded-md text-xs border transition-colors cursor-pointer\"\r\n [class.border-primary]=\"isAccessibilitySelected(g)\"\r\n [class.bg-primary-50]=\"isAccessibilitySelected(g)\"\r\n [class.text-primary]=\"isAccessibilitySelected(g)\"\r\n [class.border-gray-300]=\"!isAccessibilitySelected(g)\"\r\n [class.text-gray-700]=\"!isAccessibilitySelected(g)\"\r\n [disabled]=\"readonly()\"\r\n (click)=\"toggleAccessibility(g)\">\r\n @if (isAccessibilitySelected(g)) {\r\n <mt-icon icon=\"general.check\" styleClass=\"text-sm\"></mt-icon>\r\n }\r\n <span>{{ g.moduleName }} / {{ g.groupName }}</span>\r\n </button>\r\n }\r\n </div>\r\n </section>\r\n }\r\n\r\n <!-- ============================================================ -->\r\n <!-- Targets + operations -->\r\n <!-- ============================================================ -->\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.target-type') }} /\r\n {{ t('delegations.operations') }}\r\n </h4>\r\n\r\n @if (groupedTargets().length === 0) {\r\n <p class=\"text-xs text-gray-500\">\r\n {{ t('delegations.no-targets-selected') }}\r\n </p>\r\n }\r\n\r\n @for (group of groupedTargets(); track group.type) {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"text-xs uppercase text-gray-500 tracking-wide px-2\">\r\n {{ group.type }}\r\n </div>\r\n @for (target of group.items; track target.displayName) {\r\n <div class=\"border border-gray-200 rounded-md overflow-hidden\">\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between w-full px-3 py-2 hover:bg-surface-50 cursor-pointer\"\r\n (click)=\"toggleAccordion(target)\">\r\n <div class=\"flex items-center gap-2 min-w-0\">\r\n <mt-icon\r\n [icon]=\"\r\n isExpanded(target)\r\n ? 'arrow.chevron-down'\r\n : 'arrow.chevron-right'\r\n \"\r\n styleClass=\"text-base text-gray-400\"></mt-icon>\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ target.displayName }}\r\n </span>\r\n </div>\r\n @if (selectedOpsCount(target); as count) {\r\n @if (count > 0) {\r\n <span\r\n class=\"text-xs text-primary font-medium px-2 py-0.5 rounded-full bg-primary-50\">\r\n {{ count }}\r\n </span>\r\n }\r\n }\r\n </button>\r\n\r\n @if (isExpanded(target)) {\r\n <div class=\"flex flex-col gap-1 px-3 py-2 bg-surface-50/40\">\r\n @for (op of target.operations; track op.permissionCommand) {\r\n <label\r\n class=\"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer\"\r\n [class.opacity-60]=\"!op.isDelegable\"\r\n [class.cursor-not-allowed]=\"!op.isDelegable\"\r\n [class.hover:bg-surface-100]=\"op.isDelegable\"\r\n [pTooltip]=\"!op.isDelegable ? reasonLabel(op) : null\"\r\n tooltipPosition=\"top\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"isOperationSelected(target, op)\"\r\n [disabled]=\"!op.isDelegable || readonly()\"\r\n (change)=\"toggleOperation(target, op)\" />\r\n <span class=\"text-sm\">{{ op.displayName }}</span>\r\n @if (op.isHighRisk) {\r\n <mt-icon\r\n icon=\"alert.alert-triangle\"\r\n styleClass=\"text-sm text-amber-500\"\r\n [pTooltip]=\"t('delegations.high-risk-action')\"\r\n tooltipPosition=\"top\"></mt-icon>\r\n }\r\n @if (!op.isDelegable) {\r\n <span class=\"text-xs text-gray-400 ms-auto\">\r\n {{ reasonLabel(op) }}\r\n </span>\r\n }\r\n </label>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </section>\r\n\r\n <!-- ============================================================ -->\r\n <!-- Preview -->\r\n <!-- ============================================================ -->\r\n <section\r\n class=\"flex flex-col gap-1 px-3 py-2 rounded-md border border-dashed border-gray-300 bg-surface-50\">\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <div class=\"text-xs font-medium text-gray-700\">\r\n {{ t('delegations.preview-scope') }}\r\n </div>\r\n @if (isPreviewing()) {\r\n <p-skeleton width=\"6rem\" height=\"0.75rem\"></p-skeleton>\r\n }\r\n </div>\r\n @if (preview(); as p) {\r\n @if (p.isValid) {\r\n <div class=\"text-sm text-gray-900\">\r\n {{ p.summary || t('delegations.scope-summary') }}\r\n </div>\r\n } @else {\r\n <div class=\"text-sm text-red-700\">\r\n {{ t('delegations.scope-preview-invalid') }}\r\n </div>\r\n }\r\n @if (p.warnings.length > 0) {\r\n <ul class=\"text-xs text-amber-700 list-disc list-inside\">\r\n @for (w of p.warnings; track w.code) {\r\n <li>{{ w.message }}</li>\r\n }\r\n </ul>\r\n }\r\n @if (p.deniedItems.length > 0) {\r\n <ul class=\"text-xs text-red-700 list-disc list-inside\">\r\n @for (d of p.deniedItems; track d.permissionCommand) {\r\n <li>{{ d.message }}</li>\r\n }\r\n </ul>\r\n }\r\n } @else {\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t('delegations.no-operations-selected') }}\r\n </div>\r\n }\r\n </section>\r\n }\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block}.mt-scope-picker input[type=checkbox]{accent-color:var(--p-primary-color, currentColor)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1281
|
+
}
|
|
1282
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: ScopePicker, decorators: [{
|
|
1283
|
+
type: Component,
|
|
1284
|
+
args: [{ selector: 'mt-scope-picker', standalone: true, imports: [
|
|
1285
|
+
CommonModule,
|
|
1286
|
+
Icon,
|
|
1287
|
+
SkeletonModule,
|
|
1288
|
+
TooltipModule,
|
|
1289
|
+
TranslocoDirective,
|
|
1290
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"mt-scope-picker flex flex-col gap-4\">\r\n <!-- Loading skeleton -->\r\n @if (isLoadingOptions() && !options()) {\r\n <p-skeleton height=\"2rem\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n } @else if (options(); as opts) {\r\n <!-- ============================================================ -->\r\n <!-- Read policy -->\r\n <!-- ============================================================ -->\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.read-policy') }}\r\n </h4>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (rp of readPolicies(); track rp.key) {\r\n <button\r\n type=\"button\"\r\n class=\"px-3 py-1.5 rounded-full text-xs border transition-colors cursor-pointer\"\r\n [class.border-primary]=\"scope().readPolicy === rp.key\"\r\n [class.bg-primary-50]=\"scope().readPolicy === rp.key\"\r\n [class.text-primary]=\"scope().readPolicy === rp.key\"\r\n [class.border-gray-300]=\"scope().readPolicy !== rp.key\"\r\n [class.text-gray-700]=\"scope().readPolicy !== rp.key\"\r\n [disabled]=\"readonly()\"\r\n (click)=\"setReadPolicy(rp.key)\">\r\n {{ t('delegations.' + (rp.key === 'ImplicitReadForSelectedActions'\r\n ? 'implicit-read'\r\n : rp.key === 'ExplicitReadOnly'\r\n ? 'explicit-read-only'\r\n : 'no-read-unless-own-permission')) }}\r\n </button>\r\n }\r\n </div>\r\n </section>\r\n\r\n <!-- ============================================================ -->\r\n <!-- Accessibilities -->\r\n <!-- ============================================================ -->\r\n @if (accessibilityGroups().length > 0) {\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.scope') }} \u2014\r\n {{ t('delegations.delegated-to') }}\r\n </h4>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (g of accessibilityGroups(); track g.accessibilityGroupId) {\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center gap-2 px-3 py-1.5 rounded-md text-xs border transition-colors cursor-pointer\"\r\n [class.border-primary]=\"isAccessibilitySelected(g)\"\r\n [class.bg-primary-50]=\"isAccessibilitySelected(g)\"\r\n [class.text-primary]=\"isAccessibilitySelected(g)\"\r\n [class.border-gray-300]=\"!isAccessibilitySelected(g)\"\r\n [class.text-gray-700]=\"!isAccessibilitySelected(g)\"\r\n [disabled]=\"readonly()\"\r\n (click)=\"toggleAccessibility(g)\">\r\n @if (isAccessibilitySelected(g)) {\r\n <mt-icon icon=\"general.check\" styleClass=\"text-sm\"></mt-icon>\r\n }\r\n <span>{{ g.moduleName }} / {{ g.groupName }}</span>\r\n </button>\r\n }\r\n </div>\r\n </section>\r\n }\r\n\r\n <!-- ============================================================ -->\r\n <!-- Targets + operations -->\r\n <!-- ============================================================ -->\r\n <section class=\"flex flex-col gap-2\">\r\n <h4 class=\"text-sm font-medium text-gray-800\">\r\n {{ t('delegations.target-type') }} /\r\n {{ t('delegations.operations') }}\r\n </h4>\r\n\r\n @if (groupedTargets().length === 0) {\r\n <p class=\"text-xs text-gray-500\">\r\n {{ t('delegations.no-targets-selected') }}\r\n </p>\r\n }\r\n\r\n @for (group of groupedTargets(); track group.type) {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"text-xs uppercase text-gray-500 tracking-wide px-2\">\r\n {{ group.type }}\r\n </div>\r\n @for (target of group.items; track target.displayName) {\r\n <div class=\"border border-gray-200 rounded-md overflow-hidden\">\r\n <button\r\n type=\"button\"\r\n class=\"flex items-center justify-between w-full px-3 py-2 hover:bg-surface-50 cursor-pointer\"\r\n (click)=\"toggleAccordion(target)\">\r\n <div class=\"flex items-center gap-2 min-w-0\">\r\n <mt-icon\r\n [icon]=\"\r\n isExpanded(target)\r\n ? 'arrow.chevron-down'\r\n : 'arrow.chevron-right'\r\n \"\r\n styleClass=\"text-base text-gray-400\"></mt-icon>\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ target.displayName }}\r\n </span>\r\n </div>\r\n @if (selectedOpsCount(target); as count) {\r\n @if (count > 0) {\r\n <span\r\n class=\"text-xs text-primary font-medium px-2 py-0.5 rounded-full bg-primary-50\">\r\n {{ count }}\r\n </span>\r\n }\r\n }\r\n </button>\r\n\r\n @if (isExpanded(target)) {\r\n <div class=\"flex flex-col gap-1 px-3 py-2 bg-surface-50/40\">\r\n @for (op of target.operations; track op.permissionCommand) {\r\n <label\r\n class=\"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer\"\r\n [class.opacity-60]=\"!op.isDelegable\"\r\n [class.cursor-not-allowed]=\"!op.isDelegable\"\r\n [class.hover:bg-surface-100]=\"op.isDelegable\"\r\n [pTooltip]=\"!op.isDelegable ? reasonLabel(op) : null\"\r\n tooltipPosition=\"top\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"isOperationSelected(target, op)\"\r\n [disabled]=\"!op.isDelegable || readonly()\"\r\n (change)=\"toggleOperation(target, op)\" />\r\n <span class=\"text-sm\">{{ op.displayName }}</span>\r\n @if (op.isHighRisk) {\r\n <mt-icon\r\n icon=\"alert.alert-triangle\"\r\n styleClass=\"text-sm text-amber-500\"\r\n [pTooltip]=\"t('delegations.high-risk-action')\"\r\n tooltipPosition=\"top\"></mt-icon>\r\n }\r\n @if (!op.isDelegable) {\r\n <span class=\"text-xs text-gray-400 ms-auto\">\r\n {{ reasonLabel(op) }}\r\n </span>\r\n }\r\n </label>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </section>\r\n\r\n <!-- ============================================================ -->\r\n <!-- Preview -->\r\n <!-- ============================================================ -->\r\n <section\r\n class=\"flex flex-col gap-1 px-3 py-2 rounded-md border border-dashed border-gray-300 bg-surface-50\">\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <div class=\"text-xs font-medium text-gray-700\">\r\n {{ t('delegations.preview-scope') }}\r\n </div>\r\n @if (isPreviewing()) {\r\n <p-skeleton width=\"6rem\" height=\"0.75rem\"></p-skeleton>\r\n }\r\n </div>\r\n @if (preview(); as p) {\r\n @if (p.isValid) {\r\n <div class=\"text-sm text-gray-900\">\r\n {{ p.summary || t('delegations.scope-summary') }}\r\n </div>\r\n } @else {\r\n <div class=\"text-sm text-red-700\">\r\n {{ t('delegations.scope-preview-invalid') }}\r\n </div>\r\n }\r\n @if (p.warnings.length > 0) {\r\n <ul class=\"text-xs text-amber-700 list-disc list-inside\">\r\n @for (w of p.warnings; track w.code) {\r\n <li>{{ w.message }}</li>\r\n }\r\n </ul>\r\n }\r\n @if (p.deniedItems.length > 0) {\r\n <ul class=\"text-xs text-red-700 list-disc list-inside\">\r\n @for (d of p.deniedItems; track d.permissionCommand) {\r\n <li>{{ d.message }}</li>\r\n }\r\n </ul>\r\n }\r\n } @else {\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t('delegations.no-operations-selected') }}\r\n </div>\r\n }\r\n </section>\r\n }\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block}.mt-scope-picker input[type=checkbox]{accent-color:var(--p-primary-color, currentColor)}\n"] }]
|
|
1291
|
+
}], ctorParameters: () => [], propDecorators: { scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }, { type: i0.Output, args: ["scopeChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], delegatorUserId: [{ type: i0.Input, args: [{ isSignal: true, alias: "delegatorUserId", required: false }] }] } });
|
|
1292
|
+
|
|
1293
|
+
const EMPTY_SCOPE = {
|
|
1294
|
+
mode: 'Explicit',
|
|
1295
|
+
readPolicy: 'ImplicitReadForSelectedActions',
|
|
1296
|
+
accessibilityGroupIds: [],
|
|
1297
|
+
targets: [],
|
|
1298
|
+
};
|
|
1299
|
+
/**
|
|
1300
|
+
* Delegation create/edit form, target-shape compliant.
|
|
1301
|
+
*
|
|
1302
|
+
* - Delegator is server-derived from the authenticated principal — not collected
|
|
1303
|
+
* here (admin "create on behalf of" lives behind a separate flag).
|
|
1304
|
+
* - `isActive` no longer exists; status is server-calculated.
|
|
1305
|
+
* - Scope is collected by `<mt-scope-picker>` (loaded from `/scope/options`)
|
|
1306
|
+
* and validated via `/scope/preview` on every change. Edit mode rehydrates
|
|
1307
|
+
* the scope from the loaded delegation.
|
|
1308
|
+
*/
|
|
276
1309
|
class DelegationForm {
|
|
277
|
-
|
|
278
|
-
|
|
1310
|
+
delegationForEdit = input(null, ...(ngDevMode ? [{ debugName: "delegationForEdit" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
1312
|
+
halfWidth = 'flex flex-col gap-1 lg:col-span-6';
|
|
1313
|
+
fullWidth = 'flex flex-col gap-1 lg:col-span-12';
|
|
279
1314
|
modal = inject(ModalService);
|
|
280
1315
|
ref = inject(ModalRef);
|
|
281
|
-
|
|
282
|
-
translocoService = inject(TranslocoService);
|
|
1316
|
+
transloco = inject(TranslocoService);
|
|
283
1317
|
facade = inject(DelegationsFacade);
|
|
284
|
-
|
|
1318
|
+
// Reactive form
|
|
285
1319
|
delegationFormControl = new FormControl();
|
|
286
1320
|
formValue = toSignal(this.delegationFormControl.valueChanges);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
context = new HttpContext().set(REQUEST_CONTEXT, {
|
|
291
|
-
useBaseUrl: false,
|
|
292
|
-
});
|
|
1321
|
+
selected = this.facade.selected;
|
|
1322
|
+
isDetailLoading = this.facade.isLoadingDetail;
|
|
1323
|
+
isSaving = computed(() => this.facade.isCreating() || this.facade.isUpdating(), ...(ngDevMode ? [{ debugName: "isSaving" }] : /* istanbul ignore next */ []));
|
|
1324
|
+
context = new HttpContext().set(REQUEST_CONTEXT, { useBaseUrl: false });
|
|
293
1325
|
isDelegatingToSelf = computed(() => {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1326
|
+
const v = this.formValue();
|
|
1327
|
+
if (!v)
|
|
1328
|
+
return false;
|
|
1329
|
+
const toId = (v.delegatedUser?.id ?? v.delegatedUser);
|
|
1330
|
+
const meId = (this.selected()?.delegator?.id ?? '');
|
|
1331
|
+
return !!toId && !!meId && toId === meId;
|
|
298
1332
|
}, ...(ngDevMode ? [{ debugName: "isDelegatingToSelf" }] : /* istanbul ignore next */ []));
|
|
1333
|
+
// The current scope draft (built outside the form for MVP; placeholder UI).
|
|
1334
|
+
scope = signal(EMPTY_SCOPE, ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
|
|
299
1335
|
specificDaysOptions = signal([
|
|
300
|
-
{
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
label: this.translocoService.translate('delegations.tuesday'),
|
|
310
|
-
value: 2,
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
label: this.translocoService.translate('delegations.wednesday'),
|
|
314
|
-
value: 3,
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
label: this.translocoService.translate('delegations.thursday'),
|
|
318
|
-
value: 4,
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
label: this.translocoService.translate('delegations.friday'),
|
|
322
|
-
value: 5,
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
label: this.translocoService.translate('delegations.saturday'),
|
|
326
|
-
value: 6,
|
|
327
|
-
},
|
|
1336
|
+
{ label: this.transloco.translate('delegations.sunday'), value: 0 },
|
|
1337
|
+
{ label: this.transloco.translate('delegations.monday'), value: 1 },
|
|
1338
|
+
{ label: this.transloco.translate('delegations.tuesday'), value: 2 },
|
|
1339
|
+
{ label: this.transloco.translate('delegations.wednesday'), value: 3 },
|
|
1340
|
+
{ label: this.transloco.translate('delegations.thursday'), value: 4 },
|
|
1341
|
+
{ label: this.transloco.translate('delegations.friday'), value: 5 },
|
|
1342
|
+
{ label: this.transloco.translate('delegations.saturday'), value: 6 },
|
|
328
1343
|
], ...(ngDevMode ? [{ debugName: "specificDaysOptions" }] : /* istanbul ignore next */ []));
|
|
329
1344
|
delegationFormConfig = computed(() => ({
|
|
330
1345
|
sections: [
|
|
@@ -335,98 +1350,89 @@ class DelegationForm {
|
|
|
335
1350
|
order: 1,
|
|
336
1351
|
fields: [
|
|
337
1352
|
new UserSearchFieldConfig({
|
|
338
|
-
key: '
|
|
339
|
-
label: this.
|
|
340
|
-
apiUrl: 'Identity/users',
|
|
341
|
-
context: this.context,
|
|
342
|
-
validators: [ValidatorConfig.required()],
|
|
343
|
-
order: 1,
|
|
344
|
-
cssClass: this.halfWidthFieldClass,
|
|
345
|
-
colSpan: 6,
|
|
346
|
-
disabled: !!this.delegationForEdit()?.id,
|
|
347
|
-
}),
|
|
348
|
-
new UserSearchFieldConfig({
|
|
349
|
-
key: 'delegateTo',
|
|
350
|
-
label: this.translocoService.translate('delegations.delegated-to'),
|
|
1353
|
+
key: 'delegatedUser',
|
|
1354
|
+
label: this.transloco.translate('delegations.delegated-to'),
|
|
351
1355
|
apiUrl: 'Identity/users',
|
|
352
1356
|
context: this.context,
|
|
353
1357
|
validators: [ValidatorConfig.required()],
|
|
354
|
-
|
|
355
|
-
cssClass: this.halfWidthFieldClass,
|
|
1358
|
+
cssClass: this.halfWidth,
|
|
356
1359
|
colSpan: 6,
|
|
357
|
-
|
|
1360
|
+
order: 1,
|
|
1361
|
+
disabled: this.readonly() || !!this.delegationForEdit()?.id,
|
|
358
1362
|
}),
|
|
359
1363
|
new TextareaFieldConfig({
|
|
360
1364
|
key: 'description',
|
|
361
|
-
label: this.
|
|
362
|
-
placeholder: this.
|
|
363
|
-
cssClass: this.
|
|
1365
|
+
label: this.transloco.translate('delegations.description'),
|
|
1366
|
+
placeholder: this.transloco.translate('delegations.description'),
|
|
1367
|
+
cssClass: this.fullWidth,
|
|
364
1368
|
colSpan: 12,
|
|
365
|
-
order:
|
|
1369
|
+
order: 2,
|
|
1370
|
+
disabled: this.readonly(),
|
|
366
1371
|
}),
|
|
367
1372
|
new DateFieldConfig({
|
|
368
|
-
key: '
|
|
369
|
-
label: this.
|
|
370
|
-
placeholder: this.
|
|
1373
|
+
key: 'startDateUtc',
|
|
1374
|
+
label: this.transloco.translate('delegations.delegation-date-from'),
|
|
1375
|
+
placeholder: this.transloco.translate('delegations.delegation-date-from'),
|
|
371
1376
|
validators: [ValidatorConfig.required()],
|
|
372
1377
|
showTime: true,
|
|
373
|
-
cssClass: this.
|
|
1378
|
+
cssClass: this.halfWidth,
|
|
374
1379
|
colSpan: 6,
|
|
375
|
-
order:
|
|
1380
|
+
order: 3,
|
|
1381
|
+
disabled: this.readonly(),
|
|
376
1382
|
}),
|
|
377
1383
|
new DateFieldConfig({
|
|
378
|
-
key: '
|
|
379
|
-
label: this.
|
|
380
|
-
placeholder: this.
|
|
1384
|
+
key: 'endDateUtc',
|
|
1385
|
+
label: this.transloco.translate('delegations.delegation-date-to'),
|
|
1386
|
+
placeholder: this.transloco.translate('delegations.delegation-date-to'),
|
|
381
1387
|
validators: [ValidatorConfig.required()],
|
|
382
1388
|
showTime: true,
|
|
383
|
-
cssClass: this.
|
|
1389
|
+
cssClass: this.halfWidth,
|
|
384
1390
|
colSpan: 6,
|
|
385
|
-
order:
|
|
1391
|
+
order: 4,
|
|
1392
|
+
disabled: this.readonly(),
|
|
386
1393
|
}),
|
|
387
1394
|
new RadioButtonFieldConfig({
|
|
388
|
-
key: '
|
|
389
|
-
label: this.
|
|
1395
|
+
key: 'dayRulesType',
|
|
1396
|
+
label: this.transloco.translate('delegations.delegation-days-apply'),
|
|
390
1397
|
options: [
|
|
391
1398
|
{
|
|
392
|
-
label: this.
|
|
1399
|
+
label: this.transloco.translate('delegations.full-range'),
|
|
393
1400
|
value: 'FullRange',
|
|
394
1401
|
},
|
|
395
1402
|
{
|
|
396
|
-
label: this.
|
|
1403
|
+
label: this.transloco.translate('delegations.specific-days'),
|
|
397
1404
|
value: 'SpecificDays',
|
|
398
1405
|
},
|
|
399
1406
|
],
|
|
400
1407
|
optionLabel: 'label',
|
|
401
1408
|
optionValue: 'value',
|
|
402
1409
|
validators: [ValidatorConfig.required()],
|
|
403
|
-
cssClass: this.
|
|
1410
|
+
cssClass: this.fullWidth,
|
|
404
1411
|
colSpan: 12,
|
|
405
|
-
order:
|
|
1412
|
+
order: 5,
|
|
1413
|
+
disabled: this.readonly(),
|
|
406
1414
|
}),
|
|
407
1415
|
new MultiSelectFieldConfig({
|
|
408
1416
|
key: 'specificDays',
|
|
409
|
-
label: this.
|
|
1417
|
+
label: this.transloco.translate('delegations.specific-days'),
|
|
410
1418
|
options: this.specificDaysOptions(),
|
|
411
1419
|
optionLabel: 'label',
|
|
412
1420
|
optionValue: 'value',
|
|
413
1421
|
maxSelectedLabels: 7,
|
|
414
1422
|
relations: [
|
|
415
|
-
{
|
|
416
|
-
key: 'delegationDaysType',
|
|
417
|
-
value: 'SpecificDays',
|
|
418
|
-
action: 'show',
|
|
419
|
-
},
|
|
1423
|
+
{ key: 'dayRulesType', value: 'SpecificDays', action: 'show' },
|
|
420
1424
|
],
|
|
421
|
-
cssClass: this.
|
|
1425
|
+
cssClass: this.fullWidth,
|
|
422
1426
|
colSpan: 12,
|
|
423
|
-
order:
|
|
1427
|
+
order: 6,
|
|
1428
|
+
disabled: this.readonly(),
|
|
424
1429
|
}),
|
|
425
1430
|
new ToggleFieldConfig({
|
|
426
|
-
key: '
|
|
427
|
-
label: this.
|
|
428
|
-
cssClass: `${this.
|
|
429
|
-
order:
|
|
1431
|
+
key: 'requiresApproval',
|
|
1432
|
+
label: this.transloco.translate('delegations.requires-approval'),
|
|
1433
|
+
cssClass: `${this.fullWidth} mt-2`,
|
|
1434
|
+
order: 7,
|
|
1435
|
+
disabled: this.readonly(),
|
|
430
1436
|
}),
|
|
431
1437
|
],
|
|
432
1438
|
},
|
|
@@ -434,93 +1440,170 @@ class DelegationForm {
|
|
|
434
1440
|
}), ...(ngDevMode ? [{ debugName: "delegationFormConfig" }] : /* istanbul ignore next */ []));
|
|
435
1441
|
constructor() {
|
|
436
1442
|
effect(() => {
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1443
|
+
const id = this.delegationForEdit()?.id;
|
|
1444
|
+
const d = this.selected();
|
|
1445
|
+
if (id && d) {
|
|
1446
|
+
this.delegationFormControl.patchValue({
|
|
1447
|
+
delegatedUser: d.delegatedUser,
|
|
1448
|
+
description: d.description,
|
|
1449
|
+
startDateUtc: d.startDateUtc,
|
|
1450
|
+
endDateUtc: d.endDateUtc,
|
|
1451
|
+
dayRulesType: d.dayRules?.type ?? 'FullRange',
|
|
1452
|
+
specificDays: d.dayRules?.specificDays ?? [],
|
|
1453
|
+
requiresApproval: d.requiresApproval ?? true,
|
|
1454
|
+
});
|
|
1455
|
+
// Snapshot existing scope so we round-trip it on save until the picker ships.
|
|
1456
|
+
if (d.scope) {
|
|
1457
|
+
this.scope.set({
|
|
1458
|
+
mode: 'Explicit',
|
|
1459
|
+
readPolicy: d.scope.readPolicy,
|
|
1460
|
+
accessibilityGroupIds: d.scope.accessibilities.map((a) => a.accessibilityGroupId),
|
|
1461
|
+
targets: d.scope.targets.map((t) => ({
|
|
1462
|
+
targetType: t.targetType,
|
|
1463
|
+
targetId: t.targetId,
|
|
1464
|
+
levelId: t.levelId,
|
|
1465
|
+
levelModuleId: t.levelModuleId,
|
|
1466
|
+
domainModuleId: t.domainModuleId,
|
|
1467
|
+
moduleKey: t.moduleKey,
|
|
1468
|
+
permissionModuleType: t.permissionModuleType,
|
|
1469
|
+
permissionTargetId: t.permissionTargetId,
|
|
1470
|
+
operations: t.operations.map((op) => ({
|
|
1471
|
+
operationKey: op.operationKey,
|
|
1472
|
+
permissionCommand: op.permissionCommand,
|
|
1473
|
+
})),
|
|
1474
|
+
})),
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
451
1477
|
}
|
|
452
|
-
else {
|
|
1478
|
+
else if (!id) {
|
|
453
1479
|
this.delegationFormControl.reset({
|
|
454
|
-
|
|
455
|
-
|
|
1480
|
+
dayRulesType: 'FullRange',
|
|
1481
|
+
requiresApproval: true,
|
|
456
1482
|
});
|
|
1483
|
+
this.scope.set(EMPTY_SCOPE);
|
|
457
1484
|
}
|
|
458
1485
|
});
|
|
459
1486
|
}
|
|
460
1487
|
ngOnInit() {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
this.facade.
|
|
1488
|
+
const id = this.delegationForEdit()?.id;
|
|
1489
|
+
if (id) {
|
|
1490
|
+
this.facade.getOne(id, true);
|
|
464
1491
|
}
|
|
465
1492
|
}
|
|
466
1493
|
onSubmit() {
|
|
467
|
-
if (!this.delegationFormControl.valid)
|
|
1494
|
+
if (this.readonly() || !this.delegationFormControl.valid)
|
|
468
1495
|
return;
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
?
|
|
480
|
-
|
|
481
|
-
|
|
1496
|
+
const v = this.delegationFormControl.value;
|
|
1497
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1498
|
+
const editing = this.delegationForEdit();
|
|
1499
|
+
const base = {
|
|
1500
|
+
description: v?.description,
|
|
1501
|
+
startDateUtc: v?.startDateUtc,
|
|
1502
|
+
endDateUtc: v?.endDateUtc,
|
|
1503
|
+
timeZoneId: tz,
|
|
1504
|
+
dayRules: {
|
|
1505
|
+
type: (v?.dayRulesType ?? 'FullRange'),
|
|
1506
|
+
specificDays: v?.dayRulesType === 'SpecificDays' ? (v?.specificDays ?? []) : [],
|
|
1507
|
+
},
|
|
1508
|
+
scope: this.scope(),
|
|
482
1509
|
};
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1510
|
+
if (editing?.id) {
|
|
1511
|
+
const req = {
|
|
1512
|
+
...base,
|
|
1513
|
+
rowVersion: editing.rowVersion,
|
|
1514
|
+
requiresApproval: v?.requiresApproval,
|
|
1515
|
+
};
|
|
1516
|
+
this.facade.update(editing.id, req).subscribe({
|
|
1517
|
+
next: () => this.ref.close(true),
|
|
490
1518
|
});
|
|
491
1519
|
}
|
|
492
1520
|
else {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1521
|
+
const req = {
|
|
1522
|
+
...base,
|
|
1523
|
+
delegatedUserId: v?.delegatedUser?.id ?? v?.delegatedUser,
|
|
1524
|
+
requiresApproval: !!v?.requiresApproval,
|
|
1525
|
+
};
|
|
1526
|
+
this.facade.create(req).subscribe({
|
|
1527
|
+
next: () => this.ref.close(true),
|
|
498
1528
|
});
|
|
499
1529
|
}
|
|
500
1530
|
}
|
|
501
1531
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
502
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: DelegationForm, isStandalone: true, selector: "mt-delegation-form", inputs: { delegationForEdit: { classPropertyName: "delegationForEdit", publicName: "delegationForEdit", isSignal: true, isRequired: false, transformFunction: null }
|
|
1532
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: DelegationForm, isStandalone: true, selector: "mt-delegation-form", inputs: { delegationForEdit: { classPropertyName: "delegationForEdit", publicName: "delegationForEdit", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'delegations'\">\r\n <div\r\n [class]=\"\r\n 'delegation-form-content flex min-w-0 flex-col gap-3 overflow-x-hidden p-4 max-[640px]:p-3 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <mt-dynamic-form\r\n [formConfig]=\"delegationFormConfig()\"\r\n [formControl]=\"delegationFormControl\"\r\n />\r\n\r\n <div class=\"mt-2 border-t border-gray-200 pt-3 flex flex-col gap-2\">\r\n <h3 class=\"text-sm font-semibold text-gray-800\">\r\n {{ t(\"scope\") }}\r\n </h3>\r\n <mt-scope-picker\r\n [(scope)]=\"scope\"\r\n [readonly]=\"readonly()\"\r\n ></mt-scope-picker>\r\n </div>\r\n </div>\r\n\r\n <div\r\n [class]=\"\r\n 'delegation-form-footer ' +\r\n modal.footerClass +\r\n ' flex-col gap-2 sm:flex-row sm:items-center sm:justify-end'\r\n \"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n />\r\n @if (!readonly()) {\r\n <mt-button\r\n [label]=\"delegationForEdit() ? t('update') : t('create')\"\r\n [loading]=\"isSaving()\"\r\n [disabled]=\"!delegationFormControl.valid || isDelegatingToSelf()\"\r\n (click)=\"onSubmit()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys"], outputs: ["runtimeMessagesChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: ScopePicker, selector: "mt-scope-picker", inputs: ["scope", "readonly", "delegatorUserId"], outputs: ["scopeChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
503
1533
|
}
|
|
504
1534
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationForm, decorators: [{
|
|
505
1535
|
type: Component,
|
|
506
1536
|
args: [{ selector: 'mt-delegation-form', standalone: true, imports: [
|
|
507
1537
|
CommonModule,
|
|
508
1538
|
Button,
|
|
509
|
-
TranslocoDirective,
|
|
510
|
-
SkeletonModule,
|
|
511
1539
|
DynamicForm,
|
|
512
1540
|
ReactiveFormsModule,
|
|
513
|
-
|
|
514
|
-
|
|
1541
|
+
ScopePicker,
|
|
1542
|
+
TranslocoDirective,
|
|
1543
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t; prefix: 'delegations'\">\r\n <div\r\n [class]=\"\r\n 'delegation-form-content flex min-w-0 flex-col gap-3 overflow-x-hidden p-4 max-[640px]:p-3 ' +\r\n modal.contentClass\r\n \"\r\n >\r\n <mt-dynamic-form\r\n [formConfig]=\"delegationFormConfig()\"\r\n [formControl]=\"delegationFormControl\"\r\n />\r\n\r\n <div class=\"mt-2 border-t border-gray-200 pt-3 flex flex-col gap-2\">\r\n <h3 class=\"text-sm font-semibold text-gray-800\">\r\n {{ t(\"scope\") }}\r\n </h3>\r\n <mt-scope-picker\r\n [(scope)]=\"scope\"\r\n [readonly]=\"readonly()\"\r\n ></mt-scope-picker>\r\n </div>\r\n </div>\r\n\r\n <div\r\n [class]=\"\r\n 'delegation-form-footer ' +\r\n modal.footerClass +\r\n ' flex-col gap-2 sm:flex-row sm:items-center sm:justify-end'\r\n \"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n />\r\n @if (!readonly()) {\r\n <mt-button\r\n [label]=\"delegationForEdit() ? t('update') : t('create')\"\r\n [loading]=\"isSaving()\"\r\n [disabled]=\"!delegationFormControl.valid || isDelegatingToSelf()\"\r\n (click)=\"onSubmit()\"\r\n styleClass=\"w-full sm:w-auto\"\r\n />\r\n }\r\n </div>\r\n</ng-container>\r\n" }]
|
|
1544
|
+
}], ctorParameters: () => [], propDecorators: { delegationForEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "delegationForEdit", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }] } });
|
|
515
1545
|
|
|
1546
|
+
/**
|
|
1547
|
+
* Read-only drawer that shows the full server DTO of a delegation, including
|
|
1548
|
+
* its status history. Triggered from the row's "View details" action.
|
|
1549
|
+
*/
|
|
1550
|
+
class DelegationDetailDrawer {
|
|
1551
|
+
/** Delegation id passed in via the modal's inputValues. */
|
|
1552
|
+
delegationId = input.required(...(ngDevMode ? [{ debugName: "delegationId" }] : /* istanbul ignore next */ []));
|
|
1553
|
+
facade = inject(DelegationsFacade);
|
|
1554
|
+
transloco = inject(TranslocoService);
|
|
1555
|
+
ref = inject(ModalRef);
|
|
1556
|
+
tabs = [
|
|
1557
|
+
{ key: 'overview', i18n: 'delegations.view-details' },
|
|
1558
|
+
{ key: 'scope', i18n: 'delegations.scope' },
|
|
1559
|
+
{ key: 'history', i18n: 'delegations.status-history' },
|
|
1560
|
+
];
|
|
1561
|
+
activeTab = signal('overview', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
1562
|
+
isLoading = this.facade.isLoadingDetail;
|
|
1563
|
+
detail = this.facade.selected;
|
|
1564
|
+
statusHistory = computed(() => this.detail()?.statusHistory ?? [], ...(ngDevMode ? [{ debugName: "statusHistory" }] : /* istanbul ignore next */ []));
|
|
1565
|
+
ngOnInit() {
|
|
1566
|
+
this.facade.getOne(this.delegationId(), true);
|
|
1567
|
+
}
|
|
1568
|
+
setTab(tab) {
|
|
1569
|
+
this.activeTab.set(tab);
|
|
1570
|
+
}
|
|
1571
|
+
approvalLabel() {
|
|
1572
|
+
const s = this.detail()?.approval?.status ?? 'NotRequired';
|
|
1573
|
+
return this.transloco.translate(`delegations.approval-${s.toLowerCase()}`);
|
|
1574
|
+
}
|
|
1575
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationDetailDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1576
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: DelegationDetailDrawer, isStandalone: true, selector: "mt-delegation-detail-drawer", inputs: { delegationId: { classPropertyName: "delegationId", publicName: "delegationId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col h-full\">\r\n <!-- Tabs -->\r\n <div class=\"flex border-b border-gray-200 px-4\">\r\n @for (tab of tabs; track tab.key) {\r\n <button\r\n type=\"button\"\r\n class=\"px-3 py-2 -mb-px border-b-2 text-sm font-medium transition-colors\"\r\n [class.border-primary]=\"activeTab() === tab.key\"\r\n [class.text-primary]=\"activeTab() === tab.key\"\r\n [class.border-transparent]=\"activeTab() !== tab.key\"\r\n [class.text-gray-500]=\"activeTab() !== tab.key\"\r\n (click)=\"setTab(tab.key)\"\r\n >\r\n {{ t(tab.i18n) }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Body -->\r\n <div class=\"flex-1 overflow-y-auto p-4\">\r\n @if (isLoading() && !detail()) {\r\n <p-skeleton height=\"2rem\" class=\"mb-3\"></p-skeleton>\r\n <p-skeleton height=\"6rem\" class=\"mb-3\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n } @else if (detail(); as d) {\r\n @switch (activeTab()) {\r\n @case ('overview') {\r\n <div class=\"flex flex-col gap-4\">\r\n <div class=\"flex items-center justify-between gap-3\">\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <mt-avatar\r\n [image]=\"d.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-12! h-12! text-lg!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegator-name\") }}\r\n </div>\r\n <div class=\"text-base font-medium text-gray-900 truncate\">\r\n {{ d.delegator.displayName }}\r\n </div>\r\n @if (d.delegator.email) {\r\n <div class=\"text-xs text-gray-500 truncate\">\r\n {{ d.delegator.email }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n <mt-delegation-status-chip\r\n [status]=\"d.status\"\r\n ></mt-delegation-status-chip>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-2 gap-4\">\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegated-to\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.delegatedUser.displayName }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.approval\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ approvalLabel() }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.start-date\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.startDateUtc | date: \"medium\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.end-date\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.endDateUtc | date: \"medium\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.time-zone\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.timeZoneId }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.days\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n @if (d.dayRules.type === \"FullRange\") {\r\n {{ t(\"delegations.all-days\") }}\r\n } @else if (d.dayRules.specificDayLabels?.length) {\r\n {{ d.dayRules.specificDayLabels?.join(\", \") }}\r\n } @else {\r\n {{ d.dayRules.specificDays.join(\", \") }}\r\n }\r\n </span>\r\n </div>\r\n </div>\r\n\r\n @if (d.description) {\r\n <div class=\"flex flex-col gap-1\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.description\") }}\r\n </span>\r\n <p class=\"text-sm text-gray-900 whitespace-pre-line\">\r\n {{ d.description }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (d.approval.rejectionReason) {\r\n <div\r\n class=\"rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800\"\r\n >\r\n <div class=\"font-medium\">\r\n {{ t(\"delegations.rejection-reason\") }}\r\n </div>\r\n <div>{{ d.approval.rejectionReason }}</div>\r\n </div>\r\n }\r\n\r\n @if (d.cancellation.cancellationReason) {\r\n <div\r\n class=\"rounded-md border border-gray-200 bg-surface-50 p-3 text-sm text-gray-800\"\r\n >\r\n <div class=\"font-medium\">\r\n {{ t(\"delegations.cancel-reason\") }}\r\n </div>\r\n <div>{{ d.cancellation.cancellationReason }}</div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('scope') {\r\n <div class=\"flex flex-col gap-3\">\r\n <div class=\"text-sm text-gray-900\">\r\n {{ d.scope.summary || t(\"delegations.scope-summary\") }}\r\n </div>\r\n\r\n @if (d.scope.accessibilities.length > 0) {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegated-to\") }}\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (a of d.scope.accessibilities; track a.accessibilityGroupId) {\r\n <span\r\n class=\"text-xs px-2 py-0.5 rounded-md bg-surface-100 text-gray-700\"\r\n >\r\n {{ a.moduleName }} / {{ a.groupName }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div class=\"flex flex-col gap-2\">\r\n @for (target of d.scope.targets; track target.displayName) {\r\n <div\r\n class=\"border border-gray-200 rounded-md px-3 py-2 flex flex-col gap-1\"\r\n >\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <span class=\"text-sm font-medium text-gray-900\">\r\n {{ target.displayName }}\r\n </span>\r\n <span class=\"text-xs text-gray-500\">\r\n {{ target.targetType }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (op of target.operations; track op.permissionCommand) {\r\n <span\r\n class=\"text-xs px-2 py-0.5 rounded-md bg-primary-50 text-primary\"\r\n >\r\n {{ op.displayName }}\r\n @if (op.isHighRisk) {\r\n <mt-icon\r\n icon=\"alert.alert-triangle\"\r\n styleClass=\"text-xs text-amber-500\"\r\n ></mt-icon>\r\n }\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (d.scope.targets.length === 0) {\r\n <p class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.no-targets-selected\") }}\r\n </p>\r\n }\r\n </div>\r\n\r\n @if (d.scope.warnings.length > 0) {\r\n <ul class=\"text-xs text-amber-700 list-disc list-inside\">\r\n @for (w of d.scope.warnings; track w.code) {\r\n <li>{{ w.message }}</li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('history') {\r\n <ol class=\"flex flex-col gap-2\">\r\n @for (entry of statusHistory(); track entry.id) {\r\n <li\r\n class=\"border border-gray-200 rounded-md px-3 py-2 flex flex-col gap-1\"\r\n >\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <span class=\"text-sm font-medium text-gray-900\">\r\n {{ entry.action }}\r\n </span>\r\n <span class=\"text-xs text-gray-500\">\r\n {{ entry.createdAtUtc | date: \"short\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex items-center gap-2 text-xs text-gray-600\">\r\n @if (entry.fromStatus) {\r\n <span>{{ entry.fromStatus }}</span>\r\n <mt-icon\r\n icon=\"arrow.arrow-right\"\r\n styleClass=\"text-xs text-gray-400\"\r\n ></mt-icon>\r\n }\r\n <span>{{ entry.toStatus }}</span>\r\n </div>\r\n <div class=\"text-xs text-gray-700\">\r\n {{ t(\"delegations.delegator-name\") }}:\r\n {{ entry.actor.displayName }}\r\n </div>\r\n @if (entry.reason) {\r\n <div class=\"text-xs text-gray-600\">\r\n {{ t(\"delegations.reason\") }}: {{ entry.reason }}\r\n </div>\r\n }\r\n </li>\r\n }\r\n @if (statusHistory().length === 0) {\r\n <li class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.status-history\") }} \u2014\r\n {{ t(\"delegations.no-operations-selected\") }}\r\n </li>\r\n }\r\n </ol>\r\n }\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div\r\n class=\"border-t border-gray-200 px-4 py-3 flex items-center justify-end\"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { 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"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DelegationStatusChip, selector: "mt-delegation-status-chip", inputs: ["status"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1577
|
+
}
|
|
1578
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationDetailDrawer, decorators: [{
|
|
1579
|
+
type: Component,
|
|
1580
|
+
args: [{ selector: 'mt-delegation-detail-drawer', standalone: true, imports: [
|
|
1581
|
+
CommonModule,
|
|
1582
|
+
Avatar,
|
|
1583
|
+
Button,
|
|
1584
|
+
Icon,
|
|
1585
|
+
SkeletonModule,
|
|
1586
|
+
TranslocoDirective,
|
|
1587
|
+
DelegationStatusChip,
|
|
1588
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col h-full\">\r\n <!-- Tabs -->\r\n <div class=\"flex border-b border-gray-200 px-4\">\r\n @for (tab of tabs; track tab.key) {\r\n <button\r\n type=\"button\"\r\n class=\"px-3 py-2 -mb-px border-b-2 text-sm font-medium transition-colors\"\r\n [class.border-primary]=\"activeTab() === tab.key\"\r\n [class.text-primary]=\"activeTab() === tab.key\"\r\n [class.border-transparent]=\"activeTab() !== tab.key\"\r\n [class.text-gray-500]=\"activeTab() !== tab.key\"\r\n (click)=\"setTab(tab.key)\"\r\n >\r\n {{ t(tab.i18n) }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Body -->\r\n <div class=\"flex-1 overflow-y-auto p-4\">\r\n @if (isLoading() && !detail()) {\r\n <p-skeleton height=\"2rem\" class=\"mb-3\"></p-skeleton>\r\n <p-skeleton height=\"6rem\" class=\"mb-3\"></p-skeleton>\r\n <p-skeleton height=\"6rem\"></p-skeleton>\r\n } @else if (detail(); as d) {\r\n @switch (activeTab()) {\r\n @case ('overview') {\r\n <div class=\"flex flex-col gap-4\">\r\n <div class=\"flex items-center justify-between gap-3\">\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <mt-avatar\r\n [image]=\"d.delegator.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-12! h-12! text-lg!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegator-name\") }}\r\n </div>\r\n <div class=\"text-base font-medium text-gray-900 truncate\">\r\n {{ d.delegator.displayName }}\r\n </div>\r\n @if (d.delegator.email) {\r\n <div class=\"text-xs text-gray-500 truncate\">\r\n {{ d.delegator.email }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n <mt-delegation-status-chip\r\n [status]=\"d.status\"\r\n ></mt-delegation-status-chip>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-2 gap-4\">\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegated-to\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.delegatedUser.displayName }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.approval\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ approvalLabel() }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.start-date\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.startDateUtc | date: \"medium\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.end-date\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.endDateUtc | date: \"medium\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.time-zone\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n {{ d.timeZoneId }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-col\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.days\") }}\r\n </span>\r\n <span class=\"text-sm text-gray-900\">\r\n @if (d.dayRules.type === \"FullRange\") {\r\n {{ t(\"delegations.all-days\") }}\r\n } @else if (d.dayRules.specificDayLabels?.length) {\r\n {{ d.dayRules.specificDayLabels?.join(\", \") }}\r\n } @else {\r\n {{ d.dayRules.specificDays.join(\", \") }}\r\n }\r\n </span>\r\n </div>\r\n </div>\r\n\r\n @if (d.description) {\r\n <div class=\"flex flex-col gap-1\">\r\n <span class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.description\") }}\r\n </span>\r\n <p class=\"text-sm text-gray-900 whitespace-pre-line\">\r\n {{ d.description }}\r\n </p>\r\n </div>\r\n }\r\n\r\n @if (d.approval.rejectionReason) {\r\n <div\r\n class=\"rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800\"\r\n >\r\n <div class=\"font-medium\">\r\n {{ t(\"delegations.rejection-reason\") }}\r\n </div>\r\n <div>{{ d.approval.rejectionReason }}</div>\r\n </div>\r\n }\r\n\r\n @if (d.cancellation.cancellationReason) {\r\n <div\r\n class=\"rounded-md border border-gray-200 bg-surface-50 p-3 text-sm text-gray-800\"\r\n >\r\n <div class=\"font-medium\">\r\n {{ t(\"delegations.cancel-reason\") }}\r\n </div>\r\n <div>{{ d.cancellation.cancellationReason }}</div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('scope') {\r\n <div class=\"flex flex-col gap-3\">\r\n <div class=\"text-sm text-gray-900\">\r\n {{ d.scope.summary || t(\"delegations.scope-summary\") }}\r\n </div>\r\n\r\n @if (d.scope.accessibilities.length > 0) {\r\n <div class=\"flex flex-col gap-1\">\r\n <div class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.delegated-to\") }}\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (a of d.scope.accessibilities; track a.accessibilityGroupId) {\r\n <span\r\n class=\"text-xs px-2 py-0.5 rounded-md bg-surface-100 text-gray-700\"\r\n >\r\n {{ a.moduleName }} / {{ a.groupName }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div class=\"flex flex-col gap-2\">\r\n @for (target of d.scope.targets; track target.displayName) {\r\n <div\r\n class=\"border border-gray-200 rounded-md px-3 py-2 flex flex-col gap-1\"\r\n >\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <span class=\"text-sm font-medium text-gray-900\">\r\n {{ target.displayName }}\r\n </span>\r\n <span class=\"text-xs text-gray-500\">\r\n {{ target.targetType }}\r\n </span>\r\n </div>\r\n <div class=\"flex flex-wrap gap-1.5\">\r\n @for (op of target.operations; track op.permissionCommand) {\r\n <span\r\n class=\"text-xs px-2 py-0.5 rounded-md bg-primary-50 text-primary\"\r\n >\r\n {{ op.displayName }}\r\n @if (op.isHighRisk) {\r\n <mt-icon\r\n icon=\"alert.alert-triangle\"\r\n styleClass=\"text-xs text-amber-500\"\r\n ></mt-icon>\r\n }\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (d.scope.targets.length === 0) {\r\n <p class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.no-targets-selected\") }}\r\n </p>\r\n }\r\n </div>\r\n\r\n @if (d.scope.warnings.length > 0) {\r\n <ul class=\"text-xs text-amber-700 list-disc list-inside\">\r\n @for (w of d.scope.warnings; track w.code) {\r\n <li>{{ w.message }}</li>\r\n }\r\n </ul>\r\n }\r\n </div>\r\n }\r\n\r\n @case ('history') {\r\n <ol class=\"flex flex-col gap-2\">\r\n @for (entry of statusHistory(); track entry.id) {\r\n <li\r\n class=\"border border-gray-200 rounded-md px-3 py-2 flex flex-col gap-1\"\r\n >\r\n <div class=\"flex items-center justify-between gap-2\">\r\n <span class=\"text-sm font-medium text-gray-900\">\r\n {{ entry.action }}\r\n </span>\r\n <span class=\"text-xs text-gray-500\">\r\n {{ entry.createdAtUtc | date: \"short\" }}\r\n </span>\r\n </div>\r\n <div class=\"flex items-center gap-2 text-xs text-gray-600\">\r\n @if (entry.fromStatus) {\r\n <span>{{ entry.fromStatus }}</span>\r\n <mt-icon\r\n icon=\"arrow.arrow-right\"\r\n styleClass=\"text-xs text-gray-400\"\r\n ></mt-icon>\r\n }\r\n <span>{{ entry.toStatus }}</span>\r\n </div>\r\n <div class=\"text-xs text-gray-700\">\r\n {{ t(\"delegations.delegator-name\") }}:\r\n {{ entry.actor.displayName }}\r\n </div>\r\n @if (entry.reason) {\r\n <div class=\"text-xs text-gray-600\">\r\n {{ t(\"delegations.reason\") }}: {{ entry.reason }}\r\n </div>\r\n }\r\n </li>\r\n }\r\n @if (statusHistory().length === 0) {\r\n <li class=\"text-xs text-gray-500\">\r\n {{ t(\"delegations.status-history\") }} \u2014\r\n {{ t(\"delegations.no-operations-selected\") }}\r\n </li>\r\n }\r\n </ol>\r\n }\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div\r\n class=\"border-t border-gray-200 px-4 py-3 flex items-center justify-end\"\r\n >\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block;height:100%}\n"] }]
|
|
1589
|
+
}], propDecorators: { delegationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "delegationId", required: true }] }] } });
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* DelegationsPage — replaces the legacy single-list CRUD view with the target
|
|
1593
|
+
* two-list portal contract: "My Delegations" (I am delegator) and "Delegated To Me".
|
|
1594
|
+
*
|
|
1595
|
+
* - Rows render the BE-returned `allowedActions` only (never inferred).
|
|
1596
|
+
* - Lifecycle actions (Approve/Reject/Cancel/Edit/StartSession) dispatch through
|
|
1597
|
+
* the facade and the row is replaced in place from the response.
|
|
1598
|
+
* - Scope/detail drawer and full scope-picker form are still incremental work;
|
|
1599
|
+
* the form drawer here covers Edit/Create with the core fields.
|
|
1600
|
+
*/
|
|
516
1601
|
class DelegationsList {
|
|
517
|
-
statusCol = viewChild.required('statusCol');
|
|
518
|
-
userCol = viewChild.required('userCol');
|
|
519
|
-
userCol2 = viewChild.required('userCol2');
|
|
520
|
-
daysCol = viewChild.required('daysCol');
|
|
521
1602
|
facade = inject(DelegationsFacade);
|
|
1603
|
+
session = inject(DelegationSessionFacade);
|
|
522
1604
|
modal = inject(ModalService);
|
|
523
|
-
|
|
1605
|
+
transloco = inject(TranslocoService);
|
|
1606
|
+
// ---- Page header (unchanged from legacy admin breadcrumbs) ----
|
|
524
1607
|
breadcrumbItems = linkedSignal(() => [
|
|
525
1608
|
{
|
|
526
1609
|
label: '',
|
|
@@ -528,147 +1611,143 @@ class DelegationsList {
|
|
|
528
1611
|
routerLink: '/control-panel/workspaces',
|
|
529
1612
|
},
|
|
530
1613
|
{
|
|
531
|
-
label: this.
|
|
1614
|
+
label: this.transloco.translate('product-settings.product-settings'),
|
|
532
1615
|
routerLink: '/control-panel/product-settings',
|
|
533
1616
|
},
|
|
534
1617
|
{
|
|
535
|
-
label: this.
|
|
1618
|
+
label: this.transloco.translate('delegations.delegations'),
|
|
536
1619
|
},
|
|
537
1620
|
], ...(ngDevMode ? [{ debugName: "breadcrumbItems" }] : /* istanbul ignore next */ []));
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const all = this.allDelegations();
|
|
541
|
-
const activeDelegations = this.activeDelegations();
|
|
542
|
-
const inactiveDelegations = this.inactiveDelegations();
|
|
543
|
-
switch (tab) {
|
|
544
|
-
case 'active':
|
|
545
|
-
return activeDelegations;
|
|
546
|
-
case 'inactive':
|
|
547
|
-
return inactiveDelegations;
|
|
548
|
-
case 'all':
|
|
549
|
-
default:
|
|
550
|
-
return all;
|
|
551
|
-
}
|
|
552
|
-
}, ...(ngDevMode ? [{ debugName: "delegations" }] : /* istanbul ignore next */ []));
|
|
1621
|
+
// ---- Tabs ----
|
|
1622
|
+
activeTab = signal('my', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
|
|
553
1623
|
tabs = signal([
|
|
554
1624
|
{
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
label: this.translocoService.translate('delegations.active'),
|
|
560
|
-
value: 'active',
|
|
1625
|
+
value: 'my',
|
|
1626
|
+
label: this.transloco.translate('delegations.my-delegations'),
|
|
561
1627
|
},
|
|
562
1628
|
{
|
|
563
|
-
|
|
564
|
-
|
|
1629
|
+
value: 'assigned',
|
|
1630
|
+
label: this.transloco.translate('delegations.delegated-to-me'),
|
|
565
1631
|
},
|
|
566
1632
|
], ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
|
|
567
|
-
|
|
1633
|
+
// ---- Loading / data ----
|
|
1634
|
+
isLoading = computed(() => this.activeTab() === 'my'
|
|
1635
|
+
? this.facade.isLoadingMy()
|
|
1636
|
+
: this.facade.isLoadingAssigned(), ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
|
|
1637
|
+
rows = computed(() => this.activeTab() === 'my'
|
|
1638
|
+
? this.facade.myItems()
|
|
1639
|
+
: this.facade.assignedItems(), ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
1640
|
+
// ---- Column templates ----
|
|
1641
|
+
statusCol = viewChild.required('statusCol');
|
|
1642
|
+
userCol = viewChild.required('userCol');
|
|
1643
|
+
scopeCol = viewChild.required('scopeCol');
|
|
1644
|
+
daysCol = viewChild.required('daysCol');
|
|
1645
|
+
// ---- Table actions (header) ----
|
|
568
1646
|
tableActions = signal([
|
|
569
1647
|
{
|
|
570
1648
|
icon: 'general.plus',
|
|
571
|
-
label: this.
|
|
1649
|
+
label: this.transloco.translate('delegations.add-delegation'),
|
|
572
1650
|
color: 'primary',
|
|
573
|
-
action: () =>
|
|
574
|
-
this.addDelegationDialog();
|
|
575
|
-
},
|
|
1651
|
+
action: () => this.openForm(null),
|
|
576
1652
|
},
|
|
577
1653
|
], ...(ngDevMode ? [{ debugName: "tableActions" }] : /* istanbul ignore next */ []));
|
|
578
|
-
|
|
1654
|
+
// ---- Per-row action: read ONLY allowedActions from BE ----
|
|
579
1655
|
rowActions = signal([
|
|
1656
|
+
{
|
|
1657
|
+
icon: 'general.eye',
|
|
1658
|
+
tooltip: this.transloco.translate('delegations.view-details'),
|
|
1659
|
+
color: 'primary',
|
|
1660
|
+
variant: 'outlined',
|
|
1661
|
+
action: (row) => this.viewDetails(row),
|
|
1662
|
+
hidden: (row) => !this.has(row, 'View'),
|
|
1663
|
+
},
|
|
580
1664
|
{
|
|
581
1665
|
icon: 'custom.pencil',
|
|
582
|
-
tooltip: this.
|
|
1666
|
+
tooltip: this.transloco.translate('edit'),
|
|
583
1667
|
color: 'primary',
|
|
584
|
-
action: (row) =>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1668
|
+
action: (row) => this.openForm(row),
|
|
1669
|
+
hidden: (row) => !this.has(row, 'Edit'),
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
icon: 'general.check',
|
|
1673
|
+
tooltip: this.transloco.translate('delegations.approve'),
|
|
1674
|
+
color: 'primary',
|
|
1675
|
+
action: (row) => this.approve(row),
|
|
1676
|
+
hidden: (row) => !this.has(row, 'Approve'),
|
|
1677
|
+
loading: (row) => this.busyIds().includes(`approve:${row.id}`),
|
|
1678
|
+
confirmation: { type: 'popup' },
|
|
588
1679
|
},
|
|
589
1680
|
{
|
|
590
|
-
icon: 'general.
|
|
591
|
-
tooltip: this.
|
|
1681
|
+
icon: 'general.x-close',
|
|
1682
|
+
tooltip: this.transloco.translate('delegations.reject'),
|
|
592
1683
|
color: 'danger',
|
|
593
1684
|
variant: 'outlined',
|
|
594
|
-
action: (row) =>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1685
|
+
action: (row) => this.reject(row),
|
|
1686
|
+
hidden: (row) => !this.has(row, 'Reject'),
|
|
1687
|
+
loading: (row) => this.busyIds().includes(`reject:${row.id}`),
|
|
1688
|
+
confirmation: { type: 'popup' },
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
icon: 'general.x-close',
|
|
1692
|
+
tooltip: this.transloco.translate('delegations.cancel-delegation'),
|
|
1693
|
+
color: 'danger',
|
|
1694
|
+
variant: 'outlined',
|
|
1695
|
+
action: (row) => this.cancel(row),
|
|
1696
|
+
hidden: (row) => !this.has(row, 'Cancel') ||
|
|
1697
|
+
// If row also has Approve/Reject, the cancel for a delegate is "withdraw";
|
|
1698
|
+
// the assertion stays — both can render and BE decides outcome.
|
|
1699
|
+
false,
|
|
1700
|
+
loading: (row) => this.busyIds().includes(`cancel:${row.id}`),
|
|
1701
|
+
confirmation: { type: 'popup' },
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
icon: 'arrow.arrow-right',
|
|
1705
|
+
tooltip: this.transloco.translate('delegations.start-session'),
|
|
1706
|
+
color: 'primary',
|
|
1707
|
+
action: (row) => this.startSession(row),
|
|
1708
|
+
hidden: (row) => !this.has(row, 'StartSession'),
|
|
608
1709
|
},
|
|
609
1710
|
], ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
|
|
610
1711
|
tableColumns = linkedSignal(() => [
|
|
611
1712
|
{
|
|
612
|
-
key: '
|
|
613
|
-
label: this.
|
|
1713
|
+
key: 'status',
|
|
1714
|
+
label: this.transloco.translate('delegations.status'),
|
|
614
1715
|
type: 'custom',
|
|
615
1716
|
customCellTpl: this.statusCol(),
|
|
616
|
-
filterConfig: {
|
|
617
|
-
type: 'select',
|
|
618
|
-
label: this.translocoService.translate('delegations.status'),
|
|
619
|
-
options: [
|
|
620
|
-
{
|
|
621
|
-
label: this.translocoService.translate('delegations.active'),
|
|
622
|
-
value: true,
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
label: this.translocoService.translate('delegations.inactive'),
|
|
626
|
-
value: false,
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
},
|
|
630
1717
|
},
|
|
631
1718
|
{
|
|
632
|
-
key:
|
|
633
|
-
label: this.
|
|
1719
|
+
key: this.activeTab() === 'my' ? 'delegatedUser' : 'delegator',
|
|
1720
|
+
label: this.transloco.translate(this.activeTab() === 'my'
|
|
1721
|
+
? 'delegations.delegated-to'
|
|
1722
|
+
: 'delegations.delegator-name'),
|
|
634
1723
|
type: 'custom',
|
|
635
1724
|
customCellTpl: this.userCol(),
|
|
636
1725
|
},
|
|
637
1726
|
{
|
|
638
|
-
key: '
|
|
639
|
-
label: this.
|
|
1727
|
+
key: 'scope',
|
|
1728
|
+
label: this.transloco.translate('delegations.scope'),
|
|
640
1729
|
type: 'custom',
|
|
641
|
-
customCellTpl: this.
|
|
1730
|
+
customCellTpl: this.scopeCol(),
|
|
642
1731
|
},
|
|
643
1732
|
{
|
|
644
|
-
key: '
|
|
645
|
-
label: this.
|
|
1733
|
+
key: 'startDateUtc',
|
|
1734
|
+
label: this.transloco.translate('delegations.start-date'),
|
|
646
1735
|
type: 'dateTime',
|
|
647
|
-
filterConfig: {
|
|
648
|
-
type: 'date',
|
|
649
|
-
label: this.translocoService.translate('delegations.start-date'),
|
|
650
|
-
},
|
|
651
1736
|
},
|
|
652
1737
|
{
|
|
653
|
-
key: '
|
|
654
|
-
label: this.
|
|
1738
|
+
key: 'endDateUtc',
|
|
1739
|
+
label: this.transloco.translate('delegations.end-date'),
|
|
655
1740
|
type: 'dateTime',
|
|
656
|
-
filterConfig: {
|
|
657
|
-
type: 'date',
|
|
658
|
-
label: this.translocoService.translate('delegations.end-date'),
|
|
659
|
-
},
|
|
660
1741
|
},
|
|
661
1742
|
{
|
|
662
|
-
key: '
|
|
663
|
-
label: this.
|
|
1743
|
+
key: 'dayRules',
|
|
1744
|
+
label: this.transloco.translate('delegations.days'),
|
|
664
1745
|
type: 'custom',
|
|
665
1746
|
customCellTpl: this.daysCol(),
|
|
666
1747
|
},
|
|
667
1748
|
], ...(ngDevMode ? [{ debugName: "tableColumns" }] : /* istanbul ignore next */ []));
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
activeDelegations = this.facade.activeDelegations;
|
|
671
|
-
inactiveDelegations = this.facade.inactiveDelegations;
|
|
1749
|
+
// ---- per-row "busy" tracking (no row-version conflict UX yet — toast suffices) ----
|
|
1750
|
+
busyIds = signal([], ...(ngDevMode ? [{ debugName: "busyIds" }] : /* istanbul ignore next */ []));
|
|
672
1751
|
weekdayKeys = [
|
|
673
1752
|
'sunday',
|
|
674
1753
|
'monday',
|
|
@@ -678,59 +1757,170 @@ class DelegationsList {
|
|
|
678
1757
|
'friday',
|
|
679
1758
|
'saturday',
|
|
680
1759
|
];
|
|
1760
|
+
// ---------------------------------------------------------------------------
|
|
1761
|
+
// Init / tab change
|
|
1762
|
+
// ---------------------------------------------------------------------------
|
|
681
1763
|
ngOnInit() {
|
|
682
|
-
this.
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1764
|
+
this.loadCurrentTab();
|
|
1765
|
+
}
|
|
1766
|
+
switchTab(tab) {
|
|
1767
|
+
this.activeTab.set(tab);
|
|
1768
|
+
this.loadCurrentTab();
|
|
1769
|
+
}
|
|
1770
|
+
loadCurrentTab(query = { page: 1, pageSize: 20 }) {
|
|
1771
|
+
if (this.activeTab() === 'my') {
|
|
1772
|
+
this.facade.getMy(query);
|
|
1773
|
+
}
|
|
1774
|
+
else {
|
|
1775
|
+
this.facade.getAssigned(query);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
// ---------------------------------------------------------------------------
|
|
1779
|
+
// Helpers
|
|
1780
|
+
// ---------------------------------------------------------------------------
|
|
1781
|
+
has(row, action) {
|
|
1782
|
+
return row.allowedActions?.includes(action) ?? false;
|
|
1783
|
+
}
|
|
1784
|
+
formatDays(row) {
|
|
1785
|
+
if (row.dayRules?.type === 'SpecificDays' &&
|
|
1786
|
+
row.dayRules.specificDays?.length) {
|
|
1787
|
+
return row.dayRules.specificDays
|
|
1788
|
+
.filter((d) => d >= 0 && d < this.weekdayKeys.length)
|
|
1789
|
+
.map((d) => this.transloco.translate(`delegations.${this.weekdayKeys[d]}`))
|
|
690
1790
|
.join(', ');
|
|
691
1791
|
}
|
|
692
|
-
return this.
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1792
|
+
return this.transloco.translate('delegations.all-days');
|
|
1793
|
+
}
|
|
1794
|
+
// ---------------------------------------------------------------------------
|
|
1795
|
+
// Lifecycle actions
|
|
1796
|
+
// ---------------------------------------------------------------------------
|
|
1797
|
+
viewDetails(row) {
|
|
1798
|
+
this.modal.openModal(DelegationDetailDrawer, 'drawer', {
|
|
1799
|
+
header: this.transloco.translate('delegations.view-details'),
|
|
1800
|
+
styleClass: '!absolute !shadow-none !w-full !max-w-full sm:!w-[42rem] xl:!w-[50rem]',
|
|
1801
|
+
position: 'end',
|
|
1802
|
+
appendTo: 'page-content',
|
|
1803
|
+
dismissableMask: true,
|
|
1804
|
+
dismissible: true,
|
|
1805
|
+
inputValues: { delegationId: row.id },
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
openForm(row, readonly = false) {
|
|
1809
|
+
const isDrawer = row ? 'drawer' : 'dialog';
|
|
1810
|
+
this.modal.openModal(DelegationForm, isDrawer, {
|
|
1811
|
+
header: this.transloco.translate(row ? 'delegations.edit-delegation' : 'delegations.create-delegation'),
|
|
1812
|
+
styleClass: isDrawer === 'drawer'
|
|
1813
|
+
? '!absolute !shadow-none !w-full !max-w-full sm:!w-[42rem] xl:!w-[50rem]'
|
|
1814
|
+
: '!w-[min(96vw,50rem)] !max-w-[96vw] !h-[min(90vh,40rem)] !max-h-[90vh]',
|
|
1815
|
+
position: isDrawer === 'drawer' ? 'end' : '',
|
|
1816
|
+
appendTo: isDrawer === 'drawer' ? 'page-content' : 'body',
|
|
1817
|
+
dismissableMask: true,
|
|
1818
|
+
dismissible: true,
|
|
1819
|
+
inputValues: {
|
|
1820
|
+
delegationForEdit: row,
|
|
1821
|
+
readonly,
|
|
1822
|
+
},
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
approve(row) {
|
|
1826
|
+
this.mark(`approve:${row.id}`, true);
|
|
1827
|
+
this.facade
|
|
1828
|
+
.approve(row.id, { rowVersion: row.rowVersion })
|
|
1829
|
+
.pipe(finalize(() => this.mark(`approve:${row.id}`, false)))
|
|
1830
|
+
.subscribe();
|
|
1831
|
+
}
|
|
1832
|
+
reject(row) {
|
|
1833
|
+
this.mark(`reject:${row.id}`, true);
|
|
1834
|
+
this.facade
|
|
1835
|
+
.reject(row.id, {
|
|
1836
|
+
rowVersion: row.rowVersion,
|
|
1837
|
+
reason: this.transloco.translate('delegations.reject'),
|
|
1838
|
+
})
|
|
1839
|
+
.pipe(finalize(() => this.mark(`reject:${row.id}`, false)))
|
|
1840
|
+
.subscribe();
|
|
1841
|
+
}
|
|
1842
|
+
cancel(row) {
|
|
1843
|
+
this.mark(`cancel:${row.id}`, true);
|
|
1844
|
+
this.facade
|
|
1845
|
+
.cancel(row.id, {
|
|
1846
|
+
rowVersion: row.rowVersion,
|
|
1847
|
+
reason: this.transloco.translate('delegations.cancel-delegation'),
|
|
1848
|
+
})
|
|
1849
|
+
.pipe(finalize(() => this.mark(`cancel:${row.id}`, false)))
|
|
1850
|
+
.subscribe();
|
|
1851
|
+
}
|
|
1852
|
+
startSession(row) {
|
|
1853
|
+
this.modal.openModal(StartSessionDialog, 'dialog', {
|
|
1854
|
+
header: this.transloco.translate('delegations.start-session'),
|
|
1855
|
+
styleClass: '!w-[min(96vw,28rem)] !max-w-[96vw]',
|
|
706
1856
|
dismissableMask: true,
|
|
707
1857
|
dismissible: true,
|
|
708
1858
|
inputValues: {
|
|
709
|
-
|
|
1859
|
+
candidate: {
|
|
1860
|
+
id: row.id,
|
|
1861
|
+
delegator: row.delegator,
|
|
1862
|
+
delegatedUser: row.delegatedUser,
|
|
1863
|
+
status: row.status,
|
|
1864
|
+
allowedActions: row.allowedActions,
|
|
1865
|
+
startDateUtc: row.startDateUtc,
|
|
1866
|
+
endDateUtc: row.endDateUtc,
|
|
1867
|
+
scopeSummary: row.scope?.summary,
|
|
1868
|
+
},
|
|
1869
|
+
intent: 'start',
|
|
710
1870
|
},
|
|
711
1871
|
});
|
|
712
1872
|
}
|
|
1873
|
+
mark(key, on) {
|
|
1874
|
+
this.busyIds.update((ids) => on ? [...ids, key] : ids.filter((id) => id !== key));
|
|
1875
|
+
}
|
|
713
1876
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
714
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.
|
|
1877
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: DelegationsList, isStandalone: true, selector: "mt-delegations-list", viewQueries: [{ propertyName: "statusCol", first: true, predicate: ["statusCol"], descendants: true, isSignal: true }, { propertyName: "userCol", first: true, predicate: ["userCol"], descendants: true, isSignal: true }, { propertyName: "scopeCol", first: true, predicate: ["scopeCol"], descendants: true, isSignal: true }, { propertyName: "daysCol", first: true, predicate: ["daysCol"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col h-full gap-2 p-4\">\r\n <mt-breadcrumb [items]=\"breadcrumbItems()\"></mt-breadcrumb>\r\n\r\n <div class=\"flex items-center gap-2 border-b border-gray-200\">\r\n @for (tab of tabs(); track tab.value) {\r\n <button\r\n type=\"button\"\r\n class=\"px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors\"\r\n [class.border-primary]=\"activeTab() === tab.value\"\r\n [class.text-primary]=\"activeTab() === tab.value\"\r\n [class.border-transparent]=\"activeTab() !== tab.value\"\r\n [class.text-gray-500]=\"activeTab() !== tab.value\"\r\n (click)=\"switchTab(tab.value)\"\r\n >\r\n {{ tab.label }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"tableColumns()\"\r\n [actions]=\"tableActions()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"isLoading()\"\r\n >\r\n <ng-template #statusCol let-row>\r\n <mt-delegation-status-chip\r\n [status]=\"row.status\"\r\n ></mt-delegation-status-chip>\r\n </ng-template>\r\n\r\n <ng-template #userCol let-row>\r\n @let user = activeTab() === \"my\" ? row.delegatedUser : row.delegator;\r\n <div class=\"flex items-center gap-2\">\r\n <mt-avatar\r\n [image]=\"user?.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-8! h-8!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ user?.displayName }}\r\n </span>\r\n @if (user?.email) {\r\n <span class=\"text-xs text-gray-500 truncate\">\r\n {{ user.email }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #scopeCol let-row>\r\n <span class=\"text-sm text-gray-700\" [title]=\"row.scope?.summary\">\r\n {{ row.scope?.summary || \"\u2014\" }}\r\n </span>\r\n </ng-template>\r\n\r\n <ng-template #daysCol let-row>\r\n <span class=\"text-sm text-gray-700\">{{ formatDays(row) }}</span>\r\n </ng-template>\r\n </mt-table>\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block;min-width:0}:host ::ng-deep mt-table .p-datatable-header>div,:host ::ng-deep mt-table .p-datatable-header>div>div{gap:.75rem;min-width:0;flex-wrap:wrap}:host ::ng-deep mt-table .p-datatable-header mt-tabs{display:block;max-width:100%;overflow-x:auto;padding-bottom:.25rem}:host ::ng-deep mt-table .p-datatable-header mt-text-field{display:block;flex:1 1 16rem;min-width:min(100%,16rem)}:host ::ng-deep mt-table .p-datatable-header .p-inputtext,:host ::ng-deep mt-table .p-datatable-header .p-inputwrapper,:host ::ng-deep mt-table .p-datatable-header .p-selectbutton{width:100%;max-width:100%}@media(max-width:1024px){:host ::ng-deep mt-table .p-datatable-header>div{flex-direction:column;align-items:stretch}:host ::ng-deep mt-table .p-datatable-header>div>div{width:100%;justify-content:flex-start}:host ::ng-deep mt-table .p-datatable-header mt-text-field{flex-basis:100%;min-width:0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { kind: "component", type: Breadcrumb, selector: "mt-breadcrumb", inputs: ["items", "styleClass"], outputs: ["onItemClick"] }, { 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", "exportable", "printable", "groupable", "cellClickFilter", "freezeActions", "printTitle", "exportFilename", "actionShape", "rowActionsLoadingFn", "tableLayout", "noCard", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "paginatorPosition", "alwaysShowPaginator", "rowsPerPageOptions", "pageSize", "currentPage", "first", "filterTerm", "groupBy"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "rowClick", "rowActionsRequested", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange", "groupByChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: DelegationStatusChip, selector: "mt-delegation-status-chip", inputs: ["status"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
715
1878
|
}
|
|
716
1879
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: DelegationsList, decorators: [{
|
|
717
1880
|
type: Component,
|
|
718
1881
|
args: [{ selector: 'mt-delegations-list', standalone: true, imports: [
|
|
719
1882
|
CommonModule,
|
|
720
|
-
SkeletonModule,
|
|
721
|
-
Table,
|
|
722
1883
|
Avatar,
|
|
723
|
-
TranslocoDirective,
|
|
724
1884
|
Breadcrumb,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1885
|
+
Table,
|
|
1886
|
+
TranslocoDirective,
|
|
1887
|
+
DelegationStatusChip,
|
|
1888
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *transloco=\"let t\">\r\n <div class=\"flex flex-col h-full gap-2 p-4\">\r\n <mt-breadcrumb [items]=\"breadcrumbItems()\"></mt-breadcrumb>\r\n\r\n <div class=\"flex items-center gap-2 border-b border-gray-200\">\r\n @for (tab of tabs(); track tab.value) {\r\n <button\r\n type=\"button\"\r\n class=\"px-4 py-2 -mb-px border-b-2 text-sm font-medium transition-colors\"\r\n [class.border-primary]=\"activeTab() === tab.value\"\r\n [class.text-primary]=\"activeTab() === tab.value\"\r\n [class.border-transparent]=\"activeTab() !== tab.value\"\r\n [class.text-gray-500]=\"activeTab() !== tab.value\"\r\n (click)=\"switchTab(tab.value)\"\r\n >\r\n {{ tab.label }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <mt-table\r\n [data]=\"rows()\"\r\n [columns]=\"tableColumns()\"\r\n [actions]=\"tableActions()\"\r\n [rowActions]=\"rowActions()\"\r\n [loading]=\"isLoading()\"\r\n >\r\n <ng-template #statusCol let-row>\r\n <mt-delegation-status-chip\r\n [status]=\"row.status\"\r\n ></mt-delegation-status-chip>\r\n </ng-template>\r\n\r\n <ng-template #userCol let-row>\r\n @let user = activeTab() === \"my\" ? row.delegatedUser : row.delegator;\r\n <div class=\"flex items-center gap-2\">\r\n <mt-avatar\r\n [image]=\"user?.photo || ''\"\r\n icon=\"user.user-01\"\r\n styleClass=\"w-8! h-8!\"\r\n ></mt-avatar>\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"text-sm text-gray-900 truncate\">\r\n {{ user?.displayName }}\r\n </span>\r\n @if (user?.email) {\r\n <span class=\"text-xs text-gray-500 truncate\">\r\n {{ user.email }}\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </ng-template>\r\n\r\n <ng-template #scopeCol let-row>\r\n <span class=\"text-sm text-gray-700\" [title]=\"row.scope?.summary\">\r\n {{ row.scope?.summary || \"\u2014\" }}\r\n </span>\r\n </ng-template>\r\n\r\n <ng-template #daysCol let-row>\r\n <span class=\"text-sm text-gray-700\">{{ formatDays(row) }}</span>\r\n </ng-template>\r\n </mt-table>\r\n </div>\r\n</ng-container>\r\n", styles: [":host{display:block;min-width:0}:host ::ng-deep mt-table .p-datatable-header>div,:host ::ng-deep mt-table .p-datatable-header>div>div{gap:.75rem;min-width:0;flex-wrap:wrap}:host ::ng-deep mt-table .p-datatable-header mt-tabs{display:block;max-width:100%;overflow-x:auto;padding-bottom:.25rem}:host ::ng-deep mt-table .p-datatable-header mt-text-field{display:block;flex:1 1 16rem;min-width:min(100%,16rem)}:host ::ng-deep mt-table .p-datatable-header .p-inputtext,:host ::ng-deep mt-table .p-datatable-header .p-inputwrapper,:host ::ng-deep mt-table .p-datatable-header .p-selectbutton{width:100%;max-width:100%}@media(max-width:1024px){:host ::ng-deep mt-table .p-datatable-header>div{flex-direction:column;align-items:stretch}:host ::ng-deep mt-table .p-datatable-header>div>div{width:100%;justify-content:flex-start}:host ::ng-deep mt-table .p-datatable-header mt-text-field{flex-basis:100%;min-width:0}}\n"] }]
|
|
1889
|
+
}], propDecorators: { statusCol: [{ type: i0.ViewChild, args: ['statusCol', { isSignal: true }] }], userCol: [{ type: i0.ViewChild, args: ['userCol', { isSignal: true }] }], scopeCol: [{ type: i0.ViewChild, args: ['scopeCol', { isSignal: true }] }], daysCol: [{ type: i0.ViewChild, args: ['daysCol', { isSignal: true }] }] } });
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Pattern for endpoints that should NEVER receive the `app-delegation` header.
|
|
1893
|
+
* Token issuance, refresh, end-session and session-read calls use only `Authorization`
|
|
1894
|
+
* (architecture doc §9.1).
|
|
1895
|
+
*/
|
|
1896
|
+
const EXCLUDE = /\bidentity\/delegations\/(?:[^/]+\/)?(?:token|session)(?:\/|$)/i;
|
|
1897
|
+
/**
|
|
1898
|
+
* Adds `app-delegation: Bearer <token>` to outgoing requests when a delegation
|
|
1899
|
+
* session is active. Token-management endpoints are excluded.
|
|
1900
|
+
*
|
|
1901
|
+
* Register AFTER the gateway-auth interceptor (so `Authorization` is set first)
|
|
1902
|
+
* and BEFORE the message interceptor (so 403s still flow through toast handling).
|
|
1903
|
+
*/
|
|
1904
|
+
const appDelegationInterceptor = (req, next) => {
|
|
1905
|
+
if (EXCLUDE.test(req.url)) {
|
|
1906
|
+
return next(req);
|
|
1907
|
+
}
|
|
1908
|
+
const token = inject(DelegationSessionFacade).accessToken();
|
|
1909
|
+
if (!token) {
|
|
1910
|
+
return next(req);
|
|
1911
|
+
}
|
|
1912
|
+
return next(req.clone({
|
|
1913
|
+
setHeaders: { 'app-delegation': `Bearer ${token}` },
|
|
1914
|
+
}));
|
|
1915
|
+
};
|
|
728
1916
|
|
|
729
1917
|
// store/index.ts
|
|
730
1918
|
|
|
1919
|
+
// Runtime UI surfaces — projected into host topbars + opened via ModalService.
|
|
1920
|
+
|
|
731
1921
|
/**
|
|
732
1922
|
* Generated bundle index. Do not edit.
|
|
733
1923
|
*/
|
|
734
1924
|
|
|
735
|
-
export {
|
|
1925
|
+
export { ApproveDelegation, BootstrapDelegationSession, CancelDelegation, ClearScopePreview, ClearSelectedDelegation, CreateDelegation, DELEGATION_SESSION_STORAGE_KEY, DelegationDetailDrawer, DelegationForm, DelegationSessionActionKey, DelegationSessionFacade, DelegationSessionInvalidated, DelegationSessionState, DelegationStatusChip, Delegations, DelegationsActionKey, DelegationsFacade, DelegationsList, DelegationsState, EndDelegationSession, GetAssignedDelegations, GetDelegation, GetMyDelegations, GetScopeOptions, PreviewScope, RefreshDelegationSession, RejectDelegation, ScopePicker, SetDelegationCandidates, StartDelegationSession, StartSessionDialog, SwitchDelegationSession, TopbarDelegationMenu, UpdateDelegation, VerifyDelegationSession, appDelegationInterceptor, pickCandidates, toActiveSession };
|
|
736
1926
|
//# sourceMappingURL=masterteam-delegations.mjs.map
|