@flusys/ng-iam 1.1.1-beta → 1.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -1027
- package/fesm2022/flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs +502 -0
- package/fesm2022/flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-UGKclpFx.mjs +348 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-UGKclpFx.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs +4561 -0
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-D0m8u6nu.mjs +105 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-D0m8u6nu.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-x88X4AgJ.mjs +143 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-x88X4AgJ.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-role-form-page.component-InsxdriD.mjs +335 -0
- package/fesm2022/flusys-ng-iam-role-form-page.component-InsxdriD.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs +389 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam.mjs +1 -1
- package/package.json +11 -10
- package/types/flusys-ng-iam.d.ts +107 -686
- package/fesm2022/flusys-ng-iam-action-form-page.component-C1j10Qhw.mjs +0 -467
- package/fesm2022/flusys-ng-iam-action-form-page.component-C1j10Qhw.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-action-list-page.component-BCzSardO.mjs +0 -281
- package/fesm2022/flusys-ng-iam-action-list-page.component-BCzSardO.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-DISrddPh.mjs +0 -4940
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-DISrddPh.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-iam-container.component-BkhqmzLi.mjs +0 -97
- package/fesm2022/flusys-ng-iam-iam-container.component-BkhqmzLi.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-permission-page.component-BSQFPt_N.mjs +0 -143
- package/fesm2022/flusys-ng-iam-permission-page.component-BSQFPt_N.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-form-page.component-Cqziu_BM.mjs +0 -314
- package/fesm2022/flusys-ng-iam-role-form-page.component-Cqziu_BM.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-list-page.component-BObCxHiB.mjs +0 -266
- package/fesm2022/flusys-ng-iam-role-list-page.component-BObCxHiB.mjs.map +0 -1
|
@@ -0,0 +1,4561 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, signal, input, output, effect, Component, computed, DestroyRef } from '@angular/core';
|
|
3
|
+
import { ApiResourceService, TranslateService, PermissionValidatorService, AngularModule, TranslatePipe, ROLE_ACTION_PERMISSIONS, HasPermissionDirective, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, USER_ROLE_PERMISSIONS, USER_PERMISSION_PROVIDER, UserSelectComponent, USER_ACTION_PERMISSIONS, PROFILE_PERMISSION_PROVIDER, permissionGuard, ACTION_PERMISSIONS, ROLE_PERMISSIONS, anyPermissionGuard, resolveTranslationModule, SHARED_MESSAGES } from '@flusys/ng-shared';
|
|
4
|
+
import { APP_CONFIG, getServiceUrl, BaseApiService, isCompanyFeatureEnabled } from '@flusys/ng-core';
|
|
5
|
+
import * as i2 from '@flusys/ng-ui';
|
|
6
|
+
import { ConfirmationService, MessageService, UiModule } from '@flusys/ng-ui';
|
|
7
|
+
import { HttpClient } from '@angular/common/http';
|
|
8
|
+
import { of, firstValueFrom, map as map$1 } from 'rxjs';
|
|
9
|
+
import { tap, catchError, map } from 'rxjs/operators';
|
|
10
|
+
import * as i1 from '@angular/common';
|
|
11
|
+
import { CommonModule } from '@angular/common';
|
|
12
|
+
import * as i1$1 from '@angular/forms';
|
|
13
|
+
import { FormsModule } from '@angular/forms';
|
|
14
|
+
import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
|
|
15
|
+
|
|
16
|
+
/** Must match backend ActionType enum */
|
|
17
|
+
var ActionType;
|
|
18
|
+
(function (ActionType) {
|
|
19
|
+
ActionType["BACKEND"] = "backend";
|
|
20
|
+
ActionType["FRONTEND"] = "frontend";
|
|
21
|
+
ActionType["BOTH"] = "both";
|
|
22
|
+
})(ActionType || (ActionType = {}));
|
|
23
|
+
|
|
24
|
+
// Company interfaces
|
|
25
|
+
|
|
26
|
+
/** Maximum items for dropdown lists (companies, roles, users, branches) */
|
|
27
|
+
const MAX_DROPDOWN_ITEMS = 100;
|
|
28
|
+
|
|
29
|
+
const IAM_MESSAGES = {
|
|
30
|
+
// Action CRUD (API keys)
|
|
31
|
+
'action.create.success': 'Action created successfully',
|
|
32
|
+
'action.create.many.success': '{{count}} actions created successfully',
|
|
33
|
+
'action.get.success': 'Action retrieved successfully',
|
|
34
|
+
'action.get.by.ids.success': 'Actions retrieved successfully by Ids',
|
|
35
|
+
'action.get.by.filter.success': 'Actions retrieved successfully by filter',
|
|
36
|
+
'action.get.all.success': 'Actions retrieved successfully',
|
|
37
|
+
'action.update.success': 'Action updated successfully',
|
|
38
|
+
'action.update.many.success': '{{count}} actions updated successfully',
|
|
39
|
+
'action.delete.success': 'Action deleted successfully',
|
|
40
|
+
'action.restore.success': 'Action restored successfully',
|
|
41
|
+
// Role CRUD (API keys)
|
|
42
|
+
'role.create.success': 'Role created successfully',
|
|
43
|
+
'role.create.many.success': '{{count}} roles created successfully',
|
|
44
|
+
'role.get.success': 'Role retrieved successfully',
|
|
45
|
+
'role.get.by.ids.success': 'Roles retrieved successfully by Ids',
|
|
46
|
+
'role.get.by.filter.success': 'Roles retrieved successfully by filter',
|
|
47
|
+
'role.get.all.success': 'Roles retrieved successfully',
|
|
48
|
+
'role.update.success': 'Role updated successfully',
|
|
49
|
+
'role.update.many.success': '{{count}} roles updated successfully',
|
|
50
|
+
'role.delete.success': 'Role deleted successfully',
|
|
51
|
+
'role.restore.success': 'Role restored successfully',
|
|
52
|
+
// PERMISSION_OPERATION_MESSAGES (API keys)
|
|
53
|
+
'permission.process.success': 'Successfully processed {{total}} items: {{added}} added, {{removed}} removed',
|
|
54
|
+
'permission.user.required': 'User is required for {{method}}',
|
|
55
|
+
'permission.already.exists': 'Permission already exists',
|
|
56
|
+
// ROLE_PERMISSION_MESSAGES (API keys)
|
|
57
|
+
'role.permission.actions.success': 'Role actions retrieved successfully',
|
|
58
|
+
'role.permission.user.roles.success': 'User roles retrieved successfully',
|
|
59
|
+
// USER_ACTION_PERMISSION_MESSAGES (API keys)
|
|
60
|
+
'user.action.permission.get.success': 'User action permissions retrieved successfully',
|
|
61
|
+
// COMPANY_ACTION_PERMISSION_MESSAGES (API keys)
|
|
62
|
+
'company.action.permission.get.success': 'Company action permissions retrieved successfully',
|
|
63
|
+
// MY_PERMISSION_MESSAGES (API keys)
|
|
64
|
+
'my.permission.get.success': 'Permissions loaded successfully',
|
|
65
|
+
// IAM_MODE_MESSAGES (API keys)
|
|
66
|
+
'iam.direct.mode.unavailable': 'Direct permission assignment not available in RBAC-only mode',
|
|
67
|
+
'iam.rbac.mode.unavailable': 'Role-based permission assignment not available in direct-only mode',
|
|
68
|
+
'iam.role.assignment.unavailable': 'Role assignment not available in direct-only mode',
|
|
69
|
+
// pages/iam-container/iam-container.component.ts
|
|
70
|
+
'iam.title': 'Identity & Access Management',
|
|
71
|
+
'iam.subtitle': 'Manage roles, permissions, and access control',
|
|
72
|
+
'iam.action.title': 'Actions',
|
|
73
|
+
'iam.role.title': 'Roles',
|
|
74
|
+
'iam.permission.title': 'Permissions',
|
|
75
|
+
// pages/action/action-list-page.component.ts
|
|
76
|
+
'iam.action.new': 'New Action',
|
|
77
|
+
'iam.action.name': 'Action Name',
|
|
78
|
+
'iam.action.code': 'Action Code',
|
|
79
|
+
'iam.action.type': 'Type',
|
|
80
|
+
'iam.action.no.actions': 'No actions found',
|
|
81
|
+
'iam.action.type.backend': 'Backend',
|
|
82
|
+
'iam.action.type.frontend': 'Frontend',
|
|
83
|
+
'iam.action.type.both': 'Both',
|
|
84
|
+
'iam.action.delete.title': 'Delete Action',
|
|
85
|
+
// pages/action/action-form-page.component.ts
|
|
86
|
+
'iam.action.edit': 'Edit Action',
|
|
87
|
+
'iam.action.name.placeholder': 'Enter action name',
|
|
88
|
+
'iam.action.code.placeholder': 'Enter action code',
|
|
89
|
+
'iam.action.parent': 'Parent Action',
|
|
90
|
+
'iam.action.select.parent': 'Select Parent Action',
|
|
91
|
+
'iam.action.type.backend.label': 'Backend Only',
|
|
92
|
+
'iam.action.type.frontend.label': 'Frontend Only',
|
|
93
|
+
'iam.action.type.both.label': 'Backend & Frontend',
|
|
94
|
+
// pages/role/role-list-page.component.ts
|
|
95
|
+
'iam.company.title': 'Companies',
|
|
96
|
+
'iam.role.new': 'New Role',
|
|
97
|
+
'iam.role.name': 'Role Name',
|
|
98
|
+
'iam.role.no.roles': 'No roles found',
|
|
99
|
+
'iam.role.delete.title': 'Delete Role',
|
|
100
|
+
// pages/role/role-form-page.component.ts
|
|
101
|
+
'iam.role.edit': 'Edit Role',
|
|
102
|
+
'iam.role.name.placeholder': 'Enter role name',
|
|
103
|
+
// pages/permission/permission-page.component.ts
|
|
104
|
+
'iam.permission.role.actions': 'Role Actions',
|
|
105
|
+
'iam.permission.user.roles': 'User Roles',
|
|
106
|
+
'iam.permission.user.actions': 'User Actions',
|
|
107
|
+
'iam.permission.company.actions': 'Company Actions',
|
|
108
|
+
// components/logic-builder.component.ts
|
|
109
|
+
'iam.logic.title': 'Permission Logic',
|
|
110
|
+
'iam.logic.add.logic': 'Add Logic',
|
|
111
|
+
'iam.logic.clear.logic': 'Clear Logic',
|
|
112
|
+
'iam.logic.description': 'Define permission requirements using AND/OR logic with actions',
|
|
113
|
+
'iam.logic.click.to.toggle': 'Click to toggle',
|
|
114
|
+
'iam.logic.conditions': '{{count}} conditions',
|
|
115
|
+
'iam.logic.select.action': 'Select Action...',
|
|
116
|
+
'iam.logic.actions.available': '{{count}} available',
|
|
117
|
+
'iam.logic.add.condition': 'Add Condition:',
|
|
118
|
+
'iam.logic.group': 'Group',
|
|
119
|
+
'iam.logic.action': 'Action',
|
|
120
|
+
// components/role-action-selector.component.ts
|
|
121
|
+
'iam.permission.select.role': 'Select Role',
|
|
122
|
+
'iam.permission.select.role.placeholder': 'Search and select a role',
|
|
123
|
+
'iam.permission.action.permissions': 'Action Permissions',
|
|
124
|
+
'iam.permission.actions.available': '{{count}} actions available',
|
|
125
|
+
'iam.validation.warning.title': 'Validation Warning',
|
|
126
|
+
'iam.validation.unmet.prerequisites.plural': '{{count}} selected actions have unmet prerequisites. Fix before saving or use auto-fix on save.',
|
|
127
|
+
'iam.validation.unmet.prerequisites.singular': '{{count}} selected action has unmet prerequisites. Fix before saving or use auto-fix on save.',
|
|
128
|
+
'iam.permission.requirements': 'Requirements',
|
|
129
|
+
'iam.validation.unmet.prerequisites.tooltip': 'This action has unmet prerequisites and will fail validation on save',
|
|
130
|
+
'iam.permission.has.prerequisites': 'Has prerequisites',
|
|
131
|
+
'iam.permission.no.actions.available': 'No actions available',
|
|
132
|
+
'iam.permission.no.actions.for.role': 'No actions available for this role',
|
|
133
|
+
'iam.tooltip.remove.action': 'Click to remove',
|
|
134
|
+
'iam.tooltip.add.action': 'Click to add (auto-selects required)',
|
|
135
|
+
'iam.tooltip.assigned.to.role': 'Assigned to role',
|
|
136
|
+
'iam.tooltip.click.to.remove.role': 'Click to remove',
|
|
137
|
+
'iam.tooltip.click.to.assign.role': 'Click to assign to role',
|
|
138
|
+
// components/user-action-selector.component.ts
|
|
139
|
+
'iam.permission.select.user': 'Select User',
|
|
140
|
+
'iam.permission.select.user.placeholder': 'Search and select a user',
|
|
141
|
+
'iam.permission.select.branch': 'Select Branch',
|
|
142
|
+
'iam.permission.select.branch.placeholder': 'Search and select a branch',
|
|
143
|
+
'iam.branch.permitted.count.plural': '{{count}} permitted branches in current company',
|
|
144
|
+
'iam.branch.permitted.count': '{{count}} permitted branch in current company',
|
|
145
|
+
'iam.permission.direct.action.permissions': 'Direct Action Permissions',
|
|
146
|
+
'iam.permission.no.actions.for.user': 'No actions available for this user',
|
|
147
|
+
'iam.tooltip.assigned.to.user': 'Assigned to user',
|
|
148
|
+
'iam.tooltip.click.to.remove.user': 'Click to remove direct permission',
|
|
149
|
+
'iam.tooltip.click.to.assign.user': 'Click to assign direct permission',
|
|
150
|
+
'iam.permission.company.required': 'Please select a company first',
|
|
151
|
+
// components/company-action-selector.component.ts
|
|
152
|
+
'iam.permission.select.company': 'Select Company',
|
|
153
|
+
'iam.permission.select.company.placeholder': 'Search and select a company',
|
|
154
|
+
'iam.permission.action.whitelist': 'Action Whitelist',
|
|
155
|
+
'iam.permission.no.actions.for.company': 'No actions available for this company',
|
|
156
|
+
'iam.tooltip.selected': 'Selected',
|
|
157
|
+
'iam.tooltip.click.to.remove': 'Click to remove from company whitelist',
|
|
158
|
+
'iam.tooltip.click.to.add': 'Click to add to company whitelist',
|
|
159
|
+
'iam.permission.requires': 'requires',
|
|
160
|
+
'iam.permission.prerequisite.validation.failed': 'Prerequisite Validation Failed',
|
|
161
|
+
'iam.permission.prerequisite.error.message': 'The following actions have unmet prerequisites:',
|
|
162
|
+
'iam.permission.auto.select.required': 'Auto-select Required',
|
|
163
|
+
'iam.permission.actions.selected': 'Actions Selected',
|
|
164
|
+
'iam.permission.auto.selected.prerequisites': 'Required prerequisites have been auto-selected. Click Save to apply changes.',
|
|
165
|
+
'iam.permission.changes.reverted': 'Changes Reverted',
|
|
166
|
+
'iam.permission.selection.reverted': 'Selection has been reverted to the initial state.',
|
|
167
|
+
// components/user-role-selector.component.ts
|
|
168
|
+
'iam.permission.role.assignments': 'Role Assignments',
|
|
169
|
+
'iam.permission.roles.available': '{{count}} roles available',
|
|
170
|
+
'iam.pagination.roles.template': 'Showing {first} to {last} of {totalRecords} roles',
|
|
171
|
+
'iam.permission.no.roles.available': 'No roles available',
|
|
172
|
+
'iam.permission.no.roles.for.user': 'No roles available for this user',
|
|
173
|
+
'iam.tooltip.click.to.remove.role.from.user': 'Click to remove role',
|
|
174
|
+
'iam.tooltip.click.to.assign.role.to.user': 'Click to assign role to user',
|
|
175
|
+
// services/action-permission-logic.service.ts
|
|
176
|
+
'iam.logic.unknown.action': 'Unknown Action',
|
|
177
|
+
'iam.logic.validation.failed': 'Validation Failed',
|
|
178
|
+
'iam.logic.validation.failed.message': 'The following actions have unmet prerequisites:',
|
|
179
|
+
'iam.logic.validation.failed.prompt': 'Would you like to:',
|
|
180
|
+
'iam.logic.auto.fix': 'Auto-fix (Remove Invalid)',
|
|
181
|
+
'iam.logic.invalid.actions.removed': 'Invalid Actions Removed',
|
|
182
|
+
'iam.logic.removed.actions.detail': 'Removed {{count}} action(s) with unmet prerequisites. You can now save.',
|
|
183
|
+
'iam.logic.save.cancelled': 'Save Cancelled',
|
|
184
|
+
'iam.logic.fix.prerequisites.manually': 'Please fix the prerequisites manually before saving',
|
|
185
|
+
'iam.logic.prerequisites.satisfied': '[OK] Prerequisites Satisfied',
|
|
186
|
+
'iam.logic.all.required.selected': 'All required actions are already selected.\nYou can safely add this action.',
|
|
187
|
+
'iam.logic.prerequisites.required': 'Prerequisites Required ({{count}} missing)',
|
|
188
|
+
'iam.logic.click.to.auto.select': 'Click to auto-select required actions',
|
|
189
|
+
'iam.logic.select.action.label': 'Select Action',
|
|
190
|
+
'iam.logic.auto.select.actions': 'Auto-select Actions',
|
|
191
|
+
'iam.logic.action.selected.detail': 'Action selected successfully (prerequisites already satisfied)',
|
|
192
|
+
'iam.logic.auto.selected.detail': 'Automatically selected {{count}} action(s) including prerequisites',
|
|
193
|
+
'iam.logic.minimum.required': ' (minimum required)',
|
|
194
|
+
'iam.logic.auto.select.prompt': 'Auto-select will choose {{count}} action(s){{suffix}}.',
|
|
195
|
+
'iam.logic.missing.prerequisites': 'Missing Prerequisites',
|
|
196
|
+
'iam.logic.requires.conditions': 'requires the following conditions to be satisfied:',
|
|
197
|
+
'iam.logic.would.you.continue': 'Would you like to continue?',
|
|
198
|
+
'iam.logic.actions.selected.summary': 'Actions Selected',
|
|
199
|
+
'iam.logic.selection.cancelled': 'Selection Cancelled',
|
|
200
|
+
'iam.logic.action.not.added': 'Action not added to whitelist',
|
|
201
|
+
'iam.logic.required.by.actions': 'is required by the following action(s):',
|
|
202
|
+
'iam.logic.alternatives.available': 'Alternative options available:',
|
|
203
|
+
'iam.logic.switch.to.alternatives': 'Would you like to automatically switch to alternatives?',
|
|
204
|
+
'iam.logic.remove.dependents': 'Removing this will also remove the dependent action(s).',
|
|
205
|
+
'iam.logic.dependency.warning': 'Dependency Warning',
|
|
206
|
+
'iam.logic.use.alternatives': 'Use Alternatives',
|
|
207
|
+
'iam.logic.remove.all': 'Remove All',
|
|
208
|
+
'iam.logic.alternatives.applied': 'Alternatives Applied',
|
|
209
|
+
'iam.logic.switched.to.alternatives': 'Automatically switched to alternative action(s)',
|
|
210
|
+
'iam.logic.actions.removed': 'Actions Removed',
|
|
211
|
+
'iam.logic.removed.with.dependents': 'Removed {{name}} and {{count}} dependent action(s)',
|
|
212
|
+
'iam.logic.cancelled': 'Cancelled',
|
|
213
|
+
'iam.logic.no.changes.made': 'No changes made',
|
|
214
|
+
'iam.logic.satisfied': '(satisfied)',
|
|
215
|
+
'iam.logic.missing': '(missing)',
|
|
216
|
+
'iam.logic.invalid.node': 'Invalid logic node',
|
|
217
|
+
'iam.logic.invalid': '(invalid)',
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
class RoleApiService extends ApiResourceService {
|
|
221
|
+
constructor() {
|
|
222
|
+
super('roles', 'iam');
|
|
223
|
+
}
|
|
224
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
225
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, providedIn: 'root' });
|
|
226
|
+
}
|
|
227
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleApiService, decorators: [{
|
|
228
|
+
type: Injectable,
|
|
229
|
+
args: [{ providedIn: 'root' }]
|
|
230
|
+
}], ctorParameters: () => [] });
|
|
231
|
+
|
|
232
|
+
class ActionApiService extends ApiResourceService {
|
|
233
|
+
appConfig = inject(APP_CONFIG);
|
|
234
|
+
constructor() {
|
|
235
|
+
super('actions', 'iam');
|
|
236
|
+
}
|
|
237
|
+
/** Get actions filtered by company whitelist for permission assignment */
|
|
238
|
+
getActionsForPermission() {
|
|
239
|
+
const iamBaseUrl = getServiceUrl(this.appConfig, 'iam');
|
|
240
|
+
return this.http.post(`${iamBaseUrl}/actions/tree-for-permission`, {});
|
|
241
|
+
}
|
|
242
|
+
/** Get actions in hierarchical tree structure */
|
|
243
|
+
getTree(search, isActive, withDeleted) {
|
|
244
|
+
const body = {};
|
|
245
|
+
if (search?.trim())
|
|
246
|
+
body.search = search.trim();
|
|
247
|
+
if (isActive !== undefined)
|
|
248
|
+
body.isActive = isActive;
|
|
249
|
+
if (withDeleted !== undefined)
|
|
250
|
+
body.withDeleted = withDeleted;
|
|
251
|
+
const iamBaseUrl = getServiceUrl(this.appConfig, 'iam');
|
|
252
|
+
return this.http.post(`${iamBaseUrl}/actions/tree`, body);
|
|
253
|
+
}
|
|
254
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
255
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, providedIn: 'root' });
|
|
256
|
+
}
|
|
257
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionApiService, decorators: [{
|
|
258
|
+
type: Injectable,
|
|
259
|
+
args: [{ providedIn: 'root' }]
|
|
260
|
+
}], ctorParameters: () => [] });
|
|
261
|
+
|
|
262
|
+
function extractRequiredActionIds(logic) {
|
|
263
|
+
const actionIds = new Set();
|
|
264
|
+
if (logic) {
|
|
265
|
+
collectActionIds(logic, actionIds);
|
|
266
|
+
}
|
|
267
|
+
return actionIds;
|
|
268
|
+
}
|
|
269
|
+
function collectActionIds(node, actionIds) {
|
|
270
|
+
if (node.type === 'action' && node.actionId) {
|
|
271
|
+
actionIds.add(node.actionId);
|
|
272
|
+
}
|
|
273
|
+
else if (node.type === 'group' && node.children) {
|
|
274
|
+
node.children.forEach((child) => collectActionIds(child, actionIds));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function validateActionPrerequisites(action, selectedActionIds, allActions) {
|
|
278
|
+
if (!action.permissionLogic) {
|
|
279
|
+
return { valid: true, missingActions: [] };
|
|
280
|
+
}
|
|
281
|
+
const result = evaluateLogicNodeWithMissing(action.permissionLogic, selectedActionIds);
|
|
282
|
+
if (result.valid) {
|
|
283
|
+
return { valid: true, missingActions: [] };
|
|
284
|
+
}
|
|
285
|
+
const missingActions = allActions.filter((a) => a.id && result.missingActionIds.has(a.id));
|
|
286
|
+
return { valid: false, missingActions };
|
|
287
|
+
}
|
|
288
|
+
function evaluateLogicNodeWithMissing(node, selectedActionIds) {
|
|
289
|
+
if (node.type === 'action' && node.actionId) {
|
|
290
|
+
const valid = selectedActionIds.has(node.actionId);
|
|
291
|
+
return { valid, missingActionIds: valid ? new Set() : new Set([node.actionId]) };
|
|
292
|
+
}
|
|
293
|
+
if (node.type === 'group' && node.children?.length) {
|
|
294
|
+
const operator = node.operator || 'AND';
|
|
295
|
+
if (operator === 'AND') {
|
|
296
|
+
const missingIds = new Set();
|
|
297
|
+
let allValid = true;
|
|
298
|
+
for (const child of node.children) {
|
|
299
|
+
const childResult = evaluateLogicNodeWithMissing(child, selectedActionIds);
|
|
300
|
+
if (!childResult.valid) {
|
|
301
|
+
allValid = false;
|
|
302
|
+
childResult.missingActionIds.forEach((id) => missingIds.add(id));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { valid: allValid, missingActionIds: missingIds };
|
|
306
|
+
}
|
|
307
|
+
// OR operator
|
|
308
|
+
for (const child of node.children) {
|
|
309
|
+
const childResult = evaluateLogicNodeWithMissing(child, selectedActionIds);
|
|
310
|
+
if (childResult.valid) {
|
|
311
|
+
return { valid: true, missingActionIds: new Set() };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const firstChildResult = evaluateLogicNodeWithMissing(node.children[0], selectedActionIds);
|
|
315
|
+
return { valid: false, missingActionIds: firstChildResult.missingActionIds };
|
|
316
|
+
}
|
|
317
|
+
return { valid: true, missingActionIds: new Set() };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Shared service for smart dependency management across action selectors */
|
|
321
|
+
class ActionPermissionLogicService {
|
|
322
|
+
confirmationService = inject(ConfirmationService);
|
|
323
|
+
messageService = inject(MessageService);
|
|
324
|
+
translateAdapter = inject(TranslateService, {
|
|
325
|
+
optional: true,
|
|
326
|
+
});
|
|
327
|
+
t(key, variables) {
|
|
328
|
+
return this.translateAdapter?.translate(key, variables) ?? key;
|
|
329
|
+
}
|
|
330
|
+
/** Handle checking an action with prerequisite validation (recursive deep scan) */
|
|
331
|
+
handleCheck(action, currentSelection, allActions, onUpdate, onCancel) {
|
|
332
|
+
// Validate prerequisites with RECURSIVE DEEP SCAN
|
|
333
|
+
if (action.permissionLogic) {
|
|
334
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
335
|
+
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
336
|
+
if (!validationResult.valid) {
|
|
337
|
+
// Store previous state
|
|
338
|
+
const previousState = { ...currentSelection };
|
|
339
|
+
// Temporarily check the box
|
|
340
|
+
const tempMap = { ...currentSelection };
|
|
341
|
+
tempMap[action.id] = true;
|
|
342
|
+
onUpdate(tempMap);
|
|
343
|
+
// CRITICAL: Recursively collect ALL missing prerequisites (deep scan)
|
|
344
|
+
const allMissingActions = this.getAllMissingPrerequisitesRecursive(action, currentSelection, allActions);
|
|
345
|
+
// Show prerequisite dialog with COMPLETE dependency chain
|
|
346
|
+
this.showPrerequisiteDialog(action, allMissingActions, currentSelection, allActions, previousState, onUpdate, onCancel);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// All prerequisites satisfied, allow selection
|
|
351
|
+
const selMap = { ...currentSelection };
|
|
352
|
+
selMap[action.id] = true;
|
|
353
|
+
onUpdate(selMap);
|
|
354
|
+
}
|
|
355
|
+
/** Handle unchecking an action with dependency detection */
|
|
356
|
+
handleUncheck(action, currentSelection, allActions, onUpdate) {
|
|
357
|
+
const affectedActions = this.findActionsDependingOn(action.id, currentSelection, allActions);
|
|
358
|
+
if (affectedActions.length > 0) {
|
|
359
|
+
this.showDependencyDialog(action, affectedActions, currentSelection, allActions, onUpdate);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// No dependencies, safe to uncheck
|
|
363
|
+
const selMap = { ...currentSelection };
|
|
364
|
+
selMap[action.id] = false;
|
|
365
|
+
onUpdate(selMap);
|
|
366
|
+
}
|
|
367
|
+
/** Check if an action has unmet prerequisites */
|
|
368
|
+
hasUnmetPrerequisites(action, currentSelection, allActions) {
|
|
369
|
+
const isSelected = currentSelection[action.id];
|
|
370
|
+
if (!isSelected || !action.permissionLogic) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
374
|
+
selectedActionIds.delete(action.id);
|
|
375
|
+
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
376
|
+
return !validationResult.valid;
|
|
377
|
+
}
|
|
378
|
+
/** Get all selected actions that have unmet prerequisites */
|
|
379
|
+
getActionsWithUnmetPrerequisites(currentSelection, allActions) {
|
|
380
|
+
const selectedActions = allActions.filter((a) => currentSelection[a.id]);
|
|
381
|
+
return selectedActions.filter((action) => this.hasUnmetPrerequisites(action, currentSelection, allActions));
|
|
382
|
+
}
|
|
383
|
+
/** Show validation error dialog with auto-fix options */
|
|
384
|
+
showValidationErrorDialog(invalidActions, currentSelection, allActions, onUpdate) {
|
|
385
|
+
const errorList = invalidActions
|
|
386
|
+
.map((action) => {
|
|
387
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
388
|
+
selectedActionIds.delete(action.id);
|
|
389
|
+
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
390
|
+
const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
|
|
391
|
+
const missingNames = validationResult.missingActions
|
|
392
|
+
.map((a) => this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action')))
|
|
393
|
+
.join(', ');
|
|
394
|
+
return `<li><strong>${sanitizedActionName}</strong> requires: ${missingNames}</li>`;
|
|
395
|
+
})
|
|
396
|
+
.join('');
|
|
397
|
+
this.confirmationService.confirm({
|
|
398
|
+
header: this.t('iam.logic.validation.failed'),
|
|
399
|
+
message: `${this.t('iam.logic.validation.failed.message')}<br/><br/><ul class="pl-4">${errorList}</ul><br/>${this.t('iam.logic.validation.failed.prompt')}`,
|
|
400
|
+
icon: 'exclamation-triangle',
|
|
401
|
+
acceptLabel: this.t('iam.logic.auto.fix'),
|
|
402
|
+
rejectLabel: this.t('shared.cancel'),
|
|
403
|
+
acceptIcon: 'trash',
|
|
404
|
+
rejectIcon: 'times',
|
|
405
|
+
acceptButtonStyleClass: 'p-button-warning',
|
|
406
|
+
closeOnEscape: false,
|
|
407
|
+
closable: false,
|
|
408
|
+
accept: () => {
|
|
409
|
+
// Remove all invalid actions
|
|
410
|
+
const selMap = { ...currentSelection };
|
|
411
|
+
invalidActions.forEach((action) => {
|
|
412
|
+
selMap[action.id] = false;
|
|
413
|
+
});
|
|
414
|
+
onUpdate(selMap);
|
|
415
|
+
this.messageService.add({
|
|
416
|
+
severity: 'warn',
|
|
417
|
+
summary: this.t('iam.logic.invalid.actions.removed'),
|
|
418
|
+
detail: this.t('iam.logic.removed.actions.detail', {
|
|
419
|
+
count: invalidActions.length,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
},
|
|
423
|
+
reject: () => {
|
|
424
|
+
this.messageService.add({
|
|
425
|
+
severity: 'info',
|
|
426
|
+
summary: this.t('iam.logic.save.cancelled'),
|
|
427
|
+
detail: this.t('iam.logic.fix.prerequisites.manually'),
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** Get prerequisite description for tooltip display */
|
|
433
|
+
getPrerequisiteTooltip(action, currentSelection, allActions) {
|
|
434
|
+
if (!action.permissionLogic) {
|
|
435
|
+
return '';
|
|
436
|
+
}
|
|
437
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
438
|
+
const validationResult = validateActionPrerequisites(action, selectedActionIds, allActions);
|
|
439
|
+
if (validationResult.valid) {
|
|
440
|
+
return `${this.t('iam.logic.prerequisites.satisfied')}\n\n${this.t('iam.logic.all.required.selected')}`;
|
|
441
|
+
}
|
|
442
|
+
const allMissingActions = this.getAllMissingPrerequisitesRecursive(action, currentSelection, allActions);
|
|
443
|
+
const logicTree = this.buildTooltipLogicTree(action.permissionLogic, currentSelection, allActions, 0);
|
|
444
|
+
const missingCount = allMissingActions.length;
|
|
445
|
+
const header = `⚠ ${this.t('iam.logic.prerequisites.required', { count: missingCount })}`;
|
|
446
|
+
const hint = `\n\n💡 ${this.t('iam.logic.click.to.auto.select')}`;
|
|
447
|
+
return `${header}\n\n${logicTree}${hint}`;
|
|
448
|
+
}
|
|
449
|
+
/** Build dynamic logic tree message with AND/OR operators and nesting */
|
|
450
|
+
buildLogicMessage(logic, _missingActions, allActions, currentSelection) {
|
|
451
|
+
const selectedIds = currentSelection
|
|
452
|
+
? this.getSelectedIds(currentSelection)
|
|
453
|
+
: new Set();
|
|
454
|
+
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
455
|
+
return this.formatLogicNode(logic, selectedIds, actionMap, 0);
|
|
456
|
+
}
|
|
457
|
+
/** Extract selected action IDs from selection map */
|
|
458
|
+
getSelectedIds(currentSelection) {
|
|
459
|
+
return new Set(Object.keys(currentSelection).filter((id) => currentSelection[id]));
|
|
460
|
+
}
|
|
461
|
+
sanitizeHtml(text) {
|
|
462
|
+
return text
|
|
463
|
+
.replace(/&/g, '&')
|
|
464
|
+
.replace(/</g, '<')
|
|
465
|
+
.replace(/>/g, '>')
|
|
466
|
+
.replace(/"/g, '"')
|
|
467
|
+
.replace(/'/g, ''');
|
|
468
|
+
}
|
|
469
|
+
/** Recursively collect ALL missing prerequisites at all dependency levels */
|
|
470
|
+
getAllMissingPrerequisitesRecursive(action, currentSelection, allActions) {
|
|
471
|
+
if (!action.id)
|
|
472
|
+
return [];
|
|
473
|
+
const visited = new Set();
|
|
474
|
+
const allMissing = new Map();
|
|
475
|
+
const stack = new Set();
|
|
476
|
+
const collectMissing = (targetAction, depth = 0) => {
|
|
477
|
+
if (!targetAction.permissionLogic || !targetAction.id)
|
|
478
|
+
return;
|
|
479
|
+
if (depth > 50) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (visited.has(targetAction.id))
|
|
483
|
+
return;
|
|
484
|
+
if (stack.has(targetAction.id)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
visited.add(targetAction.id);
|
|
488
|
+
stack.add(targetAction.id);
|
|
489
|
+
const selectedActionIds = this.getSelectedIds(currentSelection);
|
|
490
|
+
const result = validateActionPrerequisites(targetAction, selectedActionIds, allActions);
|
|
491
|
+
if (!result.valid) {
|
|
492
|
+
result.missingActions.forEach((missingAction) => {
|
|
493
|
+
if (missingAction.id && missingAction.id !== action.id) {
|
|
494
|
+
allMissing.set(missingAction.id, missingAction);
|
|
495
|
+
collectMissing(missingAction, depth + 1);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
stack.delete(targetAction.id);
|
|
500
|
+
};
|
|
501
|
+
collectMissing(action);
|
|
502
|
+
return Array.from(allMissing.values());
|
|
503
|
+
}
|
|
504
|
+
showPrerequisiteDialog(action, missingActions, currentSelection, allActions, previousState, onUpdate, onCancel) {
|
|
505
|
+
const logicMessage = action.permissionLogic
|
|
506
|
+
? this.buildLogicMessage(action.permissionLogic, missingActions, allActions, currentSelection)
|
|
507
|
+
: this.buildSimpleMessage(missingActions);
|
|
508
|
+
const actionsToSelect = action.permissionLogic
|
|
509
|
+
? this.calculateSmartSelection(action.permissionLogic, missingActions, currentSelection, allActions)
|
|
510
|
+
: missingActions;
|
|
511
|
+
const selectionCount = actionsToSelect.length;
|
|
512
|
+
const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
|
|
513
|
+
const acceptLabel = selectionCount === 0
|
|
514
|
+
? this.t('iam.logic.select.action.label')
|
|
515
|
+
: this.t('iam.logic.auto.select.actions');
|
|
516
|
+
const detailMessage = selectionCount === 0
|
|
517
|
+
? this.t('iam.logic.action.selected.detail')
|
|
518
|
+
: this.t('iam.logic.auto.selected.detail', { count: selectionCount });
|
|
519
|
+
const suffix = selectionCount < missingActions.length
|
|
520
|
+
? this.t('iam.logic.minimum.required')
|
|
521
|
+
: '';
|
|
522
|
+
const autoSelectPrompt = this.t('iam.logic.auto.select.prompt', {
|
|
523
|
+
count: selectionCount,
|
|
524
|
+
suffix,
|
|
525
|
+
});
|
|
526
|
+
this.confirmationService.confirm({
|
|
527
|
+
header: this.t('iam.logic.missing.prerequisites'),
|
|
528
|
+
message: `<strong>${sanitizedActionName}</strong> ${this.t('iam.logic.requires.conditions')}<br/><br/>${logicMessage}<br/><br/><strong>${autoSelectPrompt}</strong><br/>${this.t('iam.logic.would.you.continue')}`,
|
|
529
|
+
icon: 'exclamation-triangle',
|
|
530
|
+
acceptLabel,
|
|
531
|
+
rejectLabel: this.t('shared.cancel'),
|
|
532
|
+
acceptIcon: 'check',
|
|
533
|
+
rejectIcon: 'times',
|
|
534
|
+
closeOnEscape: false,
|
|
535
|
+
closable: false,
|
|
536
|
+
accept: () => {
|
|
537
|
+
const selMap = { ...currentSelection };
|
|
538
|
+
selMap[action.id] = true;
|
|
539
|
+
actionsToSelect.forEach((a) => {
|
|
540
|
+
selMap[a.id] = true;
|
|
541
|
+
});
|
|
542
|
+
onUpdate(selMap);
|
|
543
|
+
this.messageService.add({
|
|
544
|
+
severity: 'info',
|
|
545
|
+
summary: this.t('iam.logic.actions.selected.summary'),
|
|
546
|
+
detail: detailMessage,
|
|
547
|
+
});
|
|
548
|
+
},
|
|
549
|
+
reject: () => {
|
|
550
|
+
onCancel(previousState);
|
|
551
|
+
this.messageService.add({
|
|
552
|
+
severity: 'info',
|
|
553
|
+
summary: this.t('iam.logic.selection.cancelled'),
|
|
554
|
+
detail: this.t('iam.logic.action.not.added'),
|
|
555
|
+
});
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
showDependencyDialog(action, affectedActions, currentSelection, _allActions, onUpdate) {
|
|
560
|
+
if (!action.id)
|
|
561
|
+
return;
|
|
562
|
+
const alternatives = this.findAlternatives(action.id, affectedActions, currentSelection);
|
|
563
|
+
const sanitizedActionName = this.sanitizeHtml(action.name ?? this.t('iam.logic.unknown.action'));
|
|
564
|
+
let message = `<strong>${sanitizedActionName}</strong> ${this.t('iam.logic.required.by.actions')}<br/><br/><ul class="pl-4">`;
|
|
565
|
+
affectedActions.forEach((a) => {
|
|
566
|
+
const sanitizedName = this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action'));
|
|
567
|
+
message += `<li>${sanitizedName}</li>`;
|
|
568
|
+
});
|
|
569
|
+
message += '</ul><br/>';
|
|
570
|
+
if (alternatives.length > 0) {
|
|
571
|
+
message += `<strong>${this.t('iam.logic.alternatives.available')}</strong><br/><br/>`;
|
|
572
|
+
message += `<em>${this.t('iam.logic.switch.to.alternatives')}</em>`;
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
message += `<em>${this.t('iam.logic.remove.dependents')}</em><br/><br/>`;
|
|
576
|
+
message += this.t('iam.logic.would.you.continue');
|
|
577
|
+
}
|
|
578
|
+
this.confirmationService.confirm({
|
|
579
|
+
header: this.t('iam.logic.dependency.warning'),
|
|
580
|
+
message,
|
|
581
|
+
icon: 'exclamation-triangle',
|
|
582
|
+
acceptLabel: alternatives.length > 0
|
|
583
|
+
? this.t('iam.logic.use.alternatives')
|
|
584
|
+
: this.t('iam.logic.remove.all'),
|
|
585
|
+
rejectLabel: this.t('shared.cancel'),
|
|
586
|
+
acceptIcon: 'check',
|
|
587
|
+
rejectIcon: 'times',
|
|
588
|
+
closeOnEscape: false,
|
|
589
|
+
closable: false,
|
|
590
|
+
accept: () => {
|
|
591
|
+
const selMap = { ...currentSelection };
|
|
592
|
+
if (alternatives.length > 0) {
|
|
593
|
+
// Apply alternatives
|
|
594
|
+
alternatives.forEach(({ alternativeId }) => {
|
|
595
|
+
selMap[alternativeId] = true;
|
|
596
|
+
});
|
|
597
|
+
selMap[action.id] = false;
|
|
598
|
+
this.messageService.add({
|
|
599
|
+
severity: 'success',
|
|
600
|
+
summary: this.t('iam.logic.alternatives.applied'),
|
|
601
|
+
detail: this.t('iam.logic.switched.to.alternatives'),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// Remove action and all dependents
|
|
606
|
+
selMap[action.id] = false;
|
|
607
|
+
affectedActions.forEach((a) => {
|
|
608
|
+
selMap[a.id] = false;
|
|
609
|
+
});
|
|
610
|
+
this.messageService.add({
|
|
611
|
+
severity: 'warn',
|
|
612
|
+
summary: this.t('iam.logic.actions.removed'),
|
|
613
|
+
detail: this.t('iam.logic.removed.with.dependents', {
|
|
614
|
+
name: action.name ?? '',
|
|
615
|
+
count: affectedActions.length,
|
|
616
|
+
}),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
onUpdate(selMap);
|
|
620
|
+
},
|
|
621
|
+
reject: () => {
|
|
622
|
+
this.messageService.add({
|
|
623
|
+
severity: 'info',
|
|
624
|
+
summary: this.t('iam.logic.cancelled'),
|
|
625
|
+
detail: this.t('iam.logic.no.changes.made'),
|
|
626
|
+
});
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
findActionsDependingOn(actionId, currentSelection, allActions) {
|
|
631
|
+
const selectedActions = allActions.filter((a) => currentSelection[a.id] && a.id !== actionId);
|
|
632
|
+
return selectedActions.filter((action) => {
|
|
633
|
+
if (!action.permissionLogic)
|
|
634
|
+
return false;
|
|
635
|
+
return this.doesLogicDependOn(action.permissionLogic, actionId);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
doesLogicDependOn(node, targetActionId) {
|
|
639
|
+
if (node.type === 'action') {
|
|
640
|
+
return node.actionId === targetActionId;
|
|
641
|
+
}
|
|
642
|
+
if (node.type === 'group' && node.children) {
|
|
643
|
+
return node.children.some((child) => this.doesLogicDependOn(child, targetActionId));
|
|
644
|
+
}
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
findAlternatives(removingActionId, affectedActions, currentSelection) {
|
|
648
|
+
const alternatives = [];
|
|
649
|
+
for (const affectedAction of affectedActions) {
|
|
650
|
+
if (!affectedAction.permissionLogic)
|
|
651
|
+
continue;
|
|
652
|
+
const alternative = this.findAlternativeInLogic(affectedAction.permissionLogic, removingActionId, currentSelection);
|
|
653
|
+
if (alternative) {
|
|
654
|
+
alternatives.push({ alternativeId: alternative });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return alternatives;
|
|
658
|
+
}
|
|
659
|
+
findAlternativeInLogic(node, removingActionId, currentSelection) {
|
|
660
|
+
if (node.type === 'group' && node.operator === 'OR' && node.children) {
|
|
661
|
+
const hasRemoving = node.children.some((child) => child.type === 'action' && child.actionId === removingActionId);
|
|
662
|
+
if (hasRemoving) {
|
|
663
|
+
for (const child of node.children) {
|
|
664
|
+
if (child.type === 'action' &&
|
|
665
|
+
child.actionId &&
|
|
666
|
+
child.actionId !== removingActionId &&
|
|
667
|
+
!currentSelection[child.actionId]) {
|
|
668
|
+
return child.actionId;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (node.type === 'group' && node.children) {
|
|
674
|
+
for (const child of node.children) {
|
|
675
|
+
const alternative = this.findAlternativeInLogic(child, removingActionId, currentSelection);
|
|
676
|
+
if (alternative)
|
|
677
|
+
return alternative;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
calculateSmartSelection(logic, missingActions, currentSelection, allActions) {
|
|
683
|
+
const missingIds = new Set(missingActions.map((a) => a.id));
|
|
684
|
+
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
685
|
+
const selectedIds = this.getSelectedIds(currentSelection);
|
|
686
|
+
const requiredIds = this.findRequiredActionIds(logic, missingIds, selectedIds);
|
|
687
|
+
return Array.from(requiredIds)
|
|
688
|
+
.map((id) => actionMap.get(id))
|
|
689
|
+
.filter((a) => !!a);
|
|
690
|
+
}
|
|
691
|
+
findRequiredActionIds(node, missingIds, selectedIds) {
|
|
692
|
+
if (node.type === 'action' && node.actionId) {
|
|
693
|
+
if (missingIds.has(node.actionId)) {
|
|
694
|
+
return new Set([node.actionId]);
|
|
695
|
+
}
|
|
696
|
+
return new Set();
|
|
697
|
+
}
|
|
698
|
+
if (node.type === 'group' && node.children && node.children.length > 0) {
|
|
699
|
+
const operator = node.operator || 'AND';
|
|
700
|
+
if (operator === 'AND') {
|
|
701
|
+
const required = new Set();
|
|
702
|
+
for (const child of node.children) {
|
|
703
|
+
const childRequired = this.findRequiredActionIds(child, missingIds, selectedIds);
|
|
704
|
+
childRequired.forEach((id) => required.add(id));
|
|
705
|
+
}
|
|
706
|
+
return required;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
let minRequired = null;
|
|
710
|
+
let minSize = Infinity;
|
|
711
|
+
for (const child of node.children) {
|
|
712
|
+
const childRequired = this.findRequiredActionIds(child, missingIds, selectedIds);
|
|
713
|
+
if (childRequired.size === 0) {
|
|
714
|
+
return new Set();
|
|
715
|
+
}
|
|
716
|
+
if (childRequired.size < minSize) {
|
|
717
|
+
minSize = childRequired.size;
|
|
718
|
+
minRequired = childRequired;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return minRequired || new Set();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return new Set();
|
|
725
|
+
}
|
|
726
|
+
buildSimpleMessage(missingActions) {
|
|
727
|
+
return `<ul class="pl-4">${missingActions.map((a) => `<li>${this.sanitizeHtml(a.name ?? this.t('iam.logic.unknown.action'))}</li>`).join('')}</ul>`;
|
|
728
|
+
}
|
|
729
|
+
formatLogicNode(node, selectedIds, actionMap, depth) {
|
|
730
|
+
const indent = ' '.repeat(depth);
|
|
731
|
+
if (node.type === 'action' && node.actionId) {
|
|
732
|
+
const action = actionMap.get(node.actionId);
|
|
733
|
+
const actionName = this.sanitizeHtml(action?.name || this.t('iam.logic.unknown.action'));
|
|
734
|
+
const isSelected = selectedIds.has(node.actionId);
|
|
735
|
+
if (isSelected) {
|
|
736
|
+
return `${indent}<span style="color: #10b981; font-weight: 500;">✓ ${actionName}</span> <em style="color: #059669;">${this.t('iam.logic.satisfied')}</em>`;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
return `${indent}<span style="color: #ef4444;">✗ ${actionName}</span> <em style="color: #dc2626;">${this.t('iam.logic.missing')}</em>`;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (node.type === 'group' && node.children) {
|
|
743
|
+
const operator = node.operator || 'AND';
|
|
744
|
+
const operatorColor = operator === 'AND' ? '#3b82f6' : '#8b5cf6';
|
|
745
|
+
const lines = [];
|
|
746
|
+
lines.push(`${indent}<strong style="color: ${operatorColor};">${operator}</strong> <span style="color: #6b7280;">(${node.children.length} condition${node.children.length !== 1 ? 's' : ''})</span>`);
|
|
747
|
+
node.children.forEach((child, index) => {
|
|
748
|
+
const childHtml = this.formatLogicNode(child, selectedIds, actionMap, depth + 1);
|
|
749
|
+
lines.push(childHtml);
|
|
750
|
+
if (node.children && index < node.children.length - 1) {
|
|
751
|
+
lines.push(`${indent} <em style="color: ${operatorColor}; font-size: 0.75rem;">${operator.toLowerCase()}</em>`);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
return lines.join('<br/>');
|
|
755
|
+
}
|
|
756
|
+
return `${indent}<em style="color: #9ca3af;">${this.t('iam.logic.invalid.node')}</em>`;
|
|
757
|
+
}
|
|
758
|
+
/** Build clean text-based logic tree for tooltips */
|
|
759
|
+
buildTooltipLogicTree(node, currentSelection, allActions, depth) {
|
|
760
|
+
const indent = ' '.repeat(depth);
|
|
761
|
+
const selectedIds = this.getSelectedIds(currentSelection);
|
|
762
|
+
const actionMap = new Map(allActions.map((a) => [a.id, a]));
|
|
763
|
+
if (node.type === 'action' && node.actionId) {
|
|
764
|
+
const action = actionMap.get(node.actionId);
|
|
765
|
+
const actionName = action?.name || this.t('iam.logic.unknown.action');
|
|
766
|
+
const isSelected = selectedIds.has(node.actionId);
|
|
767
|
+
if (isSelected) {
|
|
768
|
+
return `${indent}✓ ${actionName}`;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
return `${indent}✗ ${actionName}`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (node.type === 'group' && node.children) {
|
|
775
|
+
const operator = node.operator || 'AND';
|
|
776
|
+
const lines = [];
|
|
777
|
+
lines.push(`${indent}${operator}:`);
|
|
778
|
+
node.children.forEach((child, index) => {
|
|
779
|
+
const childText = this.buildTooltipLogicTree(child, currentSelection, allActions, depth + 1);
|
|
780
|
+
lines.push(childText);
|
|
781
|
+
if (node.children &&
|
|
782
|
+
index < node.children.length - 1 &&
|
|
783
|
+
operator === 'OR') {
|
|
784
|
+
lines.push(`${indent} ${operator.toLowerCase()}`);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
return lines.join('\n');
|
|
788
|
+
}
|
|
789
|
+
return `${indent}${this.t('iam.logic.invalid')}`;
|
|
790
|
+
}
|
|
791
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
792
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, providedIn: 'root' });
|
|
793
|
+
}
|
|
794
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ActionPermissionLogicService, decorators: [{
|
|
795
|
+
type: Injectable,
|
|
796
|
+
args: [{ providedIn: 'root' }]
|
|
797
|
+
}] });
|
|
798
|
+
|
|
799
|
+
class PermissionApiService extends BaseApiService {
|
|
800
|
+
constructor() {
|
|
801
|
+
super('iam');
|
|
802
|
+
}
|
|
803
|
+
assignUserActions(data) {
|
|
804
|
+
return this.http.post(this.getUrl('permissions/user-actions/assign'), data);
|
|
805
|
+
}
|
|
806
|
+
getUserActions(userId, query) {
|
|
807
|
+
return this.http.post(this.getUrl('permissions/get-user-actions'), { userId, branchId: query?.branchId });
|
|
808
|
+
}
|
|
809
|
+
assignUserRoles(data) {
|
|
810
|
+
return this.http.post(this.getUrl('permissions/user-roles/assign'), data);
|
|
811
|
+
}
|
|
812
|
+
getUserRoles(userId, query) {
|
|
813
|
+
return this.http.post(this.getUrl('permissions/get-user-roles'), { userId, branchId: query?.branchId });
|
|
814
|
+
}
|
|
815
|
+
assignRoleActions(data) {
|
|
816
|
+
return this.http.post(this.getUrl('permissions/role-actions/assign'), data);
|
|
817
|
+
}
|
|
818
|
+
getRoleActions(roleId) {
|
|
819
|
+
return this.http.post(this.getUrl('permissions/get-role-actions'), { roleId });
|
|
820
|
+
}
|
|
821
|
+
assignCompanyActions(data) {
|
|
822
|
+
return this.http.post(this.getUrl('permissions/company-actions/assign'), data);
|
|
823
|
+
}
|
|
824
|
+
getCompanyActions(companyId) {
|
|
825
|
+
return this.http.post(this.getUrl('permissions/get-company-actions'), { companyId });
|
|
826
|
+
}
|
|
827
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
828
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, providedIn: 'root' });
|
|
829
|
+
}
|
|
830
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionApiService, decorators: [{
|
|
831
|
+
type: Injectable,
|
|
832
|
+
args: [{ providedIn: 'root' }]
|
|
833
|
+
}], ctorParameters: () => [] });
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* My Permissions API Service
|
|
837
|
+
* Get current user's complete permissions
|
|
838
|
+
* Endpoint: POST /iam/permissions/my-permissions
|
|
839
|
+
* Always enabled
|
|
840
|
+
*/
|
|
841
|
+
class MyPermissionsApiService {
|
|
842
|
+
http = inject(HttpClient);
|
|
843
|
+
appConfig = inject(APP_CONFIG);
|
|
844
|
+
baseUrl = `${getServiceUrl(this.appConfig, 'iam')}/permissions`;
|
|
845
|
+
/**
|
|
846
|
+
* Get current user's complete permissions
|
|
847
|
+
* POST /iam/permissions/my-permissions
|
|
848
|
+
* Returns menus, frontend actions, and cached endpoint count
|
|
849
|
+
* @param dto Optional filter by parent codes (only affects frontend actions)
|
|
850
|
+
*/
|
|
851
|
+
getMyPermissions(dto = {}) {
|
|
852
|
+
return this.http.post(`${this.baseUrl}/my-permissions`, dto);
|
|
853
|
+
}
|
|
854
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
855
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, providedIn: 'root' });
|
|
856
|
+
}
|
|
857
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: MyPermissionsApiService, decorators: [{
|
|
858
|
+
type: Injectable,
|
|
859
|
+
args: [{
|
|
860
|
+
providedIn: 'root',
|
|
861
|
+
}]
|
|
862
|
+
}] });
|
|
863
|
+
|
|
864
|
+
class PermissionStateService {
|
|
865
|
+
permissionApi = inject(MyPermissionsApiService);
|
|
866
|
+
permissionValidator = inject(PermissionValidatorService);
|
|
867
|
+
_permissions = signal(null, /* @ts-ignore */
|
|
868
|
+
...(ngDevMode ? [{ debugName: "_permissions" }] : /* istanbul ignore next */ []));
|
|
869
|
+
permissions = this._permissions.asReadonly();
|
|
870
|
+
_isLoading = signal(false, /* @ts-ignore */
|
|
871
|
+
...(ngDevMode ? [{ debugName: "_isLoading" }] : /* istanbul ignore next */ []));
|
|
872
|
+
isLoading = this._isLoading.asReadonly();
|
|
873
|
+
loadPermissions(dto) {
|
|
874
|
+
this._isLoading.set(true);
|
|
875
|
+
return this.permissionApi.getMyPermissions(dto).pipe(tap((response) => {
|
|
876
|
+
this._permissions.set(response.data ?? null);
|
|
877
|
+
this._isLoading.set(false);
|
|
878
|
+
const actionCodes = response.data?.frontendActions.map((a) => a.code) ?? [];
|
|
879
|
+
this.permissionValidator.setPermissions(actionCodes);
|
|
880
|
+
}), catchError(() => {
|
|
881
|
+
this._permissions.set(null);
|
|
882
|
+
this._isLoading.set(false);
|
|
883
|
+
this.permissionValidator.clearPermissions();
|
|
884
|
+
return of(void 0);
|
|
885
|
+
}), map(() => void 0));
|
|
886
|
+
}
|
|
887
|
+
isLoaded() {
|
|
888
|
+
return this._permissions() !== null && this.permissionValidator.isPermissionsLoaded();
|
|
889
|
+
}
|
|
890
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
891
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, providedIn: 'root' });
|
|
892
|
+
}
|
|
893
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: PermissionStateService, decorators: [{
|
|
894
|
+
type: Injectable,
|
|
895
|
+
args: [{ providedIn: 'root' }]
|
|
896
|
+
}] });
|
|
897
|
+
|
|
898
|
+
// Role services
|
|
899
|
+
|
|
900
|
+
function toLogicNode(node) {
|
|
901
|
+
if (node.type === 'action') {
|
|
902
|
+
return { type: 'action', actionId: node.actionId };
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
type: 'group',
|
|
906
|
+
operator: node.operator,
|
|
907
|
+
children: node.children?.map(toLogicNode) ?? [],
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function toBuilderNode(node) {
|
|
911
|
+
if (node.type === 'action') {
|
|
912
|
+
return createActionNode(node.actionId);
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
...createGroupNode(node.operator),
|
|
916
|
+
children: node.children?.map(toBuilderNode) ?? [],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function createGroupNode(operator = 'AND') {
|
|
920
|
+
return { id: crypto.randomUUID(), type: 'group', operator, children: [] };
|
|
921
|
+
}
|
|
922
|
+
function createActionNode(actionId = '') {
|
|
923
|
+
return { id: crypto.randomUUID(), type: 'action', actionId };
|
|
924
|
+
}
|
|
925
|
+
/** Visual builder for AND/OR permission logic trees */
|
|
926
|
+
class LogicBuilderComponent {
|
|
927
|
+
logic = input(null, /* @ts-ignore */
|
|
928
|
+
...(ngDevMode ? [{ debugName: "logic" }] : /* istanbul ignore next */ []));
|
|
929
|
+
actions = input([], /* @ts-ignore */
|
|
930
|
+
...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
|
|
931
|
+
logicChange = output();
|
|
932
|
+
/** Internal builder tree state (private writable + public readonly pattern) */
|
|
933
|
+
_builderTree = signal(null, /* @ts-ignore */
|
|
934
|
+
...(ngDevMode ? [{ debugName: "_builderTree" }] : /* istanbul ignore next */ []));
|
|
935
|
+
builderLogic = this._builderTree.asReadonly();
|
|
936
|
+
constructor() {
|
|
937
|
+
effect(() => {
|
|
938
|
+
const logic = this.logic();
|
|
939
|
+
if (!logic) {
|
|
940
|
+
this._builderTree.set(null);
|
|
941
|
+
}
|
|
942
|
+
else if (!this._builderTree()) {
|
|
943
|
+
this._builderTree.set(toBuilderNode(logic));
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
initializeLogic() {
|
|
948
|
+
this.updateTreeAndEmit(createGroupNode());
|
|
949
|
+
}
|
|
950
|
+
clearLogic() {
|
|
951
|
+
this._builderTree.set(null);
|
|
952
|
+
this.logicChange.emit(null);
|
|
953
|
+
}
|
|
954
|
+
toggleOperator(nodeId) {
|
|
955
|
+
this.updateNode(nodeId, (node) => ({
|
|
956
|
+
...node,
|
|
957
|
+
operator: node.operator === 'AND' ? 'OR' : 'AND',
|
|
958
|
+
}));
|
|
959
|
+
}
|
|
960
|
+
addChildNode(parentId, type) {
|
|
961
|
+
const newNode = type === 'group' ? createGroupNode() : createActionNode();
|
|
962
|
+
this.updateNode(parentId, (node) => ({
|
|
963
|
+
...node,
|
|
964
|
+
children: [...(node.children || []), newNode],
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
removeNode(nodeId) {
|
|
968
|
+
const tree = this._builderTree();
|
|
969
|
+
if (!tree)
|
|
970
|
+
return;
|
|
971
|
+
if (tree.id === nodeId) {
|
|
972
|
+
this.clearLogic();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
this.updateTreeAndEmit(this.removeNodeFromTree(tree, nodeId));
|
|
976
|
+
}
|
|
977
|
+
updateActionId(nodeId, actionId) {
|
|
978
|
+
this.updateNode(nodeId, (node) => ({ ...node, actionId }));
|
|
979
|
+
}
|
|
980
|
+
/** Updates a node in the tree and emits the change */
|
|
981
|
+
updateNode(nodeId, updater) {
|
|
982
|
+
const tree = this._builderTree();
|
|
983
|
+
if (!tree)
|
|
984
|
+
return;
|
|
985
|
+
this.updateTreeAndEmit(this.updateNodeInTree(tree, nodeId, updater));
|
|
986
|
+
}
|
|
987
|
+
/** Sets the tree and emits the change */
|
|
988
|
+
updateTreeAndEmit(tree) {
|
|
989
|
+
this._builderTree.set(tree);
|
|
990
|
+
this.logicChange.emit(toLogicNode(tree));
|
|
991
|
+
}
|
|
992
|
+
updateNodeInTree(node, targetId, updater) {
|
|
993
|
+
if (node.id === targetId)
|
|
994
|
+
return updater(node);
|
|
995
|
+
if (!node.children)
|
|
996
|
+
return node;
|
|
997
|
+
return {
|
|
998
|
+
...node,
|
|
999
|
+
children: node.children.map((child) => this.updateNodeInTree(child, targetId, updater)),
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
removeNodeFromTree(node, targetId) {
|
|
1003
|
+
if (!node.children)
|
|
1004
|
+
return node;
|
|
1005
|
+
return {
|
|
1006
|
+
...node,
|
|
1007
|
+
children: node.children
|
|
1008
|
+
.filter((child) => child.id !== targetId)
|
|
1009
|
+
.map((child) => this.removeNodeFromTree(child, targetId)),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LogicBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1013
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: LogicBuilderComponent, isStandalone: true, selector: "lib-logic-builder", inputs: { logic: { classPropertyName: "logic", publicName: "logic", isSignal: true, isRequired: false, transformFunction: null }, actions: { classPropertyName: "actions", publicName: "actions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { logicChange: "logicChange" }, ngImport: i0, template: `
|
|
1014
|
+
<div class="logic-builder">
|
|
1015
|
+
<div class="flex justify-between items-center mb-3">
|
|
1016
|
+
<h4 class="text-sm font-semibold m-0">
|
|
1017
|
+
{{ 'iam.logic.title' | translate }}
|
|
1018
|
+
</h4>
|
|
1019
|
+
@if (!builderLogic()) {
|
|
1020
|
+
<button
|
|
1021
|
+
fButton
|
|
1022
|
+
size="small"
|
|
1023
|
+
(click)="initializeLogic()"
|
|
1024
|
+
>
|
|
1025
|
+
<ui-icon pButtonIcon name="plus"></ui-icon>
|
|
1026
|
+
<span pButtonLabel>{{ 'iam.logic.add.logic' | translate }}</span>
|
|
1027
|
+
</button>
|
|
1028
|
+
} @else {
|
|
1029
|
+
<button
|
|
1030
|
+
fButton
|
|
1031
|
+
size="small"
|
|
1032
|
+
severity="danger"
|
|
1033
|
+
[outlined]="true"
|
|
1034
|
+
(click)="clearLogic()"
|
|
1035
|
+
>
|
|
1036
|
+
<ui-icon pButtonIcon name="trash"></ui-icon>
|
|
1037
|
+
<span pButtonLabel>{{ 'iam.logic.clear.logic' | translate }}</span>
|
|
1038
|
+
</button>
|
|
1039
|
+
}
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
@if (builderLogic()) {
|
|
1043
|
+
<div class="border border-surface rounded p-3 bg-surface-50">
|
|
1044
|
+
<div class="mb-3 text-sm text-muted-color">
|
|
1045
|
+
{{ 'iam.logic.description' | translate }}
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
<!-- Root Node -->
|
|
1049
|
+
<div class="text-sm bg-surface-0 rounded border border-surface">
|
|
1050
|
+
<ng-container
|
|
1051
|
+
*ngTemplateOutlet="
|
|
1052
|
+
nodeTemplate;
|
|
1053
|
+
context: { $implicit: builderLogic()!, depth: 0 }
|
|
1054
|
+
"
|
|
1055
|
+
></ng-container>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
}
|
|
1059
|
+
</div>
|
|
1060
|
+
|
|
1061
|
+
<!-- Recursive Node Template -->
|
|
1062
|
+
<ng-template #nodeTemplate let-node let-depth="depth">
|
|
1063
|
+
<div
|
|
1064
|
+
class="p-3 border-b border-surface"
|
|
1065
|
+
[class.bg-surface-0]="depth % 2 === 0"
|
|
1066
|
+
[class.bg-surface-50]="depth % 2 !== 0"
|
|
1067
|
+
>
|
|
1068
|
+
<div class="flex items-center gap-3 mb-2">
|
|
1069
|
+
<span
|
|
1070
|
+
class="node-type"
|
|
1071
|
+
[class.group]="node.type === 'group'"
|
|
1072
|
+
[class.action]="node.type === 'action'"
|
|
1073
|
+
>{{ node.type.toUpperCase() }}</span
|
|
1074
|
+
>
|
|
1075
|
+
|
|
1076
|
+
<div class="flex-1 flex items-center gap-2">
|
|
1077
|
+
@if (node.type === 'group') {
|
|
1078
|
+
<!-- Group: show operator toggle -->
|
|
1079
|
+
<span
|
|
1080
|
+
class="operator-badge"
|
|
1081
|
+
[class.and]="node.operator === 'AND'"
|
|
1082
|
+
[class.or]="node.operator !== 'AND'"
|
|
1083
|
+
(click)="toggleOperator(node.id)"
|
|
1084
|
+
[fTooltip]="'iam.logic.click.to.toggle' | translate"
|
|
1085
|
+
>
|
|
1086
|
+
{{ node.operator }}
|
|
1087
|
+
</span>
|
|
1088
|
+
<span class="text-muted-color text-xs"
|
|
1089
|
+
>({{
|
|
1090
|
+
'iam.logic.conditions'
|
|
1091
|
+
| translate: { count: node.children?.length || 0 }
|
|
1092
|
+
}})</span
|
|
1093
|
+
>
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
@if (node.type === 'action') {
|
|
1097
|
+
<f-select
|
|
1098
|
+
class="flex-1"
|
|
1099
|
+
[options]="actions()"
|
|
1100
|
+
optionLabel="name"
|
|
1101
|
+
optionValue="id"
|
|
1102
|
+
[ngModel]="node.actionId"
|
|
1103
|
+
(ngModelChange)="updateActionId(node.id, $event)"
|
|
1104
|
+
[placeholder]="'iam.logic.select.action' | translate"
|
|
1105
|
+
[filter]="true"
|
|
1106
|
+
/>
|
|
1107
|
+
}
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
<div class="flex gap-1">
|
|
1111
|
+
<button
|
|
1112
|
+
fButton
|
|
1113
|
+
[text]="true"
|
|
1114
|
+
severity="danger"
|
|
1115
|
+
size="small"
|
|
1116
|
+
(click)="removeNode(node.id)"
|
|
1117
|
+
[fTooltip]="'shared.remove' | translate"
|
|
1118
|
+
>
|
|
1119
|
+
<ui-icon pButtonIcon name="trash"></ui-icon>
|
|
1120
|
+
</button>
|
|
1121
|
+
</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
|
|
1124
|
+
<!-- Children for group nodes -->
|
|
1125
|
+
@if (
|
|
1126
|
+
node.type === 'group' && node.children && node.children.length > 0
|
|
1127
|
+
) {
|
|
1128
|
+
<div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
|
|
1129
|
+
@for (child of node.children; track child.id) {
|
|
1130
|
+
<ng-container
|
|
1131
|
+
*ngTemplateOutlet="
|
|
1132
|
+
nodeTemplate;
|
|
1133
|
+
context: { $implicit: child, depth: depth + 1 }
|
|
1134
|
+
"
|
|
1135
|
+
></ng-container>
|
|
1136
|
+
}
|
|
1137
|
+
</div>
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
<!-- Add child buttons for group nodes -->
|
|
1141
|
+
@if (node.type === 'group') {
|
|
1142
|
+
<div
|
|
1143
|
+
class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface"
|
|
1144
|
+
>
|
|
1145
|
+
<div class="text-xs font-semibold text-muted-color mb-2">
|
|
1146
|
+
{{ 'iam.logic.add.condition' | translate }}
|
|
1147
|
+
</div>
|
|
1148
|
+
<div class="flex gap-2 flex-wrap">
|
|
1149
|
+
<button
|
|
1150
|
+
fButton
|
|
1151
|
+
size="small"
|
|
1152
|
+
[outlined]="true"
|
|
1153
|
+
(click)="addChildNode(node.id, 'group')"
|
|
1154
|
+
>
|
|
1155
|
+
<ui-icon pButtonIcon name="sitemap"></ui-icon>
|
|
1156
|
+
<span pButtonLabel>{{ 'iam.logic.group' | translate }}</span>
|
|
1157
|
+
</button>
|
|
1158
|
+
<button
|
|
1159
|
+
fButton
|
|
1160
|
+
size="small"
|
|
1161
|
+
severity="success"
|
|
1162
|
+
[outlined]="true"
|
|
1163
|
+
(click)="addChildNode(node.id, 'action')"
|
|
1164
|
+
>
|
|
1165
|
+
<ui-icon pButtonIcon name="bolt"></ui-icon>
|
|
1166
|
+
<span pButtonLabel>{{ 'iam.logic.action' | translate }}</span>
|
|
1167
|
+
</button>
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
}
|
|
1171
|
+
</div>
|
|
1172
|
+
</ng-template>
|
|
1173
|
+
`, isInline: true, styles: [":host{display:block}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:var(--f-blue-100, #dbeafe);color:var(--f-blue-700, #1e40af)}.node-type.action{background-color:var(--f-green-100, #d1fae5);color:var(--f-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--f-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--f-green-400, #4ade80)}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:opacity .2s}.operator-badge.and{background-color:var(--f-yellow-100, #fef3c7);color:var(--f-yellow-700, #92400e)}.operator-badge.or{background-color:var(--f-indigo-100, #e0e7ff);color:var(--f-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--f-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--f-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--f-surface-0, #ffffff);color:var(--f-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--f-surface-900, #1f2937);color:var(--f-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--f-primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
1174
|
+
}
|
|
1175
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: LogicBuilderComponent, decorators: [{
|
|
1176
|
+
type: Component,
|
|
1177
|
+
args: [{ selector: 'lib-logic-builder', imports: [AngularModule, UiModule, TranslatePipe], template: `
|
|
1178
|
+
<div class="logic-builder">
|
|
1179
|
+
<div class="flex justify-between items-center mb-3">
|
|
1180
|
+
<h4 class="text-sm font-semibold m-0">
|
|
1181
|
+
{{ 'iam.logic.title' | translate }}
|
|
1182
|
+
</h4>
|
|
1183
|
+
@if (!builderLogic()) {
|
|
1184
|
+
<button
|
|
1185
|
+
fButton
|
|
1186
|
+
size="small"
|
|
1187
|
+
(click)="initializeLogic()"
|
|
1188
|
+
>
|
|
1189
|
+
<ui-icon pButtonIcon name="plus"></ui-icon>
|
|
1190
|
+
<span pButtonLabel>{{ 'iam.logic.add.logic' | translate }}</span>
|
|
1191
|
+
</button>
|
|
1192
|
+
} @else {
|
|
1193
|
+
<button
|
|
1194
|
+
fButton
|
|
1195
|
+
size="small"
|
|
1196
|
+
severity="danger"
|
|
1197
|
+
[outlined]="true"
|
|
1198
|
+
(click)="clearLogic()"
|
|
1199
|
+
>
|
|
1200
|
+
<ui-icon pButtonIcon name="trash"></ui-icon>
|
|
1201
|
+
<span pButtonLabel>{{ 'iam.logic.clear.logic' | translate }}</span>
|
|
1202
|
+
</button>
|
|
1203
|
+
}
|
|
1204
|
+
</div>
|
|
1205
|
+
|
|
1206
|
+
@if (builderLogic()) {
|
|
1207
|
+
<div class="border border-surface rounded p-3 bg-surface-50">
|
|
1208
|
+
<div class="mb-3 text-sm text-muted-color">
|
|
1209
|
+
{{ 'iam.logic.description' | translate }}
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<!-- Root Node -->
|
|
1213
|
+
<div class="text-sm bg-surface-0 rounded border border-surface">
|
|
1214
|
+
<ng-container
|
|
1215
|
+
*ngTemplateOutlet="
|
|
1216
|
+
nodeTemplate;
|
|
1217
|
+
context: { $implicit: builderLogic()!, depth: 0 }
|
|
1218
|
+
"
|
|
1219
|
+
></ng-container>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
}
|
|
1223
|
+
</div>
|
|
1224
|
+
|
|
1225
|
+
<!-- Recursive Node Template -->
|
|
1226
|
+
<ng-template #nodeTemplate let-node let-depth="depth">
|
|
1227
|
+
<div
|
|
1228
|
+
class="p-3 border-b border-surface"
|
|
1229
|
+
[class.bg-surface-0]="depth % 2 === 0"
|
|
1230
|
+
[class.bg-surface-50]="depth % 2 !== 0"
|
|
1231
|
+
>
|
|
1232
|
+
<div class="flex items-center gap-3 mb-2">
|
|
1233
|
+
<span
|
|
1234
|
+
class="node-type"
|
|
1235
|
+
[class.group]="node.type === 'group'"
|
|
1236
|
+
[class.action]="node.type === 'action'"
|
|
1237
|
+
>{{ node.type.toUpperCase() }}</span
|
|
1238
|
+
>
|
|
1239
|
+
|
|
1240
|
+
<div class="flex-1 flex items-center gap-2">
|
|
1241
|
+
@if (node.type === 'group') {
|
|
1242
|
+
<!-- Group: show operator toggle -->
|
|
1243
|
+
<span
|
|
1244
|
+
class="operator-badge"
|
|
1245
|
+
[class.and]="node.operator === 'AND'"
|
|
1246
|
+
[class.or]="node.operator !== 'AND'"
|
|
1247
|
+
(click)="toggleOperator(node.id)"
|
|
1248
|
+
[fTooltip]="'iam.logic.click.to.toggle' | translate"
|
|
1249
|
+
>
|
|
1250
|
+
{{ node.operator }}
|
|
1251
|
+
</span>
|
|
1252
|
+
<span class="text-muted-color text-xs"
|
|
1253
|
+
>({{
|
|
1254
|
+
'iam.logic.conditions'
|
|
1255
|
+
| translate: { count: node.children?.length || 0 }
|
|
1256
|
+
}})</span
|
|
1257
|
+
>
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
@if (node.type === 'action') {
|
|
1261
|
+
<f-select
|
|
1262
|
+
class="flex-1"
|
|
1263
|
+
[options]="actions()"
|
|
1264
|
+
optionLabel="name"
|
|
1265
|
+
optionValue="id"
|
|
1266
|
+
[ngModel]="node.actionId"
|
|
1267
|
+
(ngModelChange)="updateActionId(node.id, $event)"
|
|
1268
|
+
[placeholder]="'iam.logic.select.action' | translate"
|
|
1269
|
+
[filter]="true"
|
|
1270
|
+
/>
|
|
1271
|
+
}
|
|
1272
|
+
</div>
|
|
1273
|
+
|
|
1274
|
+
<div class="flex gap-1">
|
|
1275
|
+
<button
|
|
1276
|
+
fButton
|
|
1277
|
+
[text]="true"
|
|
1278
|
+
severity="danger"
|
|
1279
|
+
size="small"
|
|
1280
|
+
(click)="removeNode(node.id)"
|
|
1281
|
+
[fTooltip]="'shared.remove' | translate"
|
|
1282
|
+
>
|
|
1283
|
+
<ui-icon pButtonIcon name="trash"></ui-icon>
|
|
1284
|
+
</button>
|
|
1285
|
+
</div>
|
|
1286
|
+
</div>
|
|
1287
|
+
|
|
1288
|
+
<!-- Children for group nodes -->
|
|
1289
|
+
@if (
|
|
1290
|
+
node.type === 'group' && node.children && node.children.length > 0
|
|
1291
|
+
) {
|
|
1292
|
+
<div class="ml-5 mt-2 pl-3 border-l-2 border-surface">
|
|
1293
|
+
@for (child of node.children; track child.id) {
|
|
1294
|
+
<ng-container
|
|
1295
|
+
*ngTemplateOutlet="
|
|
1296
|
+
nodeTemplate;
|
|
1297
|
+
context: { $implicit: child, depth: depth + 1 }
|
|
1298
|
+
"
|
|
1299
|
+
></ng-container>
|
|
1300
|
+
}
|
|
1301
|
+
</div>
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
<!-- Add child buttons for group nodes -->
|
|
1305
|
+
@if (node.type === 'group') {
|
|
1306
|
+
<div
|
|
1307
|
+
class="mt-3 p-3 bg-surface-50 rounded border border-dashed border-surface"
|
|
1308
|
+
>
|
|
1309
|
+
<div class="text-xs font-semibold text-muted-color mb-2">
|
|
1310
|
+
{{ 'iam.logic.add.condition' | translate }}
|
|
1311
|
+
</div>
|
|
1312
|
+
<div class="flex gap-2 flex-wrap">
|
|
1313
|
+
<button
|
|
1314
|
+
fButton
|
|
1315
|
+
size="small"
|
|
1316
|
+
[outlined]="true"
|
|
1317
|
+
(click)="addChildNode(node.id, 'group')"
|
|
1318
|
+
>
|
|
1319
|
+
<ui-icon pButtonIcon name="sitemap"></ui-icon>
|
|
1320
|
+
<span pButtonLabel>{{ 'iam.logic.group' | translate }}</span>
|
|
1321
|
+
</button>
|
|
1322
|
+
<button
|
|
1323
|
+
fButton
|
|
1324
|
+
size="small"
|
|
1325
|
+
severity="success"
|
|
1326
|
+
[outlined]="true"
|
|
1327
|
+
(click)="addChildNode(node.id, 'action')"
|
|
1328
|
+
>
|
|
1329
|
+
<ui-icon pButtonIcon name="bolt"></ui-icon>
|
|
1330
|
+
<span pButtonLabel>{{ 'iam.logic.action' | translate }}</span>
|
|
1331
|
+
</button>
|
|
1332
|
+
</div>
|
|
1333
|
+
</div>
|
|
1334
|
+
}
|
|
1335
|
+
</div>
|
|
1336
|
+
</ng-template>
|
|
1337
|
+
`, styles: [":host{display:block}.node-type{display:inline-flex;align-items:center;padding:.25rem .5rem;border-radius:.25rem;font-weight:600;font-size:.75rem;text-transform:uppercase}.node-type.group{background-color:var(--f-blue-100, #dbeafe);color:var(--f-blue-700, #1e40af)}.node-type.action{background-color:var(--f-green-100, #d1fae5);color:var(--f-green-700, #065f46)}:host-context(.p-dark) .node-type.group{background-color:#3b82f633;color:var(--f-blue-400, #60a5fa)}:host-context(.p-dark) .node-type.action{background-color:#22c55e33;color:var(--f-green-400, #4ade80)}.operator-badge{display:inline-flex;align-items:center;padding:.125rem .5rem;border-radius:9999px;font-weight:600;font-size:.75rem;cursor:pointer;transition:opacity .2s}.operator-badge.and{background-color:var(--f-yellow-100, #fef3c7);color:var(--f-yellow-700, #92400e)}.operator-badge.or{background-color:var(--f-indigo-100, #e0e7ff);color:var(--f-indigo-700, #3730a3)}:host-context(.p-dark) .operator-badge.and{background-color:#eab30833;color:var(--f-yellow-400, #facc15)}:host-context(.p-dark) .operator-badge.or{background-color:#6366f133;color:var(--f-indigo-400, #818cf8)}.operator-badge:hover{opacity:.8}.action-select{background-color:var(--f-surface-0, #ffffff);color:var(--f-text-color, #1f2937)}:host-context(.p-dark) .action-select{background-color:var(--f-surface-900, #1f2937);color:var(--f-text-color, #f9fafb)}.action-select:focus{outline:none;border-color:var(--f-primary-color, #3b82f6)}\n"] }]
|
|
1338
|
+
}], ctorParameters: () => [], propDecorators: { logic: [{ type: i0.Input, args: [{ isSignal: true, alias: "logic", required: false }] }], actions: [{ type: i0.Input, args: [{ isSignal: true, alias: "actions", required: false }] }], logicChange: [{ type: i0.Output, args: ["logicChange"] }] } });
|
|
1339
|
+
|
|
1340
|
+
function flattenTree(tree) {
|
|
1341
|
+
if (!tree?.length)
|
|
1342
|
+
return [];
|
|
1343
|
+
const result = [];
|
|
1344
|
+
const flatten = (nodes) => {
|
|
1345
|
+
for (const node of nodes) {
|
|
1346
|
+
result.push(node);
|
|
1347
|
+
if (node.children?.length)
|
|
1348
|
+
flatten(node.children);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
flatten(tree);
|
|
1352
|
+
return result;
|
|
1353
|
+
}
|
|
1354
|
+
function buildTreeFromFlat(flatList) {
|
|
1355
|
+
if (!flatList?.length)
|
|
1356
|
+
return [];
|
|
1357
|
+
const itemMap = new Map();
|
|
1358
|
+
const rootItems = [];
|
|
1359
|
+
flatList.forEach((item) => {
|
|
1360
|
+
if (item.id)
|
|
1361
|
+
itemMap.set(item.id, { ...item, children: [] });
|
|
1362
|
+
});
|
|
1363
|
+
flatList.forEach((item) => {
|
|
1364
|
+
if (!item.id)
|
|
1365
|
+
return;
|
|
1366
|
+
const node = itemMap.get(item.id);
|
|
1367
|
+
if (!node)
|
|
1368
|
+
return;
|
|
1369
|
+
if (!item.parentId) {
|
|
1370
|
+
rootItems.push(node);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
const parent = itemMap.get(item.parentId);
|
|
1374
|
+
if (parent) {
|
|
1375
|
+
parent.children.push(node);
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
rootItems.push(node);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
return rootItems;
|
|
1383
|
+
}
|
|
1384
|
+
function convertActionToTreeNode(actions) {
|
|
1385
|
+
if (!actions?.length)
|
|
1386
|
+
return [];
|
|
1387
|
+
const convert = (action) => {
|
|
1388
|
+
const { children, ...actionData } = action;
|
|
1389
|
+
return {
|
|
1390
|
+
data: actionData,
|
|
1391
|
+
children: children?.map(convert) ?? [],
|
|
1392
|
+
leaf: !children?.length,
|
|
1393
|
+
expanded: false,
|
|
1394
|
+
};
|
|
1395
|
+
};
|
|
1396
|
+
return actions.map(convert);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
class BaseActionSelectorComponent {
|
|
1400
|
+
permissionLogic = inject(ActionPermissionLogicService);
|
|
1401
|
+
translateAdapter = inject(TranslateService, {
|
|
1402
|
+
optional: true,
|
|
1403
|
+
});
|
|
1404
|
+
loading = signal(false, /* @ts-ignore */
|
|
1405
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1406
|
+
saving = signal(false, /* @ts-ignore */
|
|
1407
|
+
...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
1408
|
+
_actions = signal([], /* @ts-ignore */
|
|
1409
|
+
...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
|
|
1410
|
+
actions = this._actions.asReadonly();
|
|
1411
|
+
_actionsTree = signal([], /* @ts-ignore */
|
|
1412
|
+
...(ngDevMode ? [{ debugName: "_actionsTree" }] : /* istanbul ignore next */ []));
|
|
1413
|
+
actionsTree = this._actionsTree.asReadonly();
|
|
1414
|
+
_selectionMap = signal({}, /* @ts-ignore */
|
|
1415
|
+
...(ngDevMode ? [{ debugName: "_selectionMap" }] : /* istanbul ignore next */ []));
|
|
1416
|
+
selectionMap = this._selectionMap.asReadonly();
|
|
1417
|
+
_initialSelection = signal({}, /* @ts-ignore */
|
|
1418
|
+
...(ngDevMode ? [{ debugName: "_initialSelection" }] : /* istanbul ignore next */ []));
|
|
1419
|
+
initialSelection = this._initialSelection.asReadonly();
|
|
1420
|
+
treeNodes = computed(() => convertActionToTreeNode(this.actionsTree()), /* @ts-ignore */
|
|
1421
|
+
...(ngDevMode ? [{ debugName: "treeNodes" }] : /* istanbul ignore next */ []));
|
|
1422
|
+
allSelected = computed(() => {
|
|
1423
|
+
const selMap = this.selectionMap();
|
|
1424
|
+
const total = this.actions().length;
|
|
1425
|
+
if (total === 0)
|
|
1426
|
+
return false;
|
|
1427
|
+
return Object.values(selMap).filter((v) => v).length === total;
|
|
1428
|
+
}, /* @ts-ignore */
|
|
1429
|
+
...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
|
|
1430
|
+
hasChanges = computed(() => {
|
|
1431
|
+
const current = this.selectionMap();
|
|
1432
|
+
const initial = this.initialSelection();
|
|
1433
|
+
const currentKeys = Object.keys(current);
|
|
1434
|
+
if (currentKeys.length !== Object.keys(initial).length)
|
|
1435
|
+
return true;
|
|
1436
|
+
return currentKeys.some((key) => current[key] !== initial[key]);
|
|
1437
|
+
}, /* @ts-ignore */
|
|
1438
|
+
...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
|
|
1439
|
+
actionsWithUnmetPrerequisites = computed(() => {
|
|
1440
|
+
const selMap = this.selectionMap();
|
|
1441
|
+
const allActions = this.actions();
|
|
1442
|
+
const unmetSet = new Set();
|
|
1443
|
+
for (const action of allActions) {
|
|
1444
|
+
if (action.id &&
|
|
1445
|
+
this.permissionLogic.hasUnmetPrerequisites(action, selMap, allActions)) {
|
|
1446
|
+
unmetSet.add(action.id);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return unmetSet;
|
|
1450
|
+
}, /* @ts-ignore */
|
|
1451
|
+
...(ngDevMode ? [{ debugName: "actionsWithUnmetPrerequisites" }] : /* istanbul ignore next */ []));
|
|
1452
|
+
invalidActionsCount = computed(() => this.actionsWithUnmetPrerequisites().size, /* @ts-ignore */
|
|
1453
|
+
...(ngDevMode ? [{ debugName: "invalidActionsCount" }] : /* istanbul ignore next */ []));
|
|
1454
|
+
pendingAdd = computed(() => {
|
|
1455
|
+
const current = this.selectionMap();
|
|
1456
|
+
const initial = this.initialSelection();
|
|
1457
|
+
return this.actions().filter((a) => current[a.id] && !initial[a.id]);
|
|
1458
|
+
}, /* @ts-ignore */
|
|
1459
|
+
...(ngDevMode ? [{ debugName: "pendingAdd" }] : /* istanbul ignore next */ []));
|
|
1460
|
+
pendingRemove = computed(() => {
|
|
1461
|
+
const current = this.selectionMap();
|
|
1462
|
+
const initial = this.initialSelection();
|
|
1463
|
+
return this.actions().filter((a) => !current[a.id] && initial[a.id]);
|
|
1464
|
+
}, /* @ts-ignore */
|
|
1465
|
+
...(ngDevMode ? [{ debugName: "pendingRemove" }] : /* istanbul ignore next */ []));
|
|
1466
|
+
canSave = computed(() => this.hasChanges() && !this.saving() && this.invalidActionsCount() === 0, /* @ts-ignore */
|
|
1467
|
+
...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
1468
|
+
translate(key, vars) {
|
|
1469
|
+
return this.translateAdapter?.translate(key, vars) ?? key;
|
|
1470
|
+
}
|
|
1471
|
+
hasUnmetPrerequisites(action) {
|
|
1472
|
+
return action.id
|
|
1473
|
+
? this.actionsWithUnmetPrerequisites().has(action.id)
|
|
1474
|
+
: false;
|
|
1475
|
+
}
|
|
1476
|
+
getTooltip(action) {
|
|
1477
|
+
const selMap = this.selectionMap();
|
|
1478
|
+
const isCurrentlySelected = selMap[action.id];
|
|
1479
|
+
if (action.permissionLogic) {
|
|
1480
|
+
const prerequisiteInfo = this.permissionLogic.getPrerequisiteTooltip(action, selMap, this.actions());
|
|
1481
|
+
if (prerequisiteInfo) {
|
|
1482
|
+
const actionHint = isCurrentlySelected
|
|
1483
|
+
? `\n\n---\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.remove.action')}`
|
|
1484
|
+
: `\n\n---\n[${this.translate('shared.add')}] ${this.translate('iam.tooltip.add.action')}`;
|
|
1485
|
+
return `${prerequisiteInfo}${actionHint}`;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return this.getDefaultTooltip(isCurrentlySelected);
|
|
1489
|
+
}
|
|
1490
|
+
onActionToggle(action, newValue) {
|
|
1491
|
+
if (!newValue) {
|
|
1492
|
+
this.permissionLogic.handleUncheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
this.permissionLogic.handleCheck(action, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap), (previousState) => this._selectionMap.set(previousState));
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
toggleAll() {
|
|
1499
|
+
this.setAllSelection(!this.allSelected());
|
|
1500
|
+
}
|
|
1501
|
+
selectAll() {
|
|
1502
|
+
this.setAllSelection(true);
|
|
1503
|
+
}
|
|
1504
|
+
deselectAll() {
|
|
1505
|
+
this.setAllSelection(false);
|
|
1506
|
+
}
|
|
1507
|
+
setAllSelection(value) {
|
|
1508
|
+
this._selectionMap.set(this.buildSelectionMap(this.actions(), () => value));
|
|
1509
|
+
}
|
|
1510
|
+
buildSelectionMap(actions, predicate) {
|
|
1511
|
+
const selMap = {};
|
|
1512
|
+
for (const action of actions) {
|
|
1513
|
+
selMap[action.id] = predicate(action);
|
|
1514
|
+
}
|
|
1515
|
+
return selMap;
|
|
1516
|
+
}
|
|
1517
|
+
buildPayloadItems() {
|
|
1518
|
+
const items = [];
|
|
1519
|
+
for (const action of this.pendingAdd()) {
|
|
1520
|
+
items.push({ id: action.id, action: 'add' });
|
|
1521
|
+
}
|
|
1522
|
+
for (const action of this.pendingRemove()) {
|
|
1523
|
+
items.push({ id: action.id, action: 'remove' });
|
|
1524
|
+
}
|
|
1525
|
+
return items;
|
|
1526
|
+
}
|
|
1527
|
+
resetState() {
|
|
1528
|
+
this._actionsTree.set([]);
|
|
1529
|
+
this._actions.set([]);
|
|
1530
|
+
this._selectionMap.set({});
|
|
1531
|
+
this._initialSelection.set({});
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
class RoleActionSelectorComponent extends BaseActionSelectorComponent {
|
|
1536
|
+
ROLE_ACTION_PERMISSIONS = ROLE_ACTION_PERMISSIONS;
|
|
1537
|
+
destroyRef = inject(DestroyRef);
|
|
1538
|
+
roleApi = inject(RoleApiService);
|
|
1539
|
+
actionApi = inject(ActionApiService);
|
|
1540
|
+
permissionApi = inject(PermissionApiService);
|
|
1541
|
+
messageService = inject(MessageService);
|
|
1542
|
+
selectedRoleId = signal(undefined, /* @ts-ignore */
|
|
1543
|
+
...(ngDevMode ? [{ debugName: "selectedRoleId" }] : /* istanbul ignore next */ []));
|
|
1544
|
+
roles = signal([], /* @ts-ignore */
|
|
1545
|
+
...(ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []));
|
|
1546
|
+
loadDataAbortController = null;
|
|
1547
|
+
constructor() {
|
|
1548
|
+
super();
|
|
1549
|
+
this.destroyRef.onDestroy(() => {
|
|
1550
|
+
this.loadDataAbortController?.abort();
|
|
1551
|
+
});
|
|
1552
|
+
this.loadRoles();
|
|
1553
|
+
effect(() => {
|
|
1554
|
+
const roleId = this.selectedRoleId();
|
|
1555
|
+
this.loadDataAbortController?.abort();
|
|
1556
|
+
if (roleId) {
|
|
1557
|
+
this.loadDataAbortController = new AbortController();
|
|
1558
|
+
this.onRoleChange(this.loadDataAbortController.signal).catch((err) => {
|
|
1559
|
+
if (err.name !== 'AbortError') {
|
|
1560
|
+
this.loading.set(false);
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
this.resetState();
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
getDefaultTooltip(isSelected) {
|
|
1570
|
+
if (isSelected) {
|
|
1571
|
+
return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.role')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.role')}`;
|
|
1572
|
+
}
|
|
1573
|
+
return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.role')}`;
|
|
1574
|
+
}
|
|
1575
|
+
async loadRoles() {
|
|
1576
|
+
try {
|
|
1577
|
+
const response = await this.roleApi.getAll({
|
|
1578
|
+
pagination: { currentPage: 0, pageSize: MAX_DROPDOWN_ITEMS },
|
|
1579
|
+
});
|
|
1580
|
+
this.roles.set(response?.data ?? []);
|
|
1581
|
+
}
|
|
1582
|
+
catch { }
|
|
1583
|
+
}
|
|
1584
|
+
async onRoleChange(signal) {
|
|
1585
|
+
const roleId = this.selectedRoleId();
|
|
1586
|
+
if (!roleId)
|
|
1587
|
+
return;
|
|
1588
|
+
this.loading.set(true);
|
|
1589
|
+
try {
|
|
1590
|
+
const actionsResponse = await firstValueFrom(this.actionApi.getActionsForPermission());
|
|
1591
|
+
if (signal.aborted)
|
|
1592
|
+
return;
|
|
1593
|
+
const flatActions = (actionsResponse?.data ?? []).filter((a) => !!a.id);
|
|
1594
|
+
const actionsTree = buildTreeFromFlat(flatActions);
|
|
1595
|
+
this._actionsTree.set(actionsTree);
|
|
1596
|
+
this._actions.set(flatActions);
|
|
1597
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getRoleActions(roleId));
|
|
1598
|
+
if (signal.aborted)
|
|
1599
|
+
return;
|
|
1600
|
+
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
1601
|
+
const selMap = this.buildSelectionMap(flatActions, (action) => assignedIds.has(action.id));
|
|
1602
|
+
this._selectionMap.set(selMap);
|
|
1603
|
+
this._initialSelection.set({ ...selMap });
|
|
1604
|
+
}
|
|
1605
|
+
finally {
|
|
1606
|
+
if (!signal.aborted) {
|
|
1607
|
+
this.loading.set(false);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
async saveChanges() {
|
|
1612
|
+
const roleId = this.selectedRoleId();
|
|
1613
|
+
if (!roleId)
|
|
1614
|
+
return;
|
|
1615
|
+
const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
|
|
1616
|
+
if (invalidActions.length > 0) {
|
|
1617
|
+
this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
const items = this.buildPayloadItems();
|
|
1621
|
+
if (items.length === 0)
|
|
1622
|
+
return;
|
|
1623
|
+
this.saving.set(true);
|
|
1624
|
+
try {
|
|
1625
|
+
const response = await firstValueFrom(this.permissionApi.assignRoleActions({ roleId, items }));
|
|
1626
|
+
this.messageService.add({
|
|
1627
|
+
severity: 'success',
|
|
1628
|
+
summary: this.translate('shared.success'),
|
|
1629
|
+
detail: response?.messageKey
|
|
1630
|
+
? this.translate(response.messageKey, response.messageVariables)
|
|
1631
|
+
: response?.message,
|
|
1632
|
+
});
|
|
1633
|
+
this._initialSelection.set({ ...this.selectionMap() });
|
|
1634
|
+
}
|
|
1635
|
+
catch {
|
|
1636
|
+
}
|
|
1637
|
+
finally {
|
|
1638
|
+
this.saving.set(false);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1642
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: RoleActionSelectorComponent, isStandalone: true, selector: "flusys-role-action-selector", usesInheritance: true, ngImport: i0, template: `
|
|
1643
|
+
<div class="role-action-selector">
|
|
1644
|
+
<div class="mb-4">
|
|
1645
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1646
|
+
<div>
|
|
1647
|
+
<label class="block font-semibold mb-2">{{
|
|
1648
|
+
'iam.permission.select.role' | translate
|
|
1649
|
+
}}</label>
|
|
1650
|
+
<f-select
|
|
1651
|
+
[ngModel]="selectedRoleId()"
|
|
1652
|
+
(ngModelChange)="selectedRoleId.set($event)"
|
|
1653
|
+
[options]="roles()"
|
|
1654
|
+
optionLabel="name"
|
|
1655
|
+
optionValue="id"
|
|
1656
|
+
[placeholder]="
|
|
1657
|
+
'iam.permission.select.role.placeholder' | translate
|
|
1658
|
+
"
|
|
1659
|
+
[showClear]="true"
|
|
1660
|
+
[filter]="true"
|
|
1661
|
+
class="w-full"
|
|
1662
|
+
/>
|
|
1663
|
+
</div>
|
|
1664
|
+
</div>
|
|
1665
|
+
</div>
|
|
1666
|
+
|
|
1667
|
+
@if (selectedRoleId()) {
|
|
1668
|
+
@if (loading()) {
|
|
1669
|
+
<div
|
|
1670
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
1671
|
+
>
|
|
1672
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
1673
|
+
</div>
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
@if (!loading() && actions().length > 0) {
|
|
1677
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
1678
|
+
<div
|
|
1679
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
1680
|
+
>
|
|
1681
|
+
<div>
|
|
1682
|
+
<h5 class="m-0 mb-1">
|
|
1683
|
+
{{ 'iam.permission.action.permissions' | translate }}
|
|
1684
|
+
</h5>
|
|
1685
|
+
<p class="text-sm text-muted-color m-0">
|
|
1686
|
+
{{
|
|
1687
|
+
'iam.permission.actions.available'
|
|
1688
|
+
| translate: { count: actions().length }
|
|
1689
|
+
}}
|
|
1690
|
+
</p>
|
|
1691
|
+
</div>
|
|
1692
|
+
<div class="flex flex-wrap gap-2">
|
|
1693
|
+
<button
|
|
1694
|
+
fButton
|
|
1695
|
+
[outlined]="true"
|
|
1696
|
+
size="small"
|
|
1697
|
+
severity="secondary"
|
|
1698
|
+
(click)="selectAll()"
|
|
1699
|
+
>
|
|
1700
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
1701
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
1702
|
+
</button>
|
|
1703
|
+
<button
|
|
1704
|
+
fButton
|
|
1705
|
+
[outlined]="true"
|
|
1706
|
+
size="small"
|
|
1707
|
+
severity="secondary"
|
|
1708
|
+
(click)="deselectAll()"
|
|
1709
|
+
>
|
|
1710
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
1711
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
1712
|
+
</button>
|
|
1713
|
+
<button
|
|
1714
|
+
fButton
|
|
1715
|
+
*hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
|
|
1716
|
+
[disabled]="!canSave() || saving()"
|
|
1717
|
+
size="small"
|
|
1718
|
+
(click)="saveChanges()"
|
|
1719
|
+
>
|
|
1720
|
+
@if (saving()) {
|
|
1721
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
1722
|
+
} @else {
|
|
1723
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
1724
|
+
}
|
|
1725
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
1726
|
+
</button>
|
|
1727
|
+
</div>
|
|
1728
|
+
</div>
|
|
1729
|
+
|
|
1730
|
+
@if (invalidActionsCount() > 0) {
|
|
1731
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
1732
|
+
<div class="flex items-start gap-2">
|
|
1733
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
1734
|
+
<div class="flex-1">
|
|
1735
|
+
<span class="font-semibold"
|
|
1736
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
1737
|
+
>
|
|
1738
|
+
<p class="text-sm mt-1 mb-0">
|
|
1739
|
+
{{
|
|
1740
|
+
(invalidActionsCount() > 1
|
|
1741
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
1742
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
1743
|
+
) | translate: { count: invalidActionsCount() }
|
|
1744
|
+
}}
|
|
1745
|
+
</p>
|
|
1746
|
+
</div>
|
|
1747
|
+
</div>
|
|
1748
|
+
</div>
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
1752
|
+
<f-treetable
|
|
1753
|
+
[value]="treeNodes()"
|
|
1754
|
+
dataKey="id"
|
|
1755
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
1756
|
+
>
|
|
1757
|
+
<ng-template #header>
|
|
1758
|
+
<tr>
|
|
1759
|
+
<th class="w-12">
|
|
1760
|
+
<f-checkbox
|
|
1761
|
+
[ngModel]="allSelected()"
|
|
1762
|
+
[binary]="true"
|
|
1763
|
+
(ngModelChange)="toggleAll()"
|
|
1764
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
1765
|
+
tooltipPosition="top"
|
|
1766
|
+
/>
|
|
1767
|
+
</th>
|
|
1768
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
1769
|
+
<th class="hidden md:table-cell">
|
|
1770
|
+
{{ 'shared.code' | translate }}
|
|
1771
|
+
</th>
|
|
1772
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
1773
|
+
<th class="hidden lg:table-cell">
|
|
1774
|
+
{{ 'iam.permission.requirements' | translate }}
|
|
1775
|
+
</th>
|
|
1776
|
+
</tr>
|
|
1777
|
+
</ng-template>
|
|
1778
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
1779
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
1780
|
+
@let tooltip = getTooltip(rowData);
|
|
1781
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
1782
|
+
<td class="w-12">
|
|
1783
|
+
<f-checkbox
|
|
1784
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
1785
|
+
[binary]="true"
|
|
1786
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
1787
|
+
[fTooltip]="tooltip"
|
|
1788
|
+
tooltipPosition="top"
|
|
1789
|
+
/>
|
|
1790
|
+
</td>
|
|
1791
|
+
<td>
|
|
1792
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
1793
|
+
<span class="inline-flex items-center gap-2">
|
|
1794
|
+
{{ rowData.name }}
|
|
1795
|
+
@if (hasUnmet) {
|
|
1796
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
1797
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
1798
|
+
| translate
|
|
1799
|
+
"
|
|
1800
|
+
tooltipPosition="top"></ui-icon>
|
|
1801
|
+
}
|
|
1802
|
+
</span>
|
|
1803
|
+
</td>
|
|
1804
|
+
<td class="hidden md:table-cell">
|
|
1805
|
+
{{ rowData.code || '-' }}
|
|
1806
|
+
</td>
|
|
1807
|
+
<td>
|
|
1808
|
+
<f-tag
|
|
1809
|
+
[value]="rowData.actionType"
|
|
1810
|
+
[severity]="
|
|
1811
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
1812
|
+
"
|
|
1813
|
+
/>
|
|
1814
|
+
</td>
|
|
1815
|
+
<td class="hidden lg:table-cell">
|
|
1816
|
+
@if (rowData.permissionLogic) {
|
|
1817
|
+
<span class="text-sm text-muted-color">{{
|
|
1818
|
+
'iam.permission.has.prerequisites' | translate
|
|
1819
|
+
}}</span>
|
|
1820
|
+
} @else {
|
|
1821
|
+
<span class="text-muted-color">-</span>
|
|
1822
|
+
}
|
|
1823
|
+
</td>
|
|
1824
|
+
</tr>
|
|
1825
|
+
</ng-template>
|
|
1826
|
+
<ng-template #emptymessage>
|
|
1827
|
+
<tr>
|
|
1828
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
1829
|
+
@if (loading()) {
|
|
1830
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
1831
|
+
{{ 'shared.loading.actions' | translate }}
|
|
1832
|
+
} @else {
|
|
1833
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
1834
|
+
}
|
|
1835
|
+
</td>
|
|
1836
|
+
</tr>
|
|
1837
|
+
</ng-template>
|
|
1838
|
+
</f-treetable>
|
|
1839
|
+
</div>
|
|
1840
|
+
</div>
|
|
1841
|
+
|
|
1842
|
+
@if (hasChanges()) {
|
|
1843
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
1844
|
+
<div class="flex items-center gap-2 mb-3">
|
|
1845
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
1846
|
+
<span class="font-bold">{{
|
|
1847
|
+
'shared.pending.changes' | translate
|
|
1848
|
+
}}</span>
|
|
1849
|
+
</div>
|
|
1850
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
1851
|
+
@if (pendingAdd().length > 0) {
|
|
1852
|
+
<div>
|
|
1853
|
+
<div class="flex items-center gap-2 mb-2">
|
|
1854
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
1855
|
+
<strong class="text-sm"
|
|
1856
|
+
>{{ 'shared.to.add' | translate }} ({{
|
|
1857
|
+
pendingAdd().length
|
|
1858
|
+
}})</strong
|
|
1859
|
+
>
|
|
1860
|
+
</div>
|
|
1861
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
1862
|
+
@for (action of pendingAdd(); track action.id) {
|
|
1863
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
1864
|
+
}
|
|
1865
|
+
</ul>
|
|
1866
|
+
</div>
|
|
1867
|
+
}
|
|
1868
|
+
@if (pendingRemove().length > 0) {
|
|
1869
|
+
<div>
|
|
1870
|
+
<div class="flex items-center gap-2 mb-2">
|
|
1871
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
1872
|
+
<strong class="text-sm"
|
|
1873
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
1874
|
+
pendingRemove().length
|
|
1875
|
+
}})</strong
|
|
1876
|
+
>
|
|
1877
|
+
</div>
|
|
1878
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
1879
|
+
@for (action of pendingRemove(); track action.id) {
|
|
1880
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
1881
|
+
}
|
|
1882
|
+
</ul>
|
|
1883
|
+
</div>
|
|
1884
|
+
}
|
|
1885
|
+
</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
@if (!loading() && actions().length === 0) {
|
|
1891
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
1892
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
1893
|
+
<p class="text-muted-color m-0">
|
|
1894
|
+
{{ 'iam.permission.no.actions.for.role' | translate }}
|
|
1895
|
+
</p>
|
|
1896
|
+
</div>
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
</div>
|
|
1900
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
1901
|
+
}
|
|
1902
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RoleActionSelectorComponent, decorators: [{
|
|
1903
|
+
type: Component,
|
|
1904
|
+
args: [{ selector: 'flusys-role-action-selector', imports: [FormsModule, UiModule, HasPermissionDirective, TranslatePipe], template: `
|
|
1905
|
+
<div class="role-action-selector">
|
|
1906
|
+
<div class="mb-4">
|
|
1907
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1908
|
+
<div>
|
|
1909
|
+
<label class="block font-semibold mb-2">{{
|
|
1910
|
+
'iam.permission.select.role' | translate
|
|
1911
|
+
}}</label>
|
|
1912
|
+
<f-select
|
|
1913
|
+
[ngModel]="selectedRoleId()"
|
|
1914
|
+
(ngModelChange)="selectedRoleId.set($event)"
|
|
1915
|
+
[options]="roles()"
|
|
1916
|
+
optionLabel="name"
|
|
1917
|
+
optionValue="id"
|
|
1918
|
+
[placeholder]="
|
|
1919
|
+
'iam.permission.select.role.placeholder' | translate
|
|
1920
|
+
"
|
|
1921
|
+
[showClear]="true"
|
|
1922
|
+
[filter]="true"
|
|
1923
|
+
class="w-full"
|
|
1924
|
+
/>
|
|
1925
|
+
</div>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
|
|
1929
|
+
@if (selectedRoleId()) {
|
|
1930
|
+
@if (loading()) {
|
|
1931
|
+
<div
|
|
1932
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
1933
|
+
>
|
|
1934
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
1935
|
+
</div>
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
@if (!loading() && actions().length > 0) {
|
|
1939
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
1940
|
+
<div
|
|
1941
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
1942
|
+
>
|
|
1943
|
+
<div>
|
|
1944
|
+
<h5 class="m-0 mb-1">
|
|
1945
|
+
{{ 'iam.permission.action.permissions' | translate }}
|
|
1946
|
+
</h5>
|
|
1947
|
+
<p class="text-sm text-muted-color m-0">
|
|
1948
|
+
{{
|
|
1949
|
+
'iam.permission.actions.available'
|
|
1950
|
+
| translate: { count: actions().length }
|
|
1951
|
+
}}
|
|
1952
|
+
</p>
|
|
1953
|
+
</div>
|
|
1954
|
+
<div class="flex flex-wrap gap-2">
|
|
1955
|
+
<button
|
|
1956
|
+
fButton
|
|
1957
|
+
[outlined]="true"
|
|
1958
|
+
size="small"
|
|
1959
|
+
severity="secondary"
|
|
1960
|
+
(click)="selectAll()"
|
|
1961
|
+
>
|
|
1962
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
1963
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
1964
|
+
</button>
|
|
1965
|
+
<button
|
|
1966
|
+
fButton
|
|
1967
|
+
[outlined]="true"
|
|
1968
|
+
size="small"
|
|
1969
|
+
severity="secondary"
|
|
1970
|
+
(click)="deselectAll()"
|
|
1971
|
+
>
|
|
1972
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
1973
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
1974
|
+
</button>
|
|
1975
|
+
<button
|
|
1976
|
+
fButton
|
|
1977
|
+
*hasPermission="ROLE_ACTION_PERMISSIONS.ASSIGN"
|
|
1978
|
+
[disabled]="!canSave() || saving()"
|
|
1979
|
+
size="small"
|
|
1980
|
+
(click)="saveChanges()"
|
|
1981
|
+
>
|
|
1982
|
+
@if (saving()) {
|
|
1983
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
1984
|
+
} @else {
|
|
1985
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
1986
|
+
}
|
|
1987
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
1988
|
+
</button>
|
|
1989
|
+
</div>
|
|
1990
|
+
</div>
|
|
1991
|
+
|
|
1992
|
+
@if (invalidActionsCount() > 0) {
|
|
1993
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
1994
|
+
<div class="flex items-start gap-2">
|
|
1995
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
1996
|
+
<div class="flex-1">
|
|
1997
|
+
<span class="font-semibold"
|
|
1998
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
1999
|
+
>
|
|
2000
|
+
<p class="text-sm mt-1 mb-0">
|
|
2001
|
+
{{
|
|
2002
|
+
(invalidActionsCount() > 1
|
|
2003
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
2004
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
2005
|
+
) | translate: { count: invalidActionsCount() }
|
|
2006
|
+
}}
|
|
2007
|
+
</p>
|
|
2008
|
+
</div>
|
|
2009
|
+
</div>
|
|
2010
|
+
</div>
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2014
|
+
<f-treetable
|
|
2015
|
+
[value]="treeNodes()"
|
|
2016
|
+
dataKey="id"
|
|
2017
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
2018
|
+
>
|
|
2019
|
+
<ng-template #header>
|
|
2020
|
+
<tr>
|
|
2021
|
+
<th class="w-12">
|
|
2022
|
+
<f-checkbox
|
|
2023
|
+
[ngModel]="allSelected()"
|
|
2024
|
+
[binary]="true"
|
|
2025
|
+
(ngModelChange)="toggleAll()"
|
|
2026
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
2027
|
+
tooltipPosition="top"
|
|
2028
|
+
/>
|
|
2029
|
+
</th>
|
|
2030
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
2031
|
+
<th class="hidden md:table-cell">
|
|
2032
|
+
{{ 'shared.code' | translate }}
|
|
2033
|
+
</th>
|
|
2034
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
2035
|
+
<th class="hidden lg:table-cell">
|
|
2036
|
+
{{ 'iam.permission.requirements' | translate }}
|
|
2037
|
+
</th>
|
|
2038
|
+
</tr>
|
|
2039
|
+
</ng-template>
|
|
2040
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
2041
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
2042
|
+
@let tooltip = getTooltip(rowData);
|
|
2043
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
2044
|
+
<td class="w-12">
|
|
2045
|
+
<f-checkbox
|
|
2046
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
2047
|
+
[binary]="true"
|
|
2048
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
2049
|
+
[fTooltip]="tooltip"
|
|
2050
|
+
tooltipPosition="top"
|
|
2051
|
+
/>
|
|
2052
|
+
</td>
|
|
2053
|
+
<td>
|
|
2054
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
2055
|
+
<span class="inline-flex items-center gap-2">
|
|
2056
|
+
{{ rowData.name }}
|
|
2057
|
+
@if (hasUnmet) {
|
|
2058
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
2059
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
2060
|
+
| translate
|
|
2061
|
+
"
|
|
2062
|
+
tooltipPosition="top"></ui-icon>
|
|
2063
|
+
}
|
|
2064
|
+
</span>
|
|
2065
|
+
</td>
|
|
2066
|
+
<td class="hidden md:table-cell">
|
|
2067
|
+
{{ rowData.code || '-' }}
|
|
2068
|
+
</td>
|
|
2069
|
+
<td>
|
|
2070
|
+
<f-tag
|
|
2071
|
+
[value]="rowData.actionType"
|
|
2072
|
+
[severity]="
|
|
2073
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
2074
|
+
"
|
|
2075
|
+
/>
|
|
2076
|
+
</td>
|
|
2077
|
+
<td class="hidden lg:table-cell">
|
|
2078
|
+
@if (rowData.permissionLogic) {
|
|
2079
|
+
<span class="text-sm text-muted-color">{{
|
|
2080
|
+
'iam.permission.has.prerequisites' | translate
|
|
2081
|
+
}}</span>
|
|
2082
|
+
} @else {
|
|
2083
|
+
<span class="text-muted-color">-</span>
|
|
2084
|
+
}
|
|
2085
|
+
</td>
|
|
2086
|
+
</tr>
|
|
2087
|
+
</ng-template>
|
|
2088
|
+
<ng-template #emptymessage>
|
|
2089
|
+
<tr>
|
|
2090
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2091
|
+
@if (loading()) {
|
|
2092
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
2093
|
+
{{ 'shared.loading.actions' | translate }}
|
|
2094
|
+
} @else {
|
|
2095
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
2096
|
+
}
|
|
2097
|
+
</td>
|
|
2098
|
+
</tr>
|
|
2099
|
+
</ng-template>
|
|
2100
|
+
</f-treetable>
|
|
2101
|
+
</div>
|
|
2102
|
+
</div>
|
|
2103
|
+
|
|
2104
|
+
@if (hasChanges()) {
|
|
2105
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
2106
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2107
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
2108
|
+
<span class="font-bold">{{
|
|
2109
|
+
'shared.pending.changes' | translate
|
|
2110
|
+
}}</span>
|
|
2111
|
+
</div>
|
|
2112
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2113
|
+
@if (pendingAdd().length > 0) {
|
|
2114
|
+
<div>
|
|
2115
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2116
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
2117
|
+
<strong class="text-sm"
|
|
2118
|
+
>{{ 'shared.to.add' | translate }} ({{
|
|
2119
|
+
pendingAdd().length
|
|
2120
|
+
}})</strong
|
|
2121
|
+
>
|
|
2122
|
+
</div>
|
|
2123
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2124
|
+
@for (action of pendingAdd(); track action.id) {
|
|
2125
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2126
|
+
}
|
|
2127
|
+
</ul>
|
|
2128
|
+
</div>
|
|
2129
|
+
}
|
|
2130
|
+
@if (pendingRemove().length > 0) {
|
|
2131
|
+
<div>
|
|
2132
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2133
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
2134
|
+
<strong class="text-sm"
|
|
2135
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
2136
|
+
pendingRemove().length
|
|
2137
|
+
}})</strong
|
|
2138
|
+
>
|
|
2139
|
+
</div>
|
|
2140
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2141
|
+
@for (action of pendingRemove(); track action.id) {
|
|
2142
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2143
|
+
}
|
|
2144
|
+
</ul>
|
|
2145
|
+
</div>
|
|
2146
|
+
}
|
|
2147
|
+
</div>
|
|
2148
|
+
</div>
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
@if (!loading() && actions().length === 0) {
|
|
2153
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
2154
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
2155
|
+
<p class="text-muted-color m-0">
|
|
2156
|
+
{{ 'iam.permission.no.actions.for.role' | translate }}
|
|
2157
|
+
</p>
|
|
2158
|
+
</div>
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
</div>
|
|
2162
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
2163
|
+
}], ctorParameters: () => [] });
|
|
2164
|
+
|
|
2165
|
+
class CompanyActionSelectorComponent extends BaseActionSelectorComponent {
|
|
2166
|
+
COMPANY_ACTION_PERMISSIONS = COMPANY_ACTION_PERMISSIONS;
|
|
2167
|
+
companyApiProvider = inject(COMPANY_API_PROVIDER);
|
|
2168
|
+
actionApi = inject(ActionApiService);
|
|
2169
|
+
permissionApi = inject(PermissionApiService);
|
|
2170
|
+
messageService = inject(MessageService);
|
|
2171
|
+
confirmationService = inject(ConfirmationService);
|
|
2172
|
+
destroyRef = inject(DestroyRef);
|
|
2173
|
+
selectedCompanyId = signal(undefined, /* @ts-ignore */
|
|
2174
|
+
...(ngDevMode ? [{ debugName: "selectedCompanyId" }] : /* istanbul ignore next */ []));
|
|
2175
|
+
companies = signal([], /* @ts-ignore */
|
|
2176
|
+
...(ngDevMode ? [{ debugName: "companies" }] : /* istanbul ignore next */ []));
|
|
2177
|
+
loadDataAbortController = null;
|
|
2178
|
+
constructor() {
|
|
2179
|
+
super();
|
|
2180
|
+
this.destroyRef.onDestroy(() => {
|
|
2181
|
+
this.loadDataAbortController?.abort();
|
|
2182
|
+
});
|
|
2183
|
+
this.loadCompanies();
|
|
2184
|
+
effect(() => {
|
|
2185
|
+
const companyId = this.selectedCompanyId();
|
|
2186
|
+
this.loadDataAbortController?.abort();
|
|
2187
|
+
if (companyId) {
|
|
2188
|
+
this.loadDataAbortController = new AbortController();
|
|
2189
|
+
this.loadData(this.loadDataAbortController.signal).catch((err) => {
|
|
2190
|
+
if (err.name !== 'AbortError') {
|
|
2191
|
+
this.loading.set(false);
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
else {
|
|
2196
|
+
this.resetState();
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
getDefaultTooltip(isSelected) {
|
|
2201
|
+
if (isSelected) {
|
|
2202
|
+
return `[${this.translate('iam.tooltip.selected')}] ${this.translate('iam.tooltip.selected')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove')}`;
|
|
2203
|
+
}
|
|
2204
|
+
return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.add')}`;
|
|
2205
|
+
}
|
|
2206
|
+
async loadCompanies() {
|
|
2207
|
+
try {
|
|
2208
|
+
const response = await firstValueFrom(this.companyApiProvider.getCompanies({
|
|
2209
|
+
page: 0,
|
|
2210
|
+
pageSize: MAX_DROPDOWN_ITEMS,
|
|
2211
|
+
}));
|
|
2212
|
+
this.companies.set(response?.data ?? []);
|
|
2213
|
+
}
|
|
2214
|
+
catch { }
|
|
2215
|
+
}
|
|
2216
|
+
async loadData(signal) {
|
|
2217
|
+
const companyId = this.selectedCompanyId();
|
|
2218
|
+
if (!companyId)
|
|
2219
|
+
return;
|
|
2220
|
+
this.loading.set(true);
|
|
2221
|
+
try {
|
|
2222
|
+
const actionsResponse = await firstValueFrom(this.actionApi.getTree(undefined, undefined, false));
|
|
2223
|
+
if (signal.aborted)
|
|
2224
|
+
return;
|
|
2225
|
+
const actionsTree = actionsResponse?.data ?? [];
|
|
2226
|
+
const actionsList = flattenTree(actionsTree).filter((a) => !!a.id);
|
|
2227
|
+
this._actionsTree.set(actionsTree);
|
|
2228
|
+
this._actions.set(actionsList);
|
|
2229
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getCompanyActions(companyId));
|
|
2230
|
+
if (signal.aborted)
|
|
2231
|
+
return;
|
|
2232
|
+
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
2233
|
+
const selMap = this.buildSelectionMap(actionsList, (action) => assignedIds.has(action.id));
|
|
2234
|
+
this._selectionMap.set(selMap);
|
|
2235
|
+
this._initialSelection.set({ ...selMap });
|
|
2236
|
+
}
|
|
2237
|
+
finally {
|
|
2238
|
+
if (!signal.aborted) {
|
|
2239
|
+
this.loading.set(false);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async saveChanges() {
|
|
2244
|
+
const companyId = this.selectedCompanyId();
|
|
2245
|
+
if (!companyId)
|
|
2246
|
+
return;
|
|
2247
|
+
const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
|
|
2248
|
+
if (invalidActions.length > 0) {
|
|
2249
|
+
this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const items = this.buildPayloadItems();
|
|
2253
|
+
if (items.length === 0)
|
|
2254
|
+
return;
|
|
2255
|
+
this.saving.set(true);
|
|
2256
|
+
try {
|
|
2257
|
+
const response = await firstValueFrom(this.permissionApi.assignCompanyActions({ companyId, items }));
|
|
2258
|
+
if (response?.data?.prerequisiteErrors &&
|
|
2259
|
+
response.data.prerequisiteErrors.length > 0) {
|
|
2260
|
+
this.handleBackendPrerequisiteErrors(response.data.prerequisiteErrors);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
this.messageService.add({
|
|
2264
|
+
severity: 'success',
|
|
2265
|
+
summary: this.translate('shared.success'),
|
|
2266
|
+
detail: response?.messageKey
|
|
2267
|
+
? this.translate(response.messageKey, response.messageVariables)
|
|
2268
|
+
: (response?.message ?? ''),
|
|
2269
|
+
});
|
|
2270
|
+
this._initialSelection.set({ ...this.selectionMap() });
|
|
2271
|
+
}
|
|
2272
|
+
catch {
|
|
2273
|
+
}
|
|
2274
|
+
finally {
|
|
2275
|
+
this.saving.set(false);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
handleBackendPrerequisiteErrors(prerequisiteErrors) {
|
|
2279
|
+
const errorList = prerequisiteErrors
|
|
2280
|
+
.map((error) => {
|
|
2281
|
+
const requiredNames = error.requiredActions
|
|
2282
|
+
.map((a) => a.actionName)
|
|
2283
|
+
.join(', ');
|
|
2284
|
+
return `${error.actionName} ${this.translate('iam.permission.requires')}: ${requiredNames}`;
|
|
2285
|
+
})
|
|
2286
|
+
.join('\n');
|
|
2287
|
+
this.confirmationService.confirm({
|
|
2288
|
+
header: this.translate('iam.permission.prerequisite.validation.failed'),
|
|
2289
|
+
message: `${this.translate('iam.permission.prerequisite.error.message')}\n\n${errorList}\n\n${this.translate('iam.permission.auto.select.prompt')}`,
|
|
2290
|
+
icon: 'exclamation-triangle',
|
|
2291
|
+
acceptLabel: this.translate('iam.permission.auto.select.required'),
|
|
2292
|
+
rejectLabel: this.translate('shared.cancel'),
|
|
2293
|
+
acceptIcon: 'check',
|
|
2294
|
+
rejectIcon: 'times',
|
|
2295
|
+
accept: () => {
|
|
2296
|
+
const selMap = { ...this.selectionMap() };
|
|
2297
|
+
prerequisiteErrors.forEach((error) => {
|
|
2298
|
+
error.requiredActions.forEach((a) => {
|
|
2299
|
+
selMap[a.actionId] = true;
|
|
2300
|
+
});
|
|
2301
|
+
});
|
|
2302
|
+
this._selectionMap.set(selMap);
|
|
2303
|
+
this.messageService.add({
|
|
2304
|
+
severity: 'info',
|
|
2305
|
+
summary: this.translate('iam.permission.actions.selected'),
|
|
2306
|
+
detail: this.translate('iam.permission.auto.selected.prerequisites'),
|
|
2307
|
+
});
|
|
2308
|
+
},
|
|
2309
|
+
reject: () => {
|
|
2310
|
+
this._selectionMap.set({ ...this.initialSelection() });
|
|
2311
|
+
this.messageService.add({
|
|
2312
|
+
severity: 'info',
|
|
2313
|
+
summary: this.translate('iam.permission.changes.reverted'),
|
|
2314
|
+
detail: this.translate('iam.permission.selection.reverted'),
|
|
2315
|
+
});
|
|
2316
|
+
},
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: CompanyActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2320
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: CompanyActionSelectorComponent, isStandalone: true, selector: "flusys-company-action-selector", usesInheritance: true, ngImport: i0, template: `
|
|
2321
|
+
<div class="company-action-selector">
|
|
2322
|
+
<div class="mb-4">
|
|
2323
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
2324
|
+
<div>
|
|
2325
|
+
<label class="block font-semibold mb-2">{{
|
|
2326
|
+
'iam.permission.select.company' | translate
|
|
2327
|
+
}}</label>
|
|
2328
|
+
<f-select
|
|
2329
|
+
[ngModel]="selectedCompanyId()"
|
|
2330
|
+
(ngModelChange)="selectedCompanyId.set($event)"
|
|
2331
|
+
[options]="companies()"
|
|
2332
|
+
optionLabel="name"
|
|
2333
|
+
optionValue="id"
|
|
2334
|
+
[placeholder]="
|
|
2335
|
+
'iam.permission.select.company.placeholder' | translate
|
|
2336
|
+
"
|
|
2337
|
+
[showClear]="true"
|
|
2338
|
+
[filter]="true"
|
|
2339
|
+
class="w-full"
|
|
2340
|
+
/>
|
|
2341
|
+
</div>
|
|
2342
|
+
</div>
|
|
2343
|
+
</div>
|
|
2344
|
+
|
|
2345
|
+
@if (selectedCompanyId()) {
|
|
2346
|
+
@if (loading()) {
|
|
2347
|
+
<div
|
|
2348
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
2349
|
+
>
|
|
2350
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
2351
|
+
</div>
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
@if (!loading() && actions().length > 0) {
|
|
2355
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
2356
|
+
<div
|
|
2357
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
2358
|
+
>
|
|
2359
|
+
<div>
|
|
2360
|
+
<h5 class="m-0 mb-1">
|
|
2361
|
+
{{ 'iam.permission.action.whitelist' | translate }}
|
|
2362
|
+
</h5>
|
|
2363
|
+
<p class="text-sm text-muted-color m-0">
|
|
2364
|
+
{{
|
|
2365
|
+
'iam.permission.actions.available'
|
|
2366
|
+
| translate: { count: actions().length }
|
|
2367
|
+
}}
|
|
2368
|
+
</p>
|
|
2369
|
+
</div>
|
|
2370
|
+
<div class="flex flex-wrap gap-2">
|
|
2371
|
+
<button
|
|
2372
|
+
fButton
|
|
2373
|
+
[outlined]="true"
|
|
2374
|
+
size="small"
|
|
2375
|
+
severity="secondary"
|
|
2376
|
+
(click)="selectAll()"
|
|
2377
|
+
>
|
|
2378
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
2379
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
2380
|
+
</button>
|
|
2381
|
+
<button
|
|
2382
|
+
fButton
|
|
2383
|
+
[outlined]="true"
|
|
2384
|
+
size="small"
|
|
2385
|
+
severity="secondary"
|
|
2386
|
+
(click)="deselectAll()"
|
|
2387
|
+
>
|
|
2388
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
2389
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
2390
|
+
</button>
|
|
2391
|
+
<button
|
|
2392
|
+
fButton
|
|
2393
|
+
*hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
|
|
2394
|
+
[disabled]="!canSave() || saving()"
|
|
2395
|
+
size="small"
|
|
2396
|
+
(click)="saveChanges()"
|
|
2397
|
+
>
|
|
2398
|
+
@if (saving()) {
|
|
2399
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
2400
|
+
} @else {
|
|
2401
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
2402
|
+
}
|
|
2403
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
2404
|
+
</button>
|
|
2405
|
+
</div>
|
|
2406
|
+
</div>
|
|
2407
|
+
|
|
2408
|
+
@if (invalidActionsCount() > 0) {
|
|
2409
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
2410
|
+
<div class="flex items-start gap-2">
|
|
2411
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
2412
|
+
<div class="flex-1">
|
|
2413
|
+
<span class="font-semibold"
|
|
2414
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
2415
|
+
>
|
|
2416
|
+
<p class="text-sm mt-1 mb-0">
|
|
2417
|
+
{{
|
|
2418
|
+
(invalidActionsCount() > 1
|
|
2419
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
2420
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
2421
|
+
) | translate: { count: invalidActionsCount() }
|
|
2422
|
+
}}
|
|
2423
|
+
</p>
|
|
2424
|
+
</div>
|
|
2425
|
+
</div>
|
|
2426
|
+
</div>
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2430
|
+
<f-treetable
|
|
2431
|
+
[value]="treeNodes()"
|
|
2432
|
+
dataKey="id"
|
|
2433
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
2434
|
+
>
|
|
2435
|
+
<ng-template #header>
|
|
2436
|
+
<tr>
|
|
2437
|
+
<th class="w-12">
|
|
2438
|
+
<f-checkbox
|
|
2439
|
+
[ngModel]="allSelected()"
|
|
2440
|
+
[binary]="true"
|
|
2441
|
+
(ngModelChange)="toggleAll()"
|
|
2442
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
2443
|
+
tooltipPosition="top"
|
|
2444
|
+
/>
|
|
2445
|
+
</th>
|
|
2446
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
2447
|
+
<th class="hidden md:table-cell">
|
|
2448
|
+
{{ 'shared.code' | translate }}
|
|
2449
|
+
</th>
|
|
2450
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
2451
|
+
<th class="hidden lg:table-cell">
|
|
2452
|
+
{{ 'shared.description' | translate }}
|
|
2453
|
+
</th>
|
|
2454
|
+
</tr>
|
|
2455
|
+
</ng-template>
|
|
2456
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
2457
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
2458
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
2459
|
+
<td class="w-12">
|
|
2460
|
+
<f-checkbox
|
|
2461
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
2462
|
+
[binary]="true"
|
|
2463
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
2464
|
+
[fTooltip]="getTooltip(rowData)"
|
|
2465
|
+
tooltipPosition="top"
|
|
2466
|
+
/>
|
|
2467
|
+
</td>
|
|
2468
|
+
<td>
|
|
2469
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
2470
|
+
<span class="inline-flex items-center gap-2">
|
|
2471
|
+
{{ rowData.name }}
|
|
2472
|
+
@if (hasUnmet) {
|
|
2473
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
2474
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
2475
|
+
| translate
|
|
2476
|
+
"
|
|
2477
|
+
tooltipPosition="top"></ui-icon>
|
|
2478
|
+
}
|
|
2479
|
+
</span>
|
|
2480
|
+
</td>
|
|
2481
|
+
<td class="hidden md:table-cell">
|
|
2482
|
+
{{ rowData.code || '-' }}
|
|
2483
|
+
</td>
|
|
2484
|
+
<td>
|
|
2485
|
+
<f-tag
|
|
2486
|
+
[value]="rowData.actionType"
|
|
2487
|
+
[severity]="
|
|
2488
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
2489
|
+
"
|
|
2490
|
+
/>
|
|
2491
|
+
</td>
|
|
2492
|
+
<td class="hidden lg:table-cell">
|
|
2493
|
+
{{ rowData.description || '-' }}
|
|
2494
|
+
</td>
|
|
2495
|
+
</tr>
|
|
2496
|
+
</ng-template>
|
|
2497
|
+
<ng-template #emptymessage>
|
|
2498
|
+
<tr>
|
|
2499
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2500
|
+
@if (loading()) {
|
|
2501
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
2502
|
+
{{ 'shared.loading.actions' | translate }}
|
|
2503
|
+
} @else {
|
|
2504
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
2505
|
+
}
|
|
2506
|
+
</td>
|
|
2507
|
+
</tr>
|
|
2508
|
+
</ng-template>
|
|
2509
|
+
</f-treetable>
|
|
2510
|
+
</div>
|
|
2511
|
+
</div>
|
|
2512
|
+
|
|
2513
|
+
@if (hasChanges()) {
|
|
2514
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
2515
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2516
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
2517
|
+
<span class="font-bold">{{
|
|
2518
|
+
'shared.pending.changes' | translate
|
|
2519
|
+
}}</span>
|
|
2520
|
+
</div>
|
|
2521
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2522
|
+
@if (pendingAdd().length > 0) {
|
|
2523
|
+
<div>
|
|
2524
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2525
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
2526
|
+
<strong class="text-sm"
|
|
2527
|
+
>{{ 'shared.to.whitelist' | translate }} ({{
|
|
2528
|
+
pendingAdd().length
|
|
2529
|
+
}})</strong
|
|
2530
|
+
>
|
|
2531
|
+
</div>
|
|
2532
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2533
|
+
@for (action of pendingAdd(); track action.id) {
|
|
2534
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2535
|
+
}
|
|
2536
|
+
</ul>
|
|
2537
|
+
</div>
|
|
2538
|
+
}
|
|
2539
|
+
@if (pendingRemove().length > 0) {
|
|
2540
|
+
<div>
|
|
2541
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2542
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
2543
|
+
<strong class="text-sm"
|
|
2544
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
2545
|
+
pendingRemove().length
|
|
2546
|
+
}})</strong
|
|
2547
|
+
>
|
|
2548
|
+
</div>
|
|
2549
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2550
|
+
@for (action of pendingRemove(); track action.id) {
|
|
2551
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2552
|
+
}
|
|
2553
|
+
</ul>
|
|
2554
|
+
</div>
|
|
2555
|
+
}
|
|
2556
|
+
</div>
|
|
2557
|
+
</div>
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
@if (!loading() && actions().length === 0) {
|
|
2562
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
2563
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
2564
|
+
<p class="text-muted-color m-0">
|
|
2565
|
+
{{ 'iam.permission.no.actions.for.company' | translate }}
|
|
2566
|
+
</p>
|
|
2567
|
+
</div>
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
</div>
|
|
2571
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
2572
|
+
}
|
|
2573
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: CompanyActionSelectorComponent, decorators: [{
|
|
2574
|
+
type: Component,
|
|
2575
|
+
args: [{ selector: 'flusys-company-action-selector', imports: [UiModule, FormsModule, HasPermissionDirective, TranslatePipe], template: `
|
|
2576
|
+
<div class="company-action-selector">
|
|
2577
|
+
<div class="mb-4">
|
|
2578
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
2579
|
+
<div>
|
|
2580
|
+
<label class="block font-semibold mb-2">{{
|
|
2581
|
+
'iam.permission.select.company' | translate
|
|
2582
|
+
}}</label>
|
|
2583
|
+
<f-select
|
|
2584
|
+
[ngModel]="selectedCompanyId()"
|
|
2585
|
+
(ngModelChange)="selectedCompanyId.set($event)"
|
|
2586
|
+
[options]="companies()"
|
|
2587
|
+
optionLabel="name"
|
|
2588
|
+
optionValue="id"
|
|
2589
|
+
[placeholder]="
|
|
2590
|
+
'iam.permission.select.company.placeholder' | translate
|
|
2591
|
+
"
|
|
2592
|
+
[showClear]="true"
|
|
2593
|
+
[filter]="true"
|
|
2594
|
+
class="w-full"
|
|
2595
|
+
/>
|
|
2596
|
+
</div>
|
|
2597
|
+
</div>
|
|
2598
|
+
</div>
|
|
2599
|
+
|
|
2600
|
+
@if (selectedCompanyId()) {
|
|
2601
|
+
@if (loading()) {
|
|
2602
|
+
<div
|
|
2603
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
2604
|
+
>
|
|
2605
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
2606
|
+
</div>
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
@if (!loading() && actions().length > 0) {
|
|
2610
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
2611
|
+
<div
|
|
2612
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
2613
|
+
>
|
|
2614
|
+
<div>
|
|
2615
|
+
<h5 class="m-0 mb-1">
|
|
2616
|
+
{{ 'iam.permission.action.whitelist' | translate }}
|
|
2617
|
+
</h5>
|
|
2618
|
+
<p class="text-sm text-muted-color m-0">
|
|
2619
|
+
{{
|
|
2620
|
+
'iam.permission.actions.available'
|
|
2621
|
+
| translate: { count: actions().length }
|
|
2622
|
+
}}
|
|
2623
|
+
</p>
|
|
2624
|
+
</div>
|
|
2625
|
+
<div class="flex flex-wrap gap-2">
|
|
2626
|
+
<button
|
|
2627
|
+
fButton
|
|
2628
|
+
[outlined]="true"
|
|
2629
|
+
size="small"
|
|
2630
|
+
severity="secondary"
|
|
2631
|
+
(click)="selectAll()"
|
|
2632
|
+
>
|
|
2633
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
2634
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
2635
|
+
</button>
|
|
2636
|
+
<button
|
|
2637
|
+
fButton
|
|
2638
|
+
[outlined]="true"
|
|
2639
|
+
size="small"
|
|
2640
|
+
severity="secondary"
|
|
2641
|
+
(click)="deselectAll()"
|
|
2642
|
+
>
|
|
2643
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
2644
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
2645
|
+
</button>
|
|
2646
|
+
<button
|
|
2647
|
+
fButton
|
|
2648
|
+
*hasPermission="COMPANY_ACTION_PERMISSIONS.ASSIGN"
|
|
2649
|
+
[disabled]="!canSave() || saving()"
|
|
2650
|
+
size="small"
|
|
2651
|
+
(click)="saveChanges()"
|
|
2652
|
+
>
|
|
2653
|
+
@if (saving()) {
|
|
2654
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
2655
|
+
} @else {
|
|
2656
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
2657
|
+
}
|
|
2658
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
2659
|
+
</button>
|
|
2660
|
+
</div>
|
|
2661
|
+
</div>
|
|
2662
|
+
|
|
2663
|
+
@if (invalidActionsCount() > 0) {
|
|
2664
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
2665
|
+
<div class="flex items-start gap-2">
|
|
2666
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
2667
|
+
<div class="flex-1">
|
|
2668
|
+
<span class="font-semibold"
|
|
2669
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
2670
|
+
>
|
|
2671
|
+
<p class="text-sm mt-1 mb-0">
|
|
2672
|
+
{{
|
|
2673
|
+
(invalidActionsCount() > 1
|
|
2674
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
2675
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
2676
|
+
) | translate: { count: invalidActionsCount() }
|
|
2677
|
+
}}
|
|
2678
|
+
</p>
|
|
2679
|
+
</div>
|
|
2680
|
+
</div>
|
|
2681
|
+
</div>
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
2685
|
+
<f-treetable
|
|
2686
|
+
[value]="treeNodes()"
|
|
2687
|
+
dataKey="id"
|
|
2688
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
2689
|
+
>
|
|
2690
|
+
<ng-template #header>
|
|
2691
|
+
<tr>
|
|
2692
|
+
<th class="w-12">
|
|
2693
|
+
<f-checkbox
|
|
2694
|
+
[ngModel]="allSelected()"
|
|
2695
|
+
[binary]="true"
|
|
2696
|
+
(ngModelChange)="toggleAll()"
|
|
2697
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
2698
|
+
tooltipPosition="top"
|
|
2699
|
+
/>
|
|
2700
|
+
</th>
|
|
2701
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
2702
|
+
<th class="hidden md:table-cell">
|
|
2703
|
+
{{ 'shared.code' | translate }}
|
|
2704
|
+
</th>
|
|
2705
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
2706
|
+
<th class="hidden lg:table-cell">
|
|
2707
|
+
{{ 'shared.description' | translate }}
|
|
2708
|
+
</th>
|
|
2709
|
+
</tr>
|
|
2710
|
+
</ng-template>
|
|
2711
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
2712
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
2713
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
2714
|
+
<td class="w-12">
|
|
2715
|
+
<f-checkbox
|
|
2716
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
2717
|
+
[binary]="true"
|
|
2718
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
2719
|
+
[fTooltip]="getTooltip(rowData)"
|
|
2720
|
+
tooltipPosition="top"
|
|
2721
|
+
/>
|
|
2722
|
+
</td>
|
|
2723
|
+
<td>
|
|
2724
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
2725
|
+
<span class="inline-flex items-center gap-2">
|
|
2726
|
+
{{ rowData.name }}
|
|
2727
|
+
@if (hasUnmet) {
|
|
2728
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
2729
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
2730
|
+
| translate
|
|
2731
|
+
"
|
|
2732
|
+
tooltipPosition="top"></ui-icon>
|
|
2733
|
+
}
|
|
2734
|
+
</span>
|
|
2735
|
+
</td>
|
|
2736
|
+
<td class="hidden md:table-cell">
|
|
2737
|
+
{{ rowData.code || '-' }}
|
|
2738
|
+
</td>
|
|
2739
|
+
<td>
|
|
2740
|
+
<f-tag
|
|
2741
|
+
[value]="rowData.actionType"
|
|
2742
|
+
[severity]="
|
|
2743
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
2744
|
+
"
|
|
2745
|
+
/>
|
|
2746
|
+
</td>
|
|
2747
|
+
<td class="hidden lg:table-cell">
|
|
2748
|
+
{{ rowData.description || '-' }}
|
|
2749
|
+
</td>
|
|
2750
|
+
</tr>
|
|
2751
|
+
</ng-template>
|
|
2752
|
+
<ng-template #emptymessage>
|
|
2753
|
+
<tr>
|
|
2754
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
2755
|
+
@if (loading()) {
|
|
2756
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
2757
|
+
{{ 'shared.loading.actions' | translate }}
|
|
2758
|
+
} @else {
|
|
2759
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
2760
|
+
}
|
|
2761
|
+
</td>
|
|
2762
|
+
</tr>
|
|
2763
|
+
</ng-template>
|
|
2764
|
+
</f-treetable>
|
|
2765
|
+
</div>
|
|
2766
|
+
</div>
|
|
2767
|
+
|
|
2768
|
+
@if (hasChanges()) {
|
|
2769
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
2770
|
+
<div class="flex items-center gap-2 mb-3">
|
|
2771
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
2772
|
+
<span class="font-bold">{{
|
|
2773
|
+
'shared.pending.changes' | translate
|
|
2774
|
+
}}</span>
|
|
2775
|
+
</div>
|
|
2776
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
2777
|
+
@if (pendingAdd().length > 0) {
|
|
2778
|
+
<div>
|
|
2779
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2780
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
2781
|
+
<strong class="text-sm"
|
|
2782
|
+
>{{ 'shared.to.whitelist' | translate }} ({{
|
|
2783
|
+
pendingAdd().length
|
|
2784
|
+
}})</strong
|
|
2785
|
+
>
|
|
2786
|
+
</div>
|
|
2787
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2788
|
+
@for (action of pendingAdd(); track action.id) {
|
|
2789
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2790
|
+
}
|
|
2791
|
+
</ul>
|
|
2792
|
+
</div>
|
|
2793
|
+
}
|
|
2794
|
+
@if (pendingRemove().length > 0) {
|
|
2795
|
+
<div>
|
|
2796
|
+
<div class="flex items-center gap-2 mb-2">
|
|
2797
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
2798
|
+
<strong class="text-sm"
|
|
2799
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
2800
|
+
pendingRemove().length
|
|
2801
|
+
}})</strong
|
|
2802
|
+
>
|
|
2803
|
+
</div>
|
|
2804
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
2805
|
+
@for (action of pendingRemove(); track action.id) {
|
|
2806
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
2807
|
+
}
|
|
2808
|
+
</ul>
|
|
2809
|
+
</div>
|
|
2810
|
+
}
|
|
2811
|
+
</div>
|
|
2812
|
+
</div>
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
@if (!loading() && actions().length === 0) {
|
|
2817
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
2818
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
2819
|
+
<p class="text-muted-color m-0">
|
|
2820
|
+
{{ 'iam.permission.no.actions.for.company' | translate }}
|
|
2821
|
+
</p>
|
|
2822
|
+
</div>
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
</div>
|
|
2826
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
2827
|
+
}], ctorParameters: () => [] });
|
|
2828
|
+
|
|
2829
|
+
/**
|
|
2830
|
+
* User-Role Selector Component
|
|
2831
|
+
*
|
|
2832
|
+
* Manages user role assignments with branch-level granularity.
|
|
2833
|
+
*
|
|
2834
|
+
* **Architecture:**
|
|
2835
|
+
* - Presentation: DataTable for role list
|
|
2836
|
+
* - Business Logic: Role assignment without dependency validation
|
|
2837
|
+
* - State: Signals with computed dependencies
|
|
2838
|
+
*
|
|
2839
|
+
* **Core Features:**
|
|
2840
|
+
* - User dropdown with async loading
|
|
2841
|
+
* - Branch selector (if company feature enabled)
|
|
2842
|
+
* - Role assignment management (add/remove)
|
|
2843
|
+
* - Real-time change tracking
|
|
2844
|
+
*
|
|
2845
|
+
* **Granularity:**
|
|
2846
|
+
* - Company-wide: branchId = undefined
|
|
2847
|
+
* - Branch-specific: branchId = selected value
|
|
2848
|
+
*
|
|
2849
|
+
* **Performance:**
|
|
2850
|
+
* - Computed signals for reactive updates
|
|
2851
|
+
* - Branch filtering on company context change
|
|
2852
|
+
*
|
|
2853
|
+
* @example
|
|
2854
|
+
* ```html
|
|
2855
|
+
* <flusys-user-role-selector />
|
|
2856
|
+
* ```
|
|
2857
|
+
*/
|
|
2858
|
+
class UserRoleSelectorComponent {
|
|
2859
|
+
// Permission constants for template
|
|
2860
|
+
USER_ROLE_PERMISSIONS = USER_ROLE_PERMISSIONS;
|
|
2861
|
+
// Dependencies
|
|
2862
|
+
appConfig = inject(APP_CONFIG);
|
|
2863
|
+
companyContext = inject(LAYOUT_AUTH_STATE);
|
|
2864
|
+
userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
|
|
2865
|
+
roleApi = inject(RoleApiService);
|
|
2866
|
+
permissionApi = inject(PermissionApiService);
|
|
2867
|
+
messageService = inject(MessageService);
|
|
2868
|
+
translateAdapter = inject(TranslateService, {
|
|
2869
|
+
optional: true,
|
|
2870
|
+
});
|
|
2871
|
+
translate(key, vars) {
|
|
2872
|
+
return this.translateAdapter?.translate(key, vars) ?? key;
|
|
2873
|
+
}
|
|
2874
|
+
// State - User/Branch Selection
|
|
2875
|
+
selectedUserId = signal(null, /* @ts-ignore */
|
|
2876
|
+
...(ngDevMode ? [{ debugName: "selectedUserId" }] : /* istanbul ignore next */ []));
|
|
2877
|
+
selectedBranchId = signal(undefined, /* @ts-ignore */
|
|
2878
|
+
...(ngDevMode ? [{ debugName: "selectedBranchId" }] : /* istanbul ignore next */ []));
|
|
2879
|
+
branches = signal([], /* @ts-ignore */
|
|
2880
|
+
...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
|
|
2881
|
+
// State - Loading/Saving
|
|
2882
|
+
loading = signal(false, /* @ts-ignore */
|
|
2883
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
2884
|
+
saving = signal(false, /* @ts-ignore */
|
|
2885
|
+
...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
|
|
2886
|
+
// State - Roles (Business Logic)
|
|
2887
|
+
_roles = signal([], /* @ts-ignore */
|
|
2888
|
+
...(ngDevMode ? [{ debugName: "_roles" }] : /* istanbul ignore next */ []));
|
|
2889
|
+
roles = this._roles.asReadonly();
|
|
2890
|
+
// State - Selection
|
|
2891
|
+
_selectionMap = signal({}, /* @ts-ignore */
|
|
2892
|
+
...(ngDevMode ? [{ debugName: "_selectionMap" }] : /* istanbul ignore next */ []));
|
|
2893
|
+
selectionMap = this._selectionMap.asReadonly();
|
|
2894
|
+
_initialSelection = signal({}, /* @ts-ignore */
|
|
2895
|
+
...(ngDevMode ? [{ debugName: "_initialSelection" }] : /* istanbul ignore next */ []));
|
|
2896
|
+
initialSelection = this._initialSelection.asReadonly();
|
|
2897
|
+
// Computed - UI Flags
|
|
2898
|
+
showBranchSelector = computed(() => isCompanyFeatureEnabled(this.appConfig) === true, /* @ts-ignore */
|
|
2899
|
+
...(ngDevMode ? [{ debugName: "showBranchSelector" }] : /* istanbul ignore next */ []));
|
|
2900
|
+
filteredBranches = computed(() => {
|
|
2901
|
+
const currentCompanyId = this.companyContext.currentCompanyInfo()?.id || undefined;
|
|
2902
|
+
if (!currentCompanyId)
|
|
2903
|
+
return this.branches();
|
|
2904
|
+
return this.branches().filter((b) => b.companyId === currentCompanyId);
|
|
2905
|
+
}, /* @ts-ignore */
|
|
2906
|
+
...(ngDevMode ? [{ debugName: "filteredBranches" }] : /* istanbul ignore next */ []));
|
|
2907
|
+
// Computed - Selection State
|
|
2908
|
+
allSelected = computed(() => {
|
|
2909
|
+
const selMap = this.selectionMap();
|
|
2910
|
+
const total = this.roles().length;
|
|
2911
|
+
if (total === 0)
|
|
2912
|
+
return false;
|
|
2913
|
+
const selected = Object.values(selMap).filter((v) => v).length;
|
|
2914
|
+
return selected === total;
|
|
2915
|
+
}, /* @ts-ignore */
|
|
2916
|
+
...(ngDevMode ? [{ debugName: "allSelected" }] : /* istanbul ignore next */ []));
|
|
2917
|
+
// Computed - Change Detection
|
|
2918
|
+
hasChanges = computed(() => {
|
|
2919
|
+
const current = this.selectionMap();
|
|
2920
|
+
const initial = this.initialSelection();
|
|
2921
|
+
const currentKeys = Object.keys(current);
|
|
2922
|
+
const initialKeys = Object.keys(initial);
|
|
2923
|
+
if (currentKeys.length !== initialKeys.length)
|
|
2924
|
+
return true;
|
|
2925
|
+
return currentKeys.some((key) => current[key] !== initial[key]);
|
|
2926
|
+
}, /* @ts-ignore */
|
|
2927
|
+
...(ngDevMode ? [{ debugName: "hasChanges" }] : /* istanbul ignore next */ []));
|
|
2928
|
+
// Computed - Pending Changes
|
|
2929
|
+
pendingAdd = computed(() => {
|
|
2930
|
+
const current = this.selectionMap();
|
|
2931
|
+
const initial = this.initialSelection();
|
|
2932
|
+
const allRoles = this.roles();
|
|
2933
|
+
return allRoles.filter((role) => current[role.id] && !initial[role.id]);
|
|
2934
|
+
}, /* @ts-ignore */
|
|
2935
|
+
...(ngDevMode ? [{ debugName: "pendingAdd" }] : /* istanbul ignore next */ []));
|
|
2936
|
+
pendingRemove = computed(() => {
|
|
2937
|
+
const current = this.selectionMap();
|
|
2938
|
+
const initial = this.initialSelection();
|
|
2939
|
+
const allRoles = this.roles();
|
|
2940
|
+
return allRoles.filter((role) => !current[role.id] && initial[role.id]);
|
|
2941
|
+
}, /* @ts-ignore */
|
|
2942
|
+
...(ngDevMode ? [{ debugName: "pendingRemove" }] : /* istanbul ignore next */ []));
|
|
2943
|
+
// Computed - Save Button State
|
|
2944
|
+
canSave = computed(() => {
|
|
2945
|
+
return this.hasChanges() && !this.saving();
|
|
2946
|
+
}, /* @ts-ignore */
|
|
2947
|
+
...(ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []));
|
|
2948
|
+
constructor() {
|
|
2949
|
+
// Effect: Load user branches and data when user changes
|
|
2950
|
+
effect(() => {
|
|
2951
|
+
const userId = this.selectedUserId();
|
|
2952
|
+
if (userId) {
|
|
2953
|
+
if (isCompanyFeatureEnabled(this.appConfig)) {
|
|
2954
|
+
this.loadUserBranches(userId);
|
|
2955
|
+
}
|
|
2956
|
+
this.loadData();
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
this.branches.set([]);
|
|
2960
|
+
this.selectedBranchId.set(undefined);
|
|
2961
|
+
this.resetState();
|
|
2962
|
+
}
|
|
2963
|
+
});
|
|
2964
|
+
// Effect: Reload data when branch changes
|
|
2965
|
+
effect(() => {
|
|
2966
|
+
const userId = this.selectedUserId();
|
|
2967
|
+
const branchId = this.selectedBranchId();
|
|
2968
|
+
if (isCompanyFeatureEnabled(this.appConfig) &&
|
|
2969
|
+
userId &&
|
|
2970
|
+
branchId !== undefined) {
|
|
2971
|
+
this.loadData();
|
|
2972
|
+
}
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Load user's permitted branches
|
|
2977
|
+
*/
|
|
2978
|
+
async loadUserBranches(userId) {
|
|
2979
|
+
try {
|
|
2980
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id || undefined;
|
|
2981
|
+
if (!companyId) {
|
|
2982
|
+
this.branches.set([]);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
|
|
2986
|
+
const userBranches = response?.data?.map((ub) => ({
|
|
2987
|
+
id: ub.branchId,
|
|
2988
|
+
name: ub.branchName,
|
|
2989
|
+
companyId: ub.companyId,
|
|
2990
|
+
})) ?? [];
|
|
2991
|
+
this.branches.set(userBranches);
|
|
2992
|
+
}
|
|
2993
|
+
catch {
|
|
2994
|
+
// Error toast handled by global interceptor
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Load roles and assignments for selected user
|
|
2999
|
+
*/
|
|
3000
|
+
async loadData() {
|
|
3001
|
+
const userId = this.selectedUserId();
|
|
3002
|
+
if (!userId)
|
|
3003
|
+
return;
|
|
3004
|
+
const branchId = this.selectedBranchId();
|
|
3005
|
+
this.loading.set(true);
|
|
3006
|
+
try {
|
|
3007
|
+
// Load all roles
|
|
3008
|
+
const rolesResponse = await this.roleApi.getAll({
|
|
3009
|
+
pagination: { currentPage: 0, pageSize: MAX_DROPDOWN_ITEMS },
|
|
3010
|
+
});
|
|
3011
|
+
const rolesList = rolesResponse?.data ?? [];
|
|
3012
|
+
this._roles.set(rolesList);
|
|
3013
|
+
// Load existing user-role assignments
|
|
3014
|
+
const query = isCompanyFeatureEnabled(this.appConfig) && branchId
|
|
3015
|
+
? { branchId }
|
|
3016
|
+
: undefined;
|
|
3017
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getUserRoles(userId, query));
|
|
3018
|
+
const assignedIds = new Set((assignedResponse?.data || []).map((r) => r.roleId));
|
|
3019
|
+
// Build selection map
|
|
3020
|
+
const selMap = {};
|
|
3021
|
+
rolesList.forEach((role) => {
|
|
3022
|
+
selMap[role.id] = assignedIds.has(role.id);
|
|
3023
|
+
});
|
|
3024
|
+
this._selectionMap.set(selMap);
|
|
3025
|
+
this._initialSelection.set({ ...selMap });
|
|
3026
|
+
}
|
|
3027
|
+
catch {
|
|
3028
|
+
// Error toast handled by global interceptor
|
|
3029
|
+
}
|
|
3030
|
+
finally {
|
|
3031
|
+
this.loading.set(false);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
/**
|
|
3035
|
+
* Get tooltip for role checkbox
|
|
3036
|
+
*/
|
|
3037
|
+
getTooltip(role) {
|
|
3038
|
+
const selMap = this.selectionMap();
|
|
3039
|
+
const isCurrentlySelected = selMap[role.id];
|
|
3040
|
+
if (isCurrentlySelected) {
|
|
3041
|
+
return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.user')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.role.from.user')}`;
|
|
3042
|
+
}
|
|
3043
|
+
return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.role.to.user')}`;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Handle role checkbox toggle
|
|
3047
|
+
*/
|
|
3048
|
+
onRoleToggle(role, newValue) {
|
|
3049
|
+
const selMap = { ...this.selectionMap() };
|
|
3050
|
+
selMap[role.id] = newValue;
|
|
3051
|
+
this._selectionMap.set(selMap);
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Toggle all roles
|
|
3055
|
+
*/
|
|
3056
|
+
toggleAll() {
|
|
3057
|
+
this.setAllSelections(!this.allSelected());
|
|
3058
|
+
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Select all roles
|
|
3061
|
+
*/
|
|
3062
|
+
selectAll() {
|
|
3063
|
+
this.setAllSelections(true);
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Deselect all roles
|
|
3067
|
+
*/
|
|
3068
|
+
deselectAll() {
|
|
3069
|
+
this.setAllSelections(false);
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Set all role selections to a given value
|
|
3073
|
+
*/
|
|
3074
|
+
setAllSelections(value) {
|
|
3075
|
+
const selMap = {};
|
|
3076
|
+
this.roles().forEach((role) => {
|
|
3077
|
+
selMap[role.id] = value;
|
|
3078
|
+
});
|
|
3079
|
+
this._selectionMap.set(selMap);
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Save changes to backend
|
|
3083
|
+
*/
|
|
3084
|
+
async saveChanges() {
|
|
3085
|
+
const userId = this.selectedUserId();
|
|
3086
|
+
if (!userId)
|
|
3087
|
+
return;
|
|
3088
|
+
const branchId = this.selectedBranchId();
|
|
3089
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id || undefined;
|
|
3090
|
+
// Validate company context if feature enabled
|
|
3091
|
+
if (isCompanyFeatureEnabled(this.appConfig) && !companyId) {
|
|
3092
|
+
this.messageService.add({
|
|
3093
|
+
severity: 'warn',
|
|
3094
|
+
summary: this.translate('shared.warning'),
|
|
3095
|
+
detail: this.translate('iam.permission.company.required'),
|
|
3096
|
+
});
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
// Build payload
|
|
3100
|
+
const items = [];
|
|
3101
|
+
this.pendingAdd().forEach((role) => {
|
|
3102
|
+
items.push({ id: role.id, action: 'add' });
|
|
3103
|
+
});
|
|
3104
|
+
this.pendingRemove().forEach((role) => {
|
|
3105
|
+
items.push({ id: role.id, action: 'remove' });
|
|
3106
|
+
});
|
|
3107
|
+
if (items.length === 0)
|
|
3108
|
+
return;
|
|
3109
|
+
this.saving.set(true);
|
|
3110
|
+
try {
|
|
3111
|
+
const payload = { userId, items };
|
|
3112
|
+
if (isCompanyFeatureEnabled(this.appConfig)) {
|
|
3113
|
+
if (companyId) {
|
|
3114
|
+
payload.companyId = companyId;
|
|
3115
|
+
}
|
|
3116
|
+
if (branchId) {
|
|
3117
|
+
payload.branchId = branchId;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
const response = await firstValueFrom(this.permissionApi.assignUserRoles(payload));
|
|
3121
|
+
this.messageService.add({
|
|
3122
|
+
severity: 'success',
|
|
3123
|
+
summary: this.translate('shared.success'),
|
|
3124
|
+
detail: response?.messageKey
|
|
3125
|
+
? this.translate(response.messageKey, response.messageVariables)
|
|
3126
|
+
: response?.message,
|
|
3127
|
+
});
|
|
3128
|
+
// Update baseline
|
|
3129
|
+
this._initialSelection.set({ ...this.selectionMap() });
|
|
3130
|
+
}
|
|
3131
|
+
catch {
|
|
3132
|
+
// Error toast handled by global interceptor
|
|
3133
|
+
}
|
|
3134
|
+
finally {
|
|
3135
|
+
this.saving.set(false);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Reset component state
|
|
3140
|
+
*/
|
|
3141
|
+
resetState() {
|
|
3142
|
+
this._roles.set([]);
|
|
3143
|
+
this._selectionMap.set({});
|
|
3144
|
+
this._initialSelection.set({});
|
|
3145
|
+
}
|
|
3146
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserRoleSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3147
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: UserRoleSelectorComponent, isStandalone: true, selector: "flusys-user-role-selector", ngImport: i0, template: `
|
|
3148
|
+
<div class="user-role-selector">
|
|
3149
|
+
<!-- User and Branch Selectors -->
|
|
3150
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3151
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3152
|
+
<div>
|
|
3153
|
+
<label class="block font-semibold mb-2">
|
|
3154
|
+
<ui-icon name="user" class="mr-2 text-primary"></ui-icon>
|
|
3155
|
+
{{ 'iam.permission.select.user' | translate }}
|
|
3156
|
+
</label>
|
|
3157
|
+
<lib-user-select
|
|
3158
|
+
[value]="selectedUserId()"
|
|
3159
|
+
(valueChange)="selectedUserId.set($event)"
|
|
3160
|
+
[isEditMode]="true"
|
|
3161
|
+
[placeHolder]="
|
|
3162
|
+
'iam.permission.select.user.placeholder' | translate
|
|
3163
|
+
"
|
|
3164
|
+
/>
|
|
3165
|
+
</div>
|
|
3166
|
+
|
|
3167
|
+
@if (showBranchSelector()) {
|
|
3168
|
+
<div>
|
|
3169
|
+
<label class="block font-semibold mb-2">
|
|
3170
|
+
<ui-icon name="building" class="mr-2 text-primary"></ui-icon>
|
|
3171
|
+
{{ 'iam.permission.select.branch' | translate }}
|
|
3172
|
+
</label>
|
|
3173
|
+
<f-select
|
|
3174
|
+
[ngModel]="selectedBranchId()"
|
|
3175
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
3176
|
+
[options]="filteredBranches()"
|
|
3177
|
+
optionLabel="name"
|
|
3178
|
+
optionValue="id"
|
|
3179
|
+
[placeholder]="
|
|
3180
|
+
'iam.permission.select.branch.placeholder' | translate
|
|
3181
|
+
"
|
|
3182
|
+
[showClear]="true"
|
|
3183
|
+
[filter]="true"
|
|
3184
|
+
styleClass="w-full"
|
|
3185
|
+
>
|
|
3186
|
+
<ng-template #selectedItem let-branch>
|
|
3187
|
+
@if (branch) {
|
|
3188
|
+
<div class="flex items-center gap-2">
|
|
3189
|
+
<ui-icon name="building" class="text-primary"></ui-icon>
|
|
3190
|
+
<span class="font-semibold">{{ branch.name }}</span>
|
|
3191
|
+
</div>
|
|
3192
|
+
}
|
|
3193
|
+
</ng-template>
|
|
3194
|
+
<ng-template #item let-branch>
|
|
3195
|
+
<div class="flex items-center gap-2">
|
|
3196
|
+
<ui-icon name="building" class="text-muted-color"></ui-icon>
|
|
3197
|
+
<span>{{ branch.name }}</span>
|
|
3198
|
+
</div>
|
|
3199
|
+
</ng-template>
|
|
3200
|
+
</f-select>
|
|
3201
|
+
<small class="text-muted-color block mt-1">
|
|
3202
|
+
{{
|
|
3203
|
+
(filteredBranches().length !== 1
|
|
3204
|
+
? 'iam.branch.permitted.count.plural'
|
|
3205
|
+
: 'iam.branch.permitted.count'
|
|
3206
|
+
) | translate: { count: filteredBranches().length }
|
|
3207
|
+
}}
|
|
3208
|
+
</small>
|
|
3209
|
+
</div>
|
|
3210
|
+
}
|
|
3211
|
+
</div>
|
|
3212
|
+
</div>
|
|
3213
|
+
|
|
3214
|
+
@if (selectedUserId()) {
|
|
3215
|
+
<!-- Loading State -->
|
|
3216
|
+
@if (loading()) {
|
|
3217
|
+
<div
|
|
3218
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
3219
|
+
>
|
|
3220
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
3221
|
+
</div>
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
<!-- Role List -->
|
|
3225
|
+
@if (!loading() && roles().length > 0) {
|
|
3226
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
3227
|
+
<div
|
|
3228
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
3229
|
+
>
|
|
3230
|
+
<div>
|
|
3231
|
+
<h5 class="m-0 mb-1">
|
|
3232
|
+
{{ 'iam.permission.role.assignments' | translate }}
|
|
3233
|
+
</h5>
|
|
3234
|
+
<p class="text-sm text-muted-color m-0">
|
|
3235
|
+
{{
|
|
3236
|
+
'iam.permission.roles.available'
|
|
3237
|
+
| translate: { count: roles().length }
|
|
3238
|
+
}}
|
|
3239
|
+
</p>
|
|
3240
|
+
</div>
|
|
3241
|
+
<div class="flex flex-wrap gap-2">
|
|
3242
|
+
<button
|
|
3243
|
+
fButton
|
|
3244
|
+
[outlined]="true"
|
|
3245
|
+
size="small"
|
|
3246
|
+
severity="secondary"
|
|
3247
|
+
(click)="selectAll()"
|
|
3248
|
+
>
|
|
3249
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
3250
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
3251
|
+
</button>
|
|
3252
|
+
<button
|
|
3253
|
+
fButton
|
|
3254
|
+
[outlined]="true"
|
|
3255
|
+
size="small"
|
|
3256
|
+
severity="secondary"
|
|
3257
|
+
(click)="deselectAll()"
|
|
3258
|
+
>
|
|
3259
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
3260
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
3261
|
+
</button>
|
|
3262
|
+
<button
|
|
3263
|
+
fButton
|
|
3264
|
+
*hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
|
|
3265
|
+
[disabled]="!canSave() || saving()"
|
|
3266
|
+
size="small"
|
|
3267
|
+
(click)="saveChanges()"
|
|
3268
|
+
>
|
|
3269
|
+
@if (saving()) {
|
|
3270
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
3271
|
+
} @else {
|
|
3272
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
3273
|
+
}
|
|
3274
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
3275
|
+
</button>
|
|
3276
|
+
</div>
|
|
3277
|
+
</div>
|
|
3278
|
+
|
|
3279
|
+
<!-- Role Table -->
|
|
3280
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3281
|
+
<f-table
|
|
3282
|
+
[value]="roles()"
|
|
3283
|
+
[rows]="10"
|
|
3284
|
+
[paginator]="roles().length > 10"
|
|
3285
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
3286
|
+
[globalFilterFields]="['name', 'code', 'description']"
|
|
3287
|
+
[showCurrentPageReport]="true"
|
|
3288
|
+
[currentPageReportTemplate]="
|
|
3289
|
+
'iam.pagination.roles.template' | translate
|
|
3290
|
+
"
|
|
3291
|
+
[tableStyle]="{ 'min-width': '35rem' }"
|
|
3292
|
+
>
|
|
3293
|
+
<ng-template #header>
|
|
3294
|
+
<tr>
|
|
3295
|
+
<th style="width: 3rem">
|
|
3296
|
+
<f-checkbox
|
|
3297
|
+
[ngModel]="allSelected()"
|
|
3298
|
+
[binary]="true"
|
|
3299
|
+
(ngModelChange)="toggleAll()"
|
|
3300
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
3301
|
+
tooltipPosition="top"
|
|
3302
|
+
/>
|
|
3303
|
+
</th>
|
|
3304
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
3305
|
+
<th class="hidden sm:table-cell">
|
|
3306
|
+
{{ 'shared.code' | translate }}
|
|
3307
|
+
</th>
|
|
3308
|
+
<th class="hidden md:table-cell">
|
|
3309
|
+
{{ 'shared.description' | translate }}
|
|
3310
|
+
</th>
|
|
3311
|
+
</tr>
|
|
3312
|
+
</ng-template>
|
|
3313
|
+
<ng-template #body let-role>
|
|
3314
|
+
<tr>
|
|
3315
|
+
<td>
|
|
3316
|
+
<f-checkbox
|
|
3317
|
+
[ngModel]="selectionMap()[role.id]"
|
|
3318
|
+
[binary]="true"
|
|
3319
|
+
(ngModelChange)="onRoleToggle(role, $event)"
|
|
3320
|
+
[fTooltip]="getTooltip(role)"
|
|
3321
|
+
tooltipPosition="top"
|
|
3322
|
+
/>
|
|
3323
|
+
</td>
|
|
3324
|
+
<td>{{ role.name }}</td>
|
|
3325
|
+
<td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
|
|
3326
|
+
<td class="hidden md:table-cell">
|
|
3327
|
+
{{ role.description || '-' }}
|
|
3328
|
+
</td>
|
|
3329
|
+
</tr>
|
|
3330
|
+
</ng-template>
|
|
3331
|
+
<ng-template #emptymessage>
|
|
3332
|
+
<tr>
|
|
3333
|
+
<td colspan="4" class="text-center p-4 text-muted-color">
|
|
3334
|
+
@if (loading()) {
|
|
3335
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
3336
|
+
{{ 'shared.loading.roles' | translate }}
|
|
3337
|
+
} @else {
|
|
3338
|
+
{{ 'iam.permission.no.roles.available' | translate }}
|
|
3339
|
+
}
|
|
3340
|
+
</td>
|
|
3341
|
+
</tr>
|
|
3342
|
+
</ng-template>
|
|
3343
|
+
</f-table>
|
|
3344
|
+
</div>
|
|
3345
|
+
</div>
|
|
3346
|
+
|
|
3347
|
+
<!-- Change Summary -->
|
|
3348
|
+
@if (hasChanges()) {
|
|
3349
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
3350
|
+
<div class="flex items-center gap-2 mb-3">
|
|
3351
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
3352
|
+
<span class="font-bold">{{
|
|
3353
|
+
'shared.pending.changes' | translate
|
|
3354
|
+
}}</span>
|
|
3355
|
+
</div>
|
|
3356
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
3357
|
+
@if (pendingAdd().length > 0) {
|
|
3358
|
+
<div>
|
|
3359
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3360
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
3361
|
+
<strong class="text-sm"
|
|
3362
|
+
>{{ 'shared.to.assign' | translate }} ({{
|
|
3363
|
+
pendingAdd().length
|
|
3364
|
+
}})</strong
|
|
3365
|
+
>
|
|
3366
|
+
</div>
|
|
3367
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
3368
|
+
@for (role of pendingAdd(); track role.id) {
|
|
3369
|
+
<li class="text-sm mb-1">{{ role.name }}</li>
|
|
3370
|
+
}
|
|
3371
|
+
</ul>
|
|
3372
|
+
</div>
|
|
3373
|
+
}
|
|
3374
|
+
@if (pendingRemove().length > 0) {
|
|
3375
|
+
<div>
|
|
3376
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3377
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
3378
|
+
<strong class="text-sm"
|
|
3379
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
3380
|
+
pendingRemove().length
|
|
3381
|
+
}})</strong
|
|
3382
|
+
>
|
|
3383
|
+
</div>
|
|
3384
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
3385
|
+
@for (role of pendingRemove(); track role.id) {
|
|
3386
|
+
<li class="text-sm mb-1">{{ role.name }}</li>
|
|
3387
|
+
}
|
|
3388
|
+
</ul>
|
|
3389
|
+
</div>
|
|
3390
|
+
}
|
|
3391
|
+
</div>
|
|
3392
|
+
</div>
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
@if (!loading() && roles().length === 0) {
|
|
3397
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
3398
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
3399
|
+
<p class="text-muted-color m-0">
|
|
3400
|
+
{{ 'iam.permission.no.roles.for.user' | translate }}
|
|
3401
|
+
</p>
|
|
3402
|
+
</div>
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
</div>
|
|
3406
|
+
`, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "component", type: i2.TableComponent, selector: "f-table", inputs: ["value", "lazy", "loading", "rows", "totalRecords", "rowHover", "dataKey", "scrollable", "scrollHeight", "paginator", "first", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "tableStyle", "styleClass", "globalFilterFields", "filterDelay", "responsiveLayout", "selectionMode", "sortMode", "rowExpandMode", "resizableColumns", "rowStyleClass", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "expandedRowKeys", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "expandedRowKeysChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onRowSelect", "onRowUnselect", "onSelectAllChange", "onRowExpand", "onRowCollapse", "onColResize", "onRowContextMenu"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
3407
|
+
}
|
|
3408
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserRoleSelectorComponent, decorators: [{
|
|
3409
|
+
type: Component,
|
|
3410
|
+
args: [{ selector: 'flusys-user-role-selector', imports: [
|
|
3411
|
+
CommonModule,
|
|
3412
|
+
FormsModule,
|
|
3413
|
+
UiModule,
|
|
3414
|
+
HasPermissionDirective,
|
|
3415
|
+
UserSelectComponent,
|
|
3416
|
+
TranslatePipe,
|
|
3417
|
+
], template: `
|
|
3418
|
+
<div class="user-role-selector">
|
|
3419
|
+
<!-- User and Branch Selectors -->
|
|
3420
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3421
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3422
|
+
<div>
|
|
3423
|
+
<label class="block font-semibold mb-2">
|
|
3424
|
+
<ui-icon name="user" class="mr-2 text-primary"></ui-icon>
|
|
3425
|
+
{{ 'iam.permission.select.user' | translate }}
|
|
3426
|
+
</label>
|
|
3427
|
+
<lib-user-select
|
|
3428
|
+
[value]="selectedUserId()"
|
|
3429
|
+
(valueChange)="selectedUserId.set($event)"
|
|
3430
|
+
[isEditMode]="true"
|
|
3431
|
+
[placeHolder]="
|
|
3432
|
+
'iam.permission.select.user.placeholder' | translate
|
|
3433
|
+
"
|
|
3434
|
+
/>
|
|
3435
|
+
</div>
|
|
3436
|
+
|
|
3437
|
+
@if (showBranchSelector()) {
|
|
3438
|
+
<div>
|
|
3439
|
+
<label class="block font-semibold mb-2">
|
|
3440
|
+
<ui-icon name="building" class="mr-2 text-primary"></ui-icon>
|
|
3441
|
+
{{ 'iam.permission.select.branch' | translate }}
|
|
3442
|
+
</label>
|
|
3443
|
+
<f-select
|
|
3444
|
+
[ngModel]="selectedBranchId()"
|
|
3445
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
3446
|
+
[options]="filteredBranches()"
|
|
3447
|
+
optionLabel="name"
|
|
3448
|
+
optionValue="id"
|
|
3449
|
+
[placeholder]="
|
|
3450
|
+
'iam.permission.select.branch.placeholder' | translate
|
|
3451
|
+
"
|
|
3452
|
+
[showClear]="true"
|
|
3453
|
+
[filter]="true"
|
|
3454
|
+
styleClass="w-full"
|
|
3455
|
+
>
|
|
3456
|
+
<ng-template #selectedItem let-branch>
|
|
3457
|
+
@if (branch) {
|
|
3458
|
+
<div class="flex items-center gap-2">
|
|
3459
|
+
<ui-icon name="building" class="text-primary"></ui-icon>
|
|
3460
|
+
<span class="font-semibold">{{ branch.name }}</span>
|
|
3461
|
+
</div>
|
|
3462
|
+
}
|
|
3463
|
+
</ng-template>
|
|
3464
|
+
<ng-template #item let-branch>
|
|
3465
|
+
<div class="flex items-center gap-2">
|
|
3466
|
+
<ui-icon name="building" class="text-muted-color"></ui-icon>
|
|
3467
|
+
<span>{{ branch.name }}</span>
|
|
3468
|
+
</div>
|
|
3469
|
+
</ng-template>
|
|
3470
|
+
</f-select>
|
|
3471
|
+
<small class="text-muted-color block mt-1">
|
|
3472
|
+
{{
|
|
3473
|
+
(filteredBranches().length !== 1
|
|
3474
|
+
? 'iam.branch.permitted.count.plural'
|
|
3475
|
+
: 'iam.branch.permitted.count'
|
|
3476
|
+
) | translate: { count: filteredBranches().length }
|
|
3477
|
+
}}
|
|
3478
|
+
</small>
|
|
3479
|
+
</div>
|
|
3480
|
+
}
|
|
3481
|
+
</div>
|
|
3482
|
+
</div>
|
|
3483
|
+
|
|
3484
|
+
@if (selectedUserId()) {
|
|
3485
|
+
<!-- Loading State -->
|
|
3486
|
+
@if (loading()) {
|
|
3487
|
+
<div
|
|
3488
|
+
class="surface-card p-5 rounded-border shadow-sm flex justify-center"
|
|
3489
|
+
>
|
|
3490
|
+
<ui-icon name="loader-circle" spin class="text-primary text-5xl"></ui-icon>
|
|
3491
|
+
</div>
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
<!-- Role List -->
|
|
3495
|
+
@if (!loading() && roles().length > 0) {
|
|
3496
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
3497
|
+
<div
|
|
3498
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
3499
|
+
>
|
|
3500
|
+
<div>
|
|
3501
|
+
<h5 class="m-0 mb-1">
|
|
3502
|
+
{{ 'iam.permission.role.assignments' | translate }}
|
|
3503
|
+
</h5>
|
|
3504
|
+
<p class="text-sm text-muted-color m-0">
|
|
3505
|
+
{{
|
|
3506
|
+
'iam.permission.roles.available'
|
|
3507
|
+
| translate: { count: roles().length }
|
|
3508
|
+
}}
|
|
3509
|
+
</p>
|
|
3510
|
+
</div>
|
|
3511
|
+
<div class="flex flex-wrap gap-2">
|
|
3512
|
+
<button
|
|
3513
|
+
fButton
|
|
3514
|
+
[outlined]="true"
|
|
3515
|
+
size="small"
|
|
3516
|
+
severity="secondary"
|
|
3517
|
+
(click)="selectAll()"
|
|
3518
|
+
>
|
|
3519
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
3520
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
3521
|
+
</button>
|
|
3522
|
+
<button
|
|
3523
|
+
fButton
|
|
3524
|
+
[outlined]="true"
|
|
3525
|
+
size="small"
|
|
3526
|
+
severity="secondary"
|
|
3527
|
+
(click)="deselectAll()"
|
|
3528
|
+
>
|
|
3529
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
3530
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
3531
|
+
</button>
|
|
3532
|
+
<button
|
|
3533
|
+
fButton
|
|
3534
|
+
*hasPermission="USER_ROLE_PERMISSIONS.ASSIGN"
|
|
3535
|
+
[disabled]="!canSave() || saving()"
|
|
3536
|
+
size="small"
|
|
3537
|
+
(click)="saveChanges()"
|
|
3538
|
+
>
|
|
3539
|
+
@if (saving()) {
|
|
3540
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
3541
|
+
} @else {
|
|
3542
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
3543
|
+
}
|
|
3544
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
3545
|
+
</button>
|
|
3546
|
+
</div>
|
|
3547
|
+
</div>
|
|
3548
|
+
|
|
3549
|
+
<!-- Role Table -->
|
|
3550
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3551
|
+
<f-table
|
|
3552
|
+
[value]="roles()"
|
|
3553
|
+
[rows]="10"
|
|
3554
|
+
[paginator]="roles().length > 10"
|
|
3555
|
+
[rowsPerPageOptions]="[10, 20, 50]"
|
|
3556
|
+
[globalFilterFields]="['name', 'code', 'description']"
|
|
3557
|
+
[showCurrentPageReport]="true"
|
|
3558
|
+
[currentPageReportTemplate]="
|
|
3559
|
+
'iam.pagination.roles.template' | translate
|
|
3560
|
+
"
|
|
3561
|
+
[tableStyle]="{ 'min-width': '35rem' }"
|
|
3562
|
+
>
|
|
3563
|
+
<ng-template #header>
|
|
3564
|
+
<tr>
|
|
3565
|
+
<th style="width: 3rem">
|
|
3566
|
+
<f-checkbox
|
|
3567
|
+
[ngModel]="allSelected()"
|
|
3568
|
+
[binary]="true"
|
|
3569
|
+
(ngModelChange)="toggleAll()"
|
|
3570
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
3571
|
+
tooltipPosition="top"
|
|
3572
|
+
/>
|
|
3573
|
+
</th>
|
|
3574
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
3575
|
+
<th class="hidden sm:table-cell">
|
|
3576
|
+
{{ 'shared.code' | translate }}
|
|
3577
|
+
</th>
|
|
3578
|
+
<th class="hidden md:table-cell">
|
|
3579
|
+
{{ 'shared.description' | translate }}
|
|
3580
|
+
</th>
|
|
3581
|
+
</tr>
|
|
3582
|
+
</ng-template>
|
|
3583
|
+
<ng-template #body let-role>
|
|
3584
|
+
<tr>
|
|
3585
|
+
<td>
|
|
3586
|
+
<f-checkbox
|
|
3587
|
+
[ngModel]="selectionMap()[role.id]"
|
|
3588
|
+
[binary]="true"
|
|
3589
|
+
(ngModelChange)="onRoleToggle(role, $event)"
|
|
3590
|
+
[fTooltip]="getTooltip(role)"
|
|
3591
|
+
tooltipPosition="top"
|
|
3592
|
+
/>
|
|
3593
|
+
</td>
|
|
3594
|
+
<td>{{ role.name }}</td>
|
|
3595
|
+
<td class="hidden sm:table-cell">{{ role.code || '-' }}</td>
|
|
3596
|
+
<td class="hidden md:table-cell">
|
|
3597
|
+
{{ role.description || '-' }}
|
|
3598
|
+
</td>
|
|
3599
|
+
</tr>
|
|
3600
|
+
</ng-template>
|
|
3601
|
+
<ng-template #emptymessage>
|
|
3602
|
+
<tr>
|
|
3603
|
+
<td colspan="4" class="text-center p-4 text-muted-color">
|
|
3604
|
+
@if (loading()) {
|
|
3605
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
3606
|
+
{{ 'shared.loading.roles' | translate }}
|
|
3607
|
+
} @else {
|
|
3608
|
+
{{ 'iam.permission.no.roles.available' | translate }}
|
|
3609
|
+
}
|
|
3610
|
+
</td>
|
|
3611
|
+
</tr>
|
|
3612
|
+
</ng-template>
|
|
3613
|
+
</f-table>
|
|
3614
|
+
</div>
|
|
3615
|
+
</div>
|
|
3616
|
+
|
|
3617
|
+
<!-- Change Summary -->
|
|
3618
|
+
@if (hasChanges()) {
|
|
3619
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
3620
|
+
<div class="flex items-center gap-2 mb-3">
|
|
3621
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
3622
|
+
<span class="font-bold">{{
|
|
3623
|
+
'shared.pending.changes' | translate
|
|
3624
|
+
}}</span>
|
|
3625
|
+
</div>
|
|
3626
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
3627
|
+
@if (pendingAdd().length > 0) {
|
|
3628
|
+
<div>
|
|
3629
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3630
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
3631
|
+
<strong class="text-sm"
|
|
3632
|
+
>{{ 'shared.to.assign' | translate }} ({{
|
|
3633
|
+
pendingAdd().length
|
|
3634
|
+
}})</strong
|
|
3635
|
+
>
|
|
3636
|
+
</div>
|
|
3637
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
3638
|
+
@for (role of pendingAdd(); track role.id) {
|
|
3639
|
+
<li class="text-sm mb-1">{{ role.name }}</li>
|
|
3640
|
+
}
|
|
3641
|
+
</ul>
|
|
3642
|
+
</div>
|
|
3643
|
+
}
|
|
3644
|
+
@if (pendingRemove().length > 0) {
|
|
3645
|
+
<div>
|
|
3646
|
+
<div class="flex items-center gap-2 mb-2">
|
|
3647
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
3648
|
+
<strong class="text-sm"
|
|
3649
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
3650
|
+
pendingRemove().length
|
|
3651
|
+
}})</strong
|
|
3652
|
+
>
|
|
3653
|
+
</div>
|
|
3654
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
3655
|
+
@for (role of pendingRemove(); track role.id) {
|
|
3656
|
+
<li class="text-sm mb-1">{{ role.name }}</li>
|
|
3657
|
+
}
|
|
3658
|
+
</ul>
|
|
3659
|
+
</div>
|
|
3660
|
+
}
|
|
3661
|
+
</div>
|
|
3662
|
+
</div>
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
@if (!loading() && roles().length === 0) {
|
|
3667
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
3668
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
3669
|
+
<p class="text-muted-color m-0">
|
|
3670
|
+
{{ 'iam.permission.no.roles.for.user' | translate }}
|
|
3671
|
+
</p>
|
|
3672
|
+
</div>
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
</div>
|
|
3676
|
+
`, styles: [":host{display:block}\n"] }]
|
|
3677
|
+
}], ctorParameters: () => [] });
|
|
3678
|
+
|
|
3679
|
+
class UserActionSelectorComponent extends BaseActionSelectorComponent {
|
|
3680
|
+
USER_ACTION_PERMISSIONS = USER_ACTION_PERMISSIONS;
|
|
3681
|
+
appConfig = inject(APP_CONFIG);
|
|
3682
|
+
companyContext = inject(LAYOUT_AUTH_STATE);
|
|
3683
|
+
userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
|
|
3684
|
+
actionApi = inject(ActionApiService);
|
|
3685
|
+
permissionApi = inject(PermissionApiService);
|
|
3686
|
+
messageService = inject(MessageService);
|
|
3687
|
+
selectedUserId = signal(null, /* @ts-ignore */
|
|
3688
|
+
...(ngDevMode ? [{ debugName: "selectedUserId" }] : /* istanbul ignore next */ []));
|
|
3689
|
+
selectedBranchId = signal(undefined, /* @ts-ignore */
|
|
3690
|
+
...(ngDevMode ? [{ debugName: "selectedBranchId" }] : /* istanbul ignore next */ []));
|
|
3691
|
+
branches = signal([], /* @ts-ignore */
|
|
3692
|
+
...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
|
|
3693
|
+
isCompanyFeatureActive = computed(() => isCompanyFeatureEnabled(this.appConfig) === true, /* @ts-ignore */
|
|
3694
|
+
...(ngDevMode ? [{ debugName: "isCompanyFeatureActive" }] : /* istanbul ignore next */ []));
|
|
3695
|
+
showBranchSelector = this.isCompanyFeatureActive;
|
|
3696
|
+
filteredBranches = computed(() => {
|
|
3697
|
+
const currentCompanyId = this.companyContext.currentCompanyInfo()?.id || undefined;
|
|
3698
|
+
if (!currentCompanyId)
|
|
3699
|
+
return this.branches();
|
|
3700
|
+
return this.branches().filter((b) => b.companyId === currentCompanyId);
|
|
3701
|
+
}, /* @ts-ignore */
|
|
3702
|
+
...(ngDevMode ? [{ debugName: "filteredBranches" }] : /* istanbul ignore next */ []));
|
|
3703
|
+
constructor() {
|
|
3704
|
+
super();
|
|
3705
|
+
effect(() => {
|
|
3706
|
+
const userId = this.selectedUserId();
|
|
3707
|
+
this.selectedBranchId.set(undefined);
|
|
3708
|
+
if (userId) {
|
|
3709
|
+
if (this.isCompanyFeatureActive()) {
|
|
3710
|
+
this.loadUserBranches(userId);
|
|
3711
|
+
}
|
|
3712
|
+
this.loadData();
|
|
3713
|
+
}
|
|
3714
|
+
else {
|
|
3715
|
+
this.branches.set([]);
|
|
3716
|
+
this.resetState();
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
effect(() => {
|
|
3720
|
+
const branchId = this.selectedBranchId();
|
|
3721
|
+
const userId = this.selectedUserId();
|
|
3722
|
+
if (this.isCompanyFeatureActive() && userId && branchId !== undefined) {
|
|
3723
|
+
this.loadData();
|
|
3724
|
+
}
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
getDefaultTooltip(isSelected) {
|
|
3728
|
+
if (isSelected) {
|
|
3729
|
+
return `[${this.translate('shared.assigned')}] ${this.translate('iam.tooltip.assigned.to.user')}\n\n[${this.translate('shared.remove')}] ${this.translate('iam.tooltip.click.to.remove.user')}`;
|
|
3730
|
+
}
|
|
3731
|
+
return `[${this.translate('shared.add')}] ${this.translate('iam.tooltip.click.to.assign.user')}`;
|
|
3732
|
+
}
|
|
3733
|
+
async loadUserBranches(userId) {
|
|
3734
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id;
|
|
3735
|
+
if (!companyId) {
|
|
3736
|
+
this.branches.set([]);
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
const response = await firstValueFrom(this.userPermissionProvider.getUserBranchPermissions(userId));
|
|
3741
|
+
const userBranches = response?.data?.map((ub) => ({
|
|
3742
|
+
id: ub.branchId,
|
|
3743
|
+
name: ub.branchName,
|
|
3744
|
+
companyId: ub.companyId,
|
|
3745
|
+
})) ?? [];
|
|
3746
|
+
this.branches.set(userBranches);
|
|
3747
|
+
}
|
|
3748
|
+
catch { }
|
|
3749
|
+
}
|
|
3750
|
+
async loadData() {
|
|
3751
|
+
const userId = this.selectedUserId();
|
|
3752
|
+
if (!userId)
|
|
3753
|
+
return;
|
|
3754
|
+
const branchId = this.selectedBranchId();
|
|
3755
|
+
this.loading.set(true);
|
|
3756
|
+
try {
|
|
3757
|
+
const actionsResponse = await firstValueFrom(this.actionApi.getActionsForPermission());
|
|
3758
|
+
const flatActions = actionsResponse?.data ?? [];
|
|
3759
|
+
const actionsTree = buildTreeFromFlat(flatActions);
|
|
3760
|
+
this._actionsTree.set(actionsTree);
|
|
3761
|
+
this._actions.set(flatActions);
|
|
3762
|
+
const query = this.isCompanyFeatureActive() && branchId ? { branchId } : undefined;
|
|
3763
|
+
const assignedResponse = await firstValueFrom(this.permissionApi.getUserActions(userId, query));
|
|
3764
|
+
const assignedIds = new Set((assignedResponse?.data || []).map((a) => a.actionId));
|
|
3765
|
+
const selMap = this.buildSelectionMap(flatActions, (action) => assignedIds.has(action.id));
|
|
3766
|
+
this._selectionMap.set(selMap);
|
|
3767
|
+
this._initialSelection.set({ ...selMap });
|
|
3768
|
+
}
|
|
3769
|
+
catch {
|
|
3770
|
+
}
|
|
3771
|
+
finally {
|
|
3772
|
+
this.loading.set(false);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
async saveChanges() {
|
|
3776
|
+
const userId = this.selectedUserId();
|
|
3777
|
+
if (!userId)
|
|
3778
|
+
return;
|
|
3779
|
+
const branchId = this.selectedBranchId();
|
|
3780
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id;
|
|
3781
|
+
if (this.isCompanyFeatureActive() && !companyId) {
|
|
3782
|
+
this.messageService.add({
|
|
3783
|
+
severity: 'warn',
|
|
3784
|
+
summary: this.translate('shared.warning'),
|
|
3785
|
+
detail: this.translate('iam.permission.company.required'),
|
|
3786
|
+
});
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3789
|
+
const invalidActions = this.permissionLogic.getActionsWithUnmetPrerequisites(this.selectionMap(), this.actions());
|
|
3790
|
+
if (invalidActions.length > 0) {
|
|
3791
|
+
this.permissionLogic.showValidationErrorDialog(invalidActions, this.selectionMap(), this.actions(), (newMap) => this._selectionMap.set(newMap));
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
const items = this.buildPayloadItems();
|
|
3795
|
+
if (items.length === 0)
|
|
3796
|
+
return;
|
|
3797
|
+
this.saving.set(true);
|
|
3798
|
+
try {
|
|
3799
|
+
const payload = { userId, items };
|
|
3800
|
+
if (this.isCompanyFeatureActive()) {
|
|
3801
|
+
if (companyId)
|
|
3802
|
+
payload.companyId = companyId;
|
|
3803
|
+
if (branchId)
|
|
3804
|
+
payload.branchId = branchId;
|
|
3805
|
+
}
|
|
3806
|
+
const response = await firstValueFrom(this.permissionApi.assignUserActions(payload));
|
|
3807
|
+
this.messageService.add({
|
|
3808
|
+
severity: 'success',
|
|
3809
|
+
summary: this.translate('shared.success'),
|
|
3810
|
+
detail: response?.messageKey
|
|
3811
|
+
? this.translate(response.messageKey, response.messageVariables)
|
|
3812
|
+
: response?.message,
|
|
3813
|
+
});
|
|
3814
|
+
this._initialSelection.set({ ...this.selectionMap() });
|
|
3815
|
+
}
|
|
3816
|
+
catch {
|
|
3817
|
+
}
|
|
3818
|
+
finally {
|
|
3819
|
+
this.saving.set(false);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserActionSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3823
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: UserActionSelectorComponent, isStandalone: true, selector: "flusys-user-action-selector", usesInheritance: true, ngImport: i0, template: `
|
|
3824
|
+
<div class="user-action-selector">
|
|
3825
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
3826
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
3827
|
+
<div>
|
|
3828
|
+
<label class="block font-semibold mb-2">
|
|
3829
|
+
<ui-icon name="user" class="mr-2 text-primary"></ui-icon>
|
|
3830
|
+
{{ 'iam.permission.select.user' | translate }}
|
|
3831
|
+
</label>
|
|
3832
|
+
<lib-user-select
|
|
3833
|
+
[value]="selectedUserId()"
|
|
3834
|
+
(valueChange)="selectedUserId.set($event)"
|
|
3835
|
+
[isEditMode]="true"
|
|
3836
|
+
[placeHolder]="
|
|
3837
|
+
'iam.permission.select.user.placeholder' | translate
|
|
3838
|
+
"
|
|
3839
|
+
/>
|
|
3840
|
+
</div>
|
|
3841
|
+
|
|
3842
|
+
@if (showBranchSelector()) {
|
|
3843
|
+
<div>
|
|
3844
|
+
<label class="block font-semibold mb-2">
|
|
3845
|
+
<ui-icon name="building" class="mr-2 text-primary"></ui-icon>
|
|
3846
|
+
{{ 'iam.permission.select.branch' | translate }}
|
|
3847
|
+
</label>
|
|
3848
|
+
<f-select
|
|
3849
|
+
[ngModel]="selectedBranchId()"
|
|
3850
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
3851
|
+
[options]="filteredBranches()"
|
|
3852
|
+
optionLabel="name"
|
|
3853
|
+
optionValue="id"
|
|
3854
|
+
[placeholder]="
|
|
3855
|
+
'iam.permission.select.branch.placeholder' | translate
|
|
3856
|
+
"
|
|
3857
|
+
[showClear]="true"
|
|
3858
|
+
[filter]="true"
|
|
3859
|
+
class="w-full"
|
|
3860
|
+
>
|
|
3861
|
+
<ng-template #selectedItem let-branch>
|
|
3862
|
+
@if (branch) {
|
|
3863
|
+
<div class="flex items-center gap-2">
|
|
3864
|
+
<ui-icon name="building" class="text-primary"></ui-icon>
|
|
3865
|
+
<span class="font-semibold">{{ branch.name }}</span>
|
|
3866
|
+
</div>
|
|
3867
|
+
}
|
|
3868
|
+
</ng-template>
|
|
3869
|
+
<ng-template #item let-branch>
|
|
3870
|
+
<div class="flex items-center gap-2">
|
|
3871
|
+
<ui-icon name="building" class="text-muted-color"></ui-icon>
|
|
3872
|
+
<span>{{ branch.name }}</span>
|
|
3873
|
+
</div>
|
|
3874
|
+
</ng-template>
|
|
3875
|
+
</f-select>
|
|
3876
|
+
<small class="text-muted-color block mt-1">
|
|
3877
|
+
{{
|
|
3878
|
+
(filteredBranches().length !== 1
|
|
3879
|
+
? 'iam.branch.permitted.count.plural'
|
|
3880
|
+
: 'iam.branch.permitted.count'
|
|
3881
|
+
) | translate: { count: filteredBranches().length }
|
|
3882
|
+
}}
|
|
3883
|
+
</small>
|
|
3884
|
+
</div>
|
|
3885
|
+
}
|
|
3886
|
+
</div>
|
|
3887
|
+
</div>
|
|
3888
|
+
|
|
3889
|
+
@if (selectedUserId()) {
|
|
3890
|
+
@if (loading()) {
|
|
3891
|
+
<div class="flex justify-center p-5">
|
|
3892
|
+
<ui-icon name="loader-circle" spin class="text-4xl"></ui-icon>
|
|
3893
|
+
</div>
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
@if (!loading() && actions().length > 0) {
|
|
3897
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
3898
|
+
<div
|
|
3899
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
3900
|
+
>
|
|
3901
|
+
<div>
|
|
3902
|
+
<h5 class="m-0 mb-1">
|
|
3903
|
+
{{ 'iam.permission.direct.action.permissions' | translate }}
|
|
3904
|
+
</h5>
|
|
3905
|
+
<p class="text-sm text-muted-color m-0">
|
|
3906
|
+
{{
|
|
3907
|
+
'iam.permission.actions.available'
|
|
3908
|
+
| translate: { count: actions().length }
|
|
3909
|
+
}}
|
|
3910
|
+
</p>
|
|
3911
|
+
</div>
|
|
3912
|
+
<div class="flex flex-wrap gap-2">
|
|
3913
|
+
<button
|
|
3914
|
+
fButton
|
|
3915
|
+
[outlined]="true"
|
|
3916
|
+
size="small"
|
|
3917
|
+
severity="secondary"
|
|
3918
|
+
(click)="selectAll()"
|
|
3919
|
+
>
|
|
3920
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
3921
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
3922
|
+
</button>
|
|
3923
|
+
<button
|
|
3924
|
+
fButton
|
|
3925
|
+
[outlined]="true"
|
|
3926
|
+
size="small"
|
|
3927
|
+
severity="secondary"
|
|
3928
|
+
(click)="deselectAll()"
|
|
3929
|
+
>
|
|
3930
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
3931
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
3932
|
+
</button>
|
|
3933
|
+
<button
|
|
3934
|
+
fButton
|
|
3935
|
+
*hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
|
|
3936
|
+
[disabled]="!canSave() || saving()"
|
|
3937
|
+
size="small"
|
|
3938
|
+
(click)="saveChanges()"
|
|
3939
|
+
>
|
|
3940
|
+
@if (saving()) {
|
|
3941
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
3942
|
+
} @else {
|
|
3943
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
3944
|
+
}
|
|
3945
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
3946
|
+
</button>
|
|
3947
|
+
</div>
|
|
3948
|
+
</div>
|
|
3949
|
+
|
|
3950
|
+
@if (invalidActionsCount() > 0) {
|
|
3951
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
3952
|
+
<div class="flex items-start gap-2">
|
|
3953
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
3954
|
+
<div class="flex-1">
|
|
3955
|
+
<span class="font-semibold"
|
|
3956
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
3957
|
+
>
|
|
3958
|
+
<p class="text-sm mt-1 mb-0">
|
|
3959
|
+
{{
|
|
3960
|
+
(invalidActionsCount() > 1
|
|
3961
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
3962
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
3963
|
+
) | translate: { count: invalidActionsCount() }
|
|
3964
|
+
}}
|
|
3965
|
+
</p>
|
|
3966
|
+
</div>
|
|
3967
|
+
</div>
|
|
3968
|
+
</div>
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
3972
|
+
<f-treetable
|
|
3973
|
+
[value]="treeNodes()"
|
|
3974
|
+
dataKey="id"
|
|
3975
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
3976
|
+
>
|
|
3977
|
+
<ng-template #header>
|
|
3978
|
+
<tr>
|
|
3979
|
+
<th class="w-12">
|
|
3980
|
+
<f-checkbox
|
|
3981
|
+
[ngModel]="allSelected()"
|
|
3982
|
+
[binary]="true"
|
|
3983
|
+
(ngModelChange)="toggleAll()"
|
|
3984
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
3985
|
+
tooltipPosition="top"
|
|
3986
|
+
/>
|
|
3987
|
+
</th>
|
|
3988
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
3989
|
+
<th class="hidden md:table-cell">
|
|
3990
|
+
{{ 'shared.code' | translate }}
|
|
3991
|
+
</th>
|
|
3992
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
3993
|
+
<th class="hidden lg:table-cell">
|
|
3994
|
+
{{ 'shared.description' | translate }}
|
|
3995
|
+
</th>
|
|
3996
|
+
</tr>
|
|
3997
|
+
</ng-template>
|
|
3998
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
3999
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
4000
|
+
@let tooltip = getTooltip(rowData);
|
|
4001
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
4002
|
+
<td class="w-12">
|
|
4003
|
+
<f-checkbox
|
|
4004
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
4005
|
+
[binary]="true"
|
|
4006
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
4007
|
+
[fTooltip]="tooltip"
|
|
4008
|
+
tooltipPosition="top"
|
|
4009
|
+
/>
|
|
4010
|
+
</td>
|
|
4011
|
+
<td>
|
|
4012
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
4013
|
+
<span class="inline-flex items-center gap-2">
|
|
4014
|
+
{{ rowData.name }}
|
|
4015
|
+
@if (hasUnmet) {
|
|
4016
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
4017
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
4018
|
+
| translate
|
|
4019
|
+
"
|
|
4020
|
+
tooltipPosition="top"></ui-icon>
|
|
4021
|
+
}
|
|
4022
|
+
</span>
|
|
4023
|
+
</td>
|
|
4024
|
+
<td class="hidden md:table-cell">
|
|
4025
|
+
{{ rowData.code || '-' }}
|
|
4026
|
+
</td>
|
|
4027
|
+
<td>
|
|
4028
|
+
<f-tag
|
|
4029
|
+
[value]="rowData.actionType"
|
|
4030
|
+
[severity]="
|
|
4031
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
4032
|
+
"
|
|
4033
|
+
/>
|
|
4034
|
+
</td>
|
|
4035
|
+
<td class="hidden lg:table-cell">
|
|
4036
|
+
{{ rowData.description || '-' }}
|
|
4037
|
+
</td>
|
|
4038
|
+
</tr>
|
|
4039
|
+
</ng-template>
|
|
4040
|
+
<ng-template #emptymessage>
|
|
4041
|
+
<tr>
|
|
4042
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
4043
|
+
@if (loading()) {
|
|
4044
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
4045
|
+
{{ 'shared.loading.actions' | translate }}
|
|
4046
|
+
} @else {
|
|
4047
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
4048
|
+
}
|
|
4049
|
+
</td>
|
|
4050
|
+
</tr>
|
|
4051
|
+
</ng-template>
|
|
4052
|
+
</f-treetable>
|
|
4053
|
+
</div>
|
|
4054
|
+
</div>
|
|
4055
|
+
|
|
4056
|
+
@if (hasChanges()) {
|
|
4057
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
4058
|
+
<div class="flex items-center gap-2 mb-3">
|
|
4059
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
4060
|
+
<span class="font-bold">{{
|
|
4061
|
+
'shared.pending.changes' | translate
|
|
4062
|
+
}}</span>
|
|
4063
|
+
</div>
|
|
4064
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
4065
|
+
@if (pendingAdd().length > 0) {
|
|
4066
|
+
<div>
|
|
4067
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4068
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
4069
|
+
<strong class="text-sm"
|
|
4070
|
+
>{{ 'shared.to.assign' | translate }} ({{
|
|
4071
|
+
pendingAdd().length
|
|
4072
|
+
}})</strong
|
|
4073
|
+
>
|
|
4074
|
+
</div>
|
|
4075
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
4076
|
+
@for (action of pendingAdd(); track action.id) {
|
|
4077
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
4078
|
+
}
|
|
4079
|
+
</ul>
|
|
4080
|
+
</div>
|
|
4081
|
+
}
|
|
4082
|
+
@if (pendingRemove().length > 0) {
|
|
4083
|
+
<div>
|
|
4084
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4085
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
4086
|
+
<strong class="text-sm"
|
|
4087
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
4088
|
+
pendingRemove().length
|
|
4089
|
+
}})</strong
|
|
4090
|
+
>
|
|
4091
|
+
</div>
|
|
4092
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
4093
|
+
@for (action of pendingRemove(); track action.id) {
|
|
4094
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
4095
|
+
}
|
|
4096
|
+
</ul>
|
|
4097
|
+
</div>
|
|
4098
|
+
}
|
|
4099
|
+
</div>
|
|
4100
|
+
</div>
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
@if (!loading() && actions().length === 0) {
|
|
4105
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
4106
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
4107
|
+
<p class="text-muted-color m-0">
|
|
4108
|
+
{{ 'iam.permission.no.actions.for.user' | translate }}
|
|
4109
|
+
</p>
|
|
4110
|
+
</div>
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
</div>
|
|
4114
|
+
`, isInline: true, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: UiModule }, { kind: "directive", type: i2.FButtonDirective, selector: "[fButton]", inputs: ["severity", "text", "outlined", "link", "variant", "rounded", "raised", "fluid", "disabled", "loading", "loadingIcon", "size", "label", "icon", "iconPos", "ariaLabel", "plain"] }, { kind: "component", type: i2.TagComponent, selector: "f-tag", inputs: ["value", "severity", "icon", "rounded", "color", "styleClass", "style"] }, { kind: "component", type: i2.CheckboxComponent, selector: "f-checkbox", inputs: ["label", "binary", "disabled", "readonly", "inputId", "invalid", "indeterminate", "variant", "size", "touched", "styleClass", "style", "checked"], outputs: ["onChange", "onBlur", "touch", "checkedChange"] }, { kind: "component", type: i2.SelectComponent, selector: "f-select", inputs: ["options", "optionLabel", "optionValue", "placeholder", "disabled", "filter", "filterBy", "filterMatchMode", "filterLocale", "filterPlaceholder", "resetFilterOnHide", "autofocusFilter", "emptyMessage", "emptyFilterMessage", "showClear", "loading", "loadingIcon", "dropdownIcon", "clearIcon", "inputId", "invalid", "touched", "styleClass", "style", "editable", "readonly", "autofocus", "checkmark", "group", "optionGroupLabel", "optionGroupChildren", "virtualScroll", "virtualScrollItemSize", "lazy"], outputs: ["onFilter", "onChange", "onShow", "onHide", "onBlur", "onFocus", "onClear", "onLazyLoad"] }, { kind: "directive", type: i2.TooltipDirective, selector: "[fTooltip]", inputs: ["fTooltip", "tooltipPosition", "positionStyle", "tooltipStyleClass", "disabled", "tooltipEvent", "showDelay", "hideDelay", "life", "autoHide", "positionTop", "positionLeft", "escape"] }, { kind: "component", type: i2.TreeTableComponent, selector: "f-treetable", inputs: ["value", "loading", "dataKey", "styleClass", "tableStyle", "scrollable", "scrollHeight", "resizableColumns", "lazy", "totalRecords", "paginator", "first", "rows", "rowsPerPageOptions", "showCurrentPageReport", "currentPageReportTemplate", "globalFilterFields", "filterDelay", "selectionMode", "sortMode", "virtualScroll", "virtualScrollItemSize", "sortField", "sortOrder", "multiSortMeta", "filters", "selection", "contextMenuSelection"], outputs: ["sortFieldChange", "sortOrderChange", "multiSortMetaChange", "filtersChange", "selectionChange", "contextMenuSelectionChange", "onLazyLoad", "onSort", "onFilter", "onNodeExpand", "onNodeCollapse", "onNodeSelect", "onNodeUnselect", "onRowContextMenu"] }, { kind: "component", type: i2.TreeTableTogglerComponent, selector: "f-treetabletoggler", inputs: ["rowNode"] }, { kind: "component", type: i2.UiIconComponent, selector: "ui-icon", inputs: ["name", "size", "spin", "fill", "severity"] }, { kind: "directive", type: HasPermissionDirective, selector: "[hasPermission]", inputs: ["hasPermission"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
|
|
4115
|
+
}
|
|
4116
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: UserActionSelectorComponent, decorators: [{
|
|
4117
|
+
type: Component,
|
|
4118
|
+
args: [{ selector: 'flusys-user-action-selector', imports: [
|
|
4119
|
+
FormsModule,
|
|
4120
|
+
UiModule,
|
|
4121
|
+
HasPermissionDirective,
|
|
4122
|
+
UserSelectComponent,
|
|
4123
|
+
TranslatePipe,
|
|
4124
|
+
], template: `
|
|
4125
|
+
<div class="user-action-selector">
|
|
4126
|
+
<div class="surface-card p-4 rounded-border mb-4 shadow-sm">
|
|
4127
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
|
4128
|
+
<div>
|
|
4129
|
+
<label class="block font-semibold mb-2">
|
|
4130
|
+
<ui-icon name="user" class="mr-2 text-primary"></ui-icon>
|
|
4131
|
+
{{ 'iam.permission.select.user' | translate }}
|
|
4132
|
+
</label>
|
|
4133
|
+
<lib-user-select
|
|
4134
|
+
[value]="selectedUserId()"
|
|
4135
|
+
(valueChange)="selectedUserId.set($event)"
|
|
4136
|
+
[isEditMode]="true"
|
|
4137
|
+
[placeHolder]="
|
|
4138
|
+
'iam.permission.select.user.placeholder' | translate
|
|
4139
|
+
"
|
|
4140
|
+
/>
|
|
4141
|
+
</div>
|
|
4142
|
+
|
|
4143
|
+
@if (showBranchSelector()) {
|
|
4144
|
+
<div>
|
|
4145
|
+
<label class="block font-semibold mb-2">
|
|
4146
|
+
<ui-icon name="building" class="mr-2 text-primary"></ui-icon>
|
|
4147
|
+
{{ 'iam.permission.select.branch' | translate }}
|
|
4148
|
+
</label>
|
|
4149
|
+
<f-select
|
|
4150
|
+
[ngModel]="selectedBranchId()"
|
|
4151
|
+
(ngModelChange)="selectedBranchId.set($event)"
|
|
4152
|
+
[options]="filteredBranches()"
|
|
4153
|
+
optionLabel="name"
|
|
4154
|
+
optionValue="id"
|
|
4155
|
+
[placeholder]="
|
|
4156
|
+
'iam.permission.select.branch.placeholder' | translate
|
|
4157
|
+
"
|
|
4158
|
+
[showClear]="true"
|
|
4159
|
+
[filter]="true"
|
|
4160
|
+
class="w-full"
|
|
4161
|
+
>
|
|
4162
|
+
<ng-template #selectedItem let-branch>
|
|
4163
|
+
@if (branch) {
|
|
4164
|
+
<div class="flex items-center gap-2">
|
|
4165
|
+
<ui-icon name="building" class="text-primary"></ui-icon>
|
|
4166
|
+
<span class="font-semibold">{{ branch.name }}</span>
|
|
4167
|
+
</div>
|
|
4168
|
+
}
|
|
4169
|
+
</ng-template>
|
|
4170
|
+
<ng-template #item let-branch>
|
|
4171
|
+
<div class="flex items-center gap-2">
|
|
4172
|
+
<ui-icon name="building" class="text-muted-color"></ui-icon>
|
|
4173
|
+
<span>{{ branch.name }}</span>
|
|
4174
|
+
</div>
|
|
4175
|
+
</ng-template>
|
|
4176
|
+
</f-select>
|
|
4177
|
+
<small class="text-muted-color block mt-1">
|
|
4178
|
+
{{
|
|
4179
|
+
(filteredBranches().length !== 1
|
|
4180
|
+
? 'iam.branch.permitted.count.plural'
|
|
4181
|
+
: 'iam.branch.permitted.count'
|
|
4182
|
+
) | translate: { count: filteredBranches().length }
|
|
4183
|
+
}}
|
|
4184
|
+
</small>
|
|
4185
|
+
</div>
|
|
4186
|
+
}
|
|
4187
|
+
</div>
|
|
4188
|
+
</div>
|
|
4189
|
+
|
|
4190
|
+
@if (selectedUserId()) {
|
|
4191
|
+
@if (loading()) {
|
|
4192
|
+
<div class="flex justify-center p-5">
|
|
4193
|
+
<ui-icon name="loader-circle" spin class="text-4xl"></ui-icon>
|
|
4194
|
+
</div>
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
@if (!loading() && actions().length > 0) {
|
|
4198
|
+
<div class="surface-card p-4 rounded-border shadow-sm">
|
|
4199
|
+
<div
|
|
4200
|
+
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4"
|
|
4201
|
+
>
|
|
4202
|
+
<div>
|
|
4203
|
+
<h5 class="m-0 mb-1">
|
|
4204
|
+
{{ 'iam.permission.direct.action.permissions' | translate }}
|
|
4205
|
+
</h5>
|
|
4206
|
+
<p class="text-sm text-muted-color m-0">
|
|
4207
|
+
{{
|
|
4208
|
+
'iam.permission.actions.available'
|
|
4209
|
+
| translate: { count: actions().length }
|
|
4210
|
+
}}
|
|
4211
|
+
</p>
|
|
4212
|
+
</div>
|
|
4213
|
+
<div class="flex flex-wrap gap-2">
|
|
4214
|
+
<button
|
|
4215
|
+
fButton
|
|
4216
|
+
[outlined]="true"
|
|
4217
|
+
size="small"
|
|
4218
|
+
severity="secondary"
|
|
4219
|
+
(click)="selectAll()"
|
|
4220
|
+
>
|
|
4221
|
+
<ui-icon pButtonIcon name="check"></ui-icon>
|
|
4222
|
+
<span pButtonLabel>{{ 'shared.select.all' | translate }}</span>
|
|
4223
|
+
</button>
|
|
4224
|
+
<button
|
|
4225
|
+
fButton
|
|
4226
|
+
[outlined]="true"
|
|
4227
|
+
size="small"
|
|
4228
|
+
severity="secondary"
|
|
4229
|
+
(click)="deselectAll()"
|
|
4230
|
+
>
|
|
4231
|
+
<ui-icon pButtonIcon name="times"></ui-icon>
|
|
4232
|
+
<span pButtonLabel>{{ 'shared.deselect.all' | translate }}</span>
|
|
4233
|
+
</button>
|
|
4234
|
+
<button
|
|
4235
|
+
fButton
|
|
4236
|
+
*hasPermission="USER_ACTION_PERMISSIONS.ASSIGN"
|
|
4237
|
+
[disabled]="!canSave() || saving()"
|
|
4238
|
+
size="small"
|
|
4239
|
+
(click)="saveChanges()"
|
|
4240
|
+
>
|
|
4241
|
+
@if (saving()) {
|
|
4242
|
+
<ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
|
|
4243
|
+
} @else {
|
|
4244
|
+
<ui-icon pButtonIcon name="save"></ui-icon>
|
|
4245
|
+
}
|
|
4246
|
+
<span pButtonLabel>{{ 'shared.save.changes' | translate }}</span>
|
|
4247
|
+
</button>
|
|
4248
|
+
</div>
|
|
4249
|
+
</div>
|
|
4250
|
+
|
|
4251
|
+
@if (invalidActionsCount() > 0) {
|
|
4252
|
+
<div class="validation-warning rounded-border p-3 mb-4">
|
|
4253
|
+
<div class="flex items-start gap-2">
|
|
4254
|
+
<ui-icon name="exclamation-triangle" class="text-xl"></ui-icon>
|
|
4255
|
+
<div class="flex-1">
|
|
4256
|
+
<span class="font-semibold"
|
|
4257
|
+
>{{ 'iam.validation.warning.title' | translate }}:</span
|
|
4258
|
+
>
|
|
4259
|
+
<p class="text-sm mt-1 mb-0">
|
|
4260
|
+
{{
|
|
4261
|
+
(invalidActionsCount() > 1
|
|
4262
|
+
? 'iam.validation.unmet.prerequisites.plural'
|
|
4263
|
+
: 'iam.validation.unmet.prerequisites.singular'
|
|
4264
|
+
) | translate: { count: invalidActionsCount() }
|
|
4265
|
+
}}
|
|
4266
|
+
</p>
|
|
4267
|
+
</div>
|
|
4268
|
+
</div>
|
|
4269
|
+
</div>
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
4273
|
+
<f-treetable
|
|
4274
|
+
[value]="treeNodes()"
|
|
4275
|
+
dataKey="id"
|
|
4276
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
4277
|
+
>
|
|
4278
|
+
<ng-template #header>
|
|
4279
|
+
<tr>
|
|
4280
|
+
<th class="w-12">
|
|
4281
|
+
<f-checkbox
|
|
4282
|
+
[ngModel]="allSelected()"
|
|
4283
|
+
[binary]="true"
|
|
4284
|
+
(ngModelChange)="toggleAll()"
|
|
4285
|
+
[fTooltip]="'shared.select.deselect.all' | translate"
|
|
4286
|
+
tooltipPosition="top"
|
|
4287
|
+
/>
|
|
4288
|
+
</th>
|
|
4289
|
+
<th>{{ 'shared.name' | translate }}</th>
|
|
4290
|
+
<th class="hidden md:table-cell">
|
|
4291
|
+
{{ 'shared.code' | translate }}
|
|
4292
|
+
</th>
|
|
4293
|
+
<th>{{ 'iam.action.type' | translate }}</th>
|
|
4294
|
+
<th class="hidden lg:table-cell">
|
|
4295
|
+
{{ 'shared.description' | translate }}
|
|
4296
|
+
</th>
|
|
4297
|
+
</tr>
|
|
4298
|
+
</ng-template>
|
|
4299
|
+
<ng-template #body let-rowNode let-rowData="rowData">
|
|
4300
|
+
@let hasUnmet = hasUnmetPrerequisites(rowData);
|
|
4301
|
+
@let tooltip = getTooltip(rowData);
|
|
4302
|
+
<tr [class.highlight-warning]="hasUnmet">
|
|
4303
|
+
<td class="w-12">
|
|
4304
|
+
<f-checkbox
|
|
4305
|
+
[ngModel]="selectionMap()[rowData.id]"
|
|
4306
|
+
[binary]="true"
|
|
4307
|
+
(ngModelChange)="onActionToggle(rowData, $event)"
|
|
4308
|
+
[fTooltip]="tooltip"
|
|
4309
|
+
tooltipPosition="top"
|
|
4310
|
+
/>
|
|
4311
|
+
</td>
|
|
4312
|
+
<td>
|
|
4313
|
+
<f-treetabletoggler [rowNode]="rowNode" />
|
|
4314
|
+
<span class="inline-flex items-center gap-2">
|
|
4315
|
+
{{ rowData.name }}
|
|
4316
|
+
@if (hasUnmet) {
|
|
4317
|
+
<ui-icon name="exclamation-triangle" class="text-orange-500" [fTooltip]="
|
|
4318
|
+
'iam.validation.unmet.prerequisites.tooltip'
|
|
4319
|
+
| translate
|
|
4320
|
+
"
|
|
4321
|
+
tooltipPosition="top"></ui-icon>
|
|
4322
|
+
}
|
|
4323
|
+
</span>
|
|
4324
|
+
</td>
|
|
4325
|
+
<td class="hidden md:table-cell">
|
|
4326
|
+
{{ rowData.code || '-' }}
|
|
4327
|
+
</td>
|
|
4328
|
+
<td>
|
|
4329
|
+
<f-tag
|
|
4330
|
+
[value]="rowData.actionType"
|
|
4331
|
+
[severity]="
|
|
4332
|
+
rowData.actionType === 'backend' ? 'info' : 'success'
|
|
4333
|
+
"
|
|
4334
|
+
/>
|
|
4335
|
+
</td>
|
|
4336
|
+
<td class="hidden lg:table-cell">
|
|
4337
|
+
{{ rowData.description || '-' }}
|
|
4338
|
+
</td>
|
|
4339
|
+
</tr>
|
|
4340
|
+
</ng-template>
|
|
4341
|
+
<ng-template #emptymessage>
|
|
4342
|
+
<tr>
|
|
4343
|
+
<td colspan="5" class="text-center p-4 text-muted-color">
|
|
4344
|
+
@if (loading()) {
|
|
4345
|
+
<ui-icon name="loader-circle" spin></ui-icon>
|
|
4346
|
+
{{ 'shared.loading.actions' | translate }}
|
|
4347
|
+
} @else {
|
|
4348
|
+
{{ 'iam.permission.no.actions.available' | translate }}
|
|
4349
|
+
}
|
|
4350
|
+
</td>
|
|
4351
|
+
</tr>
|
|
4352
|
+
</ng-template>
|
|
4353
|
+
</f-treetable>
|
|
4354
|
+
</div>
|
|
4355
|
+
</div>
|
|
4356
|
+
|
|
4357
|
+
@if (hasChanges()) {
|
|
4358
|
+
<div class="border border-surface rounded-border p-3 mt-4">
|
|
4359
|
+
<div class="flex items-center gap-2 mb-3">
|
|
4360
|
+
<ui-icon name="info-circle" class="text-primary"></ui-icon>
|
|
4361
|
+
<span class="font-bold">{{
|
|
4362
|
+
'shared.pending.changes' | translate
|
|
4363
|
+
}}</span>
|
|
4364
|
+
</div>
|
|
4365
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
4366
|
+
@if (pendingAdd().length > 0) {
|
|
4367
|
+
<div>
|
|
4368
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4369
|
+
<ui-icon name="plus-circle" class="text-green-500"></ui-icon>
|
|
4370
|
+
<strong class="text-sm"
|
|
4371
|
+
>{{ 'shared.to.assign' | translate }} ({{
|
|
4372
|
+
pendingAdd().length
|
|
4373
|
+
}})</strong
|
|
4374
|
+
>
|
|
4375
|
+
</div>
|
|
4376
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
4377
|
+
@for (action of pendingAdd(); track action.id) {
|
|
4378
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
4379
|
+
}
|
|
4380
|
+
</ul>
|
|
4381
|
+
</div>
|
|
4382
|
+
}
|
|
4383
|
+
@if (pendingRemove().length > 0) {
|
|
4384
|
+
<div>
|
|
4385
|
+
<div class="flex items-center gap-2 mb-2">
|
|
4386
|
+
<ui-icon name="minus-circle" class="text-red-500"></ui-icon>
|
|
4387
|
+
<strong class="text-sm"
|
|
4388
|
+
>{{ 'shared.to.remove' | translate }} ({{
|
|
4389
|
+
pendingRemove().length
|
|
4390
|
+
}})</strong
|
|
4391
|
+
>
|
|
4392
|
+
</div>
|
|
4393
|
+
<ul class="list-none p-0 m-0 pl-4">
|
|
4394
|
+
@for (action of pendingRemove(); track action.id) {
|
|
4395
|
+
<li class="text-sm mb-1">{{ action.name }}</li>
|
|
4396
|
+
}
|
|
4397
|
+
</ul>
|
|
4398
|
+
</div>
|
|
4399
|
+
}
|
|
4400
|
+
</div>
|
|
4401
|
+
</div>
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
@if (!loading() && actions().length === 0) {
|
|
4406
|
+
<div class="surface-card p-5 rounded-border shadow-sm text-center">
|
|
4407
|
+
<ui-icon name="info-circle" class="text-muted-color mb-3 block text-5xl"></ui-icon>
|
|
4408
|
+
<p class="text-muted-color m-0">
|
|
4409
|
+
{{ 'iam.permission.no.actions.for.user' | translate }}
|
|
4410
|
+
</p>
|
|
4411
|
+
</div>
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
</div>
|
|
4415
|
+
`, styles: [":host{display:block}.validation-warning{background-color:var(--f-yellow-50, #fefce8);border-left:4px solid var(--f-yellow-500, #eab308);color:var(--f-yellow-700, #a16207)}:host-context(.p-dark) .validation-warning{background-color:#eab3081a;color:var(--f-yellow-400, #facc15)}:host ::ng-deep tr.highlight-warning{background-color:var(--f-red-50, rgba(239, 68, 68, .1))!important}:host-context(.p-dark) ::ng-deep tr.highlight-warning{background-color:#ef444426!important}\n"] }]
|
|
4416
|
+
}], ctorParameters: () => [] });
|
|
4417
|
+
|
|
4418
|
+
// Logic Builder Component
|
|
4419
|
+
|
|
4420
|
+
/**
|
|
4421
|
+
* Profile Permission Provider Adapter
|
|
4422
|
+
*
|
|
4423
|
+
* Implements IProfilePermissionProvider using ng-iam's PermissionApiService.
|
|
4424
|
+
* Allows ng-auth profile page to display user permissions without
|
|
4425
|
+
* depending on ng-iam directly.
|
|
4426
|
+
*
|
|
4427
|
+
* @example
|
|
4428
|
+
* // In app.config.ts
|
|
4429
|
+
* providers: [
|
|
4430
|
+
* ...provideIamProviders()
|
|
4431
|
+
* ]
|
|
4432
|
+
*
|
|
4433
|
+
* // In profile component
|
|
4434
|
+
* private readonly permissionProvider = inject(PROFILE_PERMISSION_PROVIDER, { optional: true });
|
|
4435
|
+
*/
|
|
4436
|
+
class ProfilePermissionProviderAdapter {
|
|
4437
|
+
permissionApi = inject(PermissionApiService);
|
|
4438
|
+
getUserRoles(userId, branchId) {
|
|
4439
|
+
return this.permissionApi
|
|
4440
|
+
.getUserRoles(userId, branchId ? { branchId } : undefined)
|
|
4441
|
+
.pipe(map$1((response) => ({
|
|
4442
|
+
success: response.success,
|
|
4443
|
+
message: response.message,
|
|
4444
|
+
data: (response.data ?? []).map((role) => ({
|
|
4445
|
+
id: role.roleId,
|
|
4446
|
+
name: role.roleName,
|
|
4447
|
+
description: null,
|
|
4448
|
+
})),
|
|
4449
|
+
})));
|
|
4450
|
+
}
|
|
4451
|
+
getUserActions(userId, branchId) {
|
|
4452
|
+
return this.permissionApi
|
|
4453
|
+
.getUserActions(userId, branchId ? { branchId } : undefined)
|
|
4454
|
+
.pipe(map$1((response) => ({
|
|
4455
|
+
success: response.success,
|
|
4456
|
+
message: response.message,
|
|
4457
|
+
data: (response.data ?? []).map((action) => ({
|
|
4458
|
+
id: action.actionId,
|
|
4459
|
+
name: action.actionName,
|
|
4460
|
+
code: action.actionCode,
|
|
4461
|
+
description: null,
|
|
4462
|
+
})),
|
|
4463
|
+
})));
|
|
4464
|
+
}
|
|
4465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
4466
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter });
|
|
4467
|
+
}
|
|
4468
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: ProfilePermissionProviderAdapter, decorators: [{
|
|
4469
|
+
type: Injectable
|
|
4470
|
+
}] });
|
|
4471
|
+
|
|
4472
|
+
/** Registers IAM provider adapters for ng-shared interfaces */
|
|
4473
|
+
function provideIamProviders() {
|
|
4474
|
+
return [
|
|
4475
|
+
{ provide: PROFILE_PERMISSION_PROVIDER, useClass: ProfilePermissionProviderAdapter },
|
|
4476
|
+
];
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
/**
|
|
4480
|
+
* IAM Routes Configuration
|
|
4481
|
+
*
|
|
4482
|
+
* Identity and Access Management routing with permission guards.
|
|
4483
|
+
* - Actions: Permission actions management
|
|
4484
|
+
* - Roles: Role management (conditional on RBAC/FULL mode)
|
|
4485
|
+
* - Permissions: User permission assignments
|
|
4486
|
+
*/
|
|
4487
|
+
const IAM_ROUTES = [
|
|
4488
|
+
{
|
|
4489
|
+
path: '',
|
|
4490
|
+
resolve: { translations: resolveTranslationModule({ modules: ['iam'], fallbackMessages: { ...IAM_MESSAGES, ...SHARED_MESSAGES } }) },
|
|
4491
|
+
loadComponent: () => import('./flusys-ng-iam-iam-container.component-D0m8u6nu.mjs').then((m) => m.IamContainerComponent),
|
|
4492
|
+
children: [
|
|
4493
|
+
// Actions Management
|
|
4494
|
+
{
|
|
4495
|
+
path: 'actions',
|
|
4496
|
+
canActivate: [permissionGuard(ACTION_PERMISSIONS.READ)],
|
|
4497
|
+
children: [
|
|
4498
|
+
{
|
|
4499
|
+
path: '',
|
|
4500
|
+
loadComponent: () => import('./flusys-ng-iam-action-list-page.component-UGKclpFx.mjs').then((m) => m.ActionListPageComponent),
|
|
4501
|
+
},
|
|
4502
|
+
{
|
|
4503
|
+
path: 'new',
|
|
4504
|
+
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs').then((m) => m.ActionFormPageComponent),
|
|
4505
|
+
},
|
|
4506
|
+
{
|
|
4507
|
+
path: ':id',
|
|
4508
|
+
loadComponent: () => import('./flusys-ng-iam-action-form-page.component-Vc5Toouu.mjs').then((m) => m.ActionFormPageComponent),
|
|
4509
|
+
},
|
|
4510
|
+
],
|
|
4511
|
+
},
|
|
4512
|
+
// Roles Management
|
|
4513
|
+
{
|
|
4514
|
+
path: 'roles',
|
|
4515
|
+
canActivate: [permissionGuard(ROLE_PERMISSIONS.READ)],
|
|
4516
|
+
children: [
|
|
4517
|
+
{
|
|
4518
|
+
path: '',
|
|
4519
|
+
loadComponent: () => import('./flusys-ng-iam-role-list-page.component-8NGXCeSZ.mjs').then((m) => m.RoleListPageComponent),
|
|
4520
|
+
},
|
|
4521
|
+
{
|
|
4522
|
+
path: 'new',
|
|
4523
|
+
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-InsxdriD.mjs').then((m) => m.RoleFormPageComponent),
|
|
4524
|
+
},
|
|
4525
|
+
{
|
|
4526
|
+
path: ':id',
|
|
4527
|
+
loadComponent: () => import('./flusys-ng-iam-role-form-page.component-InsxdriD.mjs').then((m) => m.RoleFormPageComponent),
|
|
4528
|
+
},
|
|
4529
|
+
],
|
|
4530
|
+
},
|
|
4531
|
+
// Permissions Management (requires any permission tab access)
|
|
4532
|
+
{
|
|
4533
|
+
path: 'permissions',
|
|
4534
|
+
canActivate: [
|
|
4535
|
+
anyPermissionGuard([
|
|
4536
|
+
ROLE_ACTION_PERMISSIONS.READ,
|
|
4537
|
+
USER_ROLE_PERMISSIONS.READ,
|
|
4538
|
+
USER_ACTION_PERMISSIONS.READ,
|
|
4539
|
+
COMPANY_ACTION_PERMISSIONS.READ,
|
|
4540
|
+
]),
|
|
4541
|
+
],
|
|
4542
|
+
loadComponent: () => import('./flusys-ng-iam-permission-page.component-x88X4AgJ.mjs').then((m) => m.PermissionPageComponent),
|
|
4543
|
+
},
|
|
4544
|
+
// Default redirect to actions
|
|
4545
|
+
{
|
|
4546
|
+
path: '',
|
|
4547
|
+
redirectTo: 'actions',
|
|
4548
|
+
pathMatch: 'full',
|
|
4549
|
+
},
|
|
4550
|
+
],
|
|
4551
|
+
},
|
|
4552
|
+
];
|
|
4553
|
+
|
|
4554
|
+
// ==================== INTERFACES ====================
|
|
4555
|
+
|
|
4556
|
+
/**
|
|
4557
|
+
* Generated bundle index. Do not edit.
|
|
4558
|
+
*/
|
|
4559
|
+
|
|
4560
|
+
export { ActionApiService as A, CompanyActionSelectorComponent as C, IAM_MESSAGES as I, LogicBuilderComponent as L, MAX_DROPDOWN_ITEMS as M, PermissionApiService as P, RoleApiService as R, UserRoleSelectorComponent as U, ActionType as a, RoleActionSelectorComponent as b, convertActionToTreeNode as c, UserActionSelectorComponent as d, ActionPermissionLogicService as e, IAM_ROUTES as f, MyPermissionsApiService as g, PermissionStateService as h, ProfilePermissionProviderAdapter as i, provideIamProviders as p };
|
|
4561
|
+
//# sourceMappingURL=flusys-ng-iam-flusys-ng-iam-CrycDULz.mjs.map
|