@flusys/ng-iam 1.1.0-beta → 2.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -24
- package/fesm2022/flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs +389 -0
- package/fesm2022/flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-CQ6RazN0.mjs +262 -0
- package/fesm2022/flusys-ng-iam-action-list-page.component-CQ6RazN0.mjs.map +1 -0
- package/fesm2022/{flusys-ng-iam-flusys-ng-iam-BjdM-Vgz.mjs → flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs} +1002 -1571
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-BToYxEej.mjs +92 -0
- package/fesm2022/flusys-ng-iam-iam-container.component-BToYxEej.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-BS7xXmsn.mjs +137 -0
- package/fesm2022/flusys-ng-iam-permission-page.component-BS7xXmsn.mjs.map +1 -0
- package/fesm2022/{flusys-ng-iam-role-form-page.component-Ctigzpon.mjs → flusys-ng-iam-role-form-page.component-BjPwXkip.mjs} +106 -148
- package/fesm2022/flusys-ng-iam-role-form-page.component-BjPwXkip.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-Cz-jk-R_.mjs +299 -0
- package/fesm2022/flusys-ng-iam-role-list-page.component-Cz-jk-R_.mjs.map +1 -0
- package/fesm2022/flusys-ng-iam.mjs +1 -1
- package/package.json +4 -4
- package/types/flusys-ng-iam.d.ts +75 -454
- package/fesm2022/flusys-ng-iam-action-form-page.component-DBJzC5GS.mjs +0 -467
- package/fesm2022/flusys-ng-iam-action-form-page.component-DBJzC5GS.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-action-list-page.component-Dfts0JCt.mjs +0 -281
- package/fesm2022/flusys-ng-iam-action-list-page.component-Dfts0JCt.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-flusys-ng-iam-BjdM-Vgz.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs +0 -97
- package/fesm2022/flusys-ng-iam-iam-container.component-Chl5MDkV.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-permission-page.component-cDrwUAQ_.mjs +0 -143
- package/fesm2022/flusys-ng-iam-permission-page.component-cDrwUAQ_.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-form-page.component-Ctigzpon.mjs.map +0 -1
- package/fesm2022/flusys-ng-iam-role-list-page.component-BF-Z_TQK.mjs +0 -266
- package/fesm2022/flusys-ng-iam-role-list-page.component-BF-Z_TQK.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
| Version | 1.0.0 |
|
|
15
15
|
| Dependencies | ng-core, ng-shared, ng-layout (IMenuItem only) |
|
|
16
16
|
| NO dependency on | ng-auth (fully independent via Provider Pattern) |
|
|
17
|
+
| CSS Framework | Tailwind CSS (not PrimeFlex) |
|
|
17
18
|
| Build | `npm run build:ng-iam` |
|
|
18
19
|
| Entry point | `public-api.ts` |
|
|
19
20
|
|
|
@@ -64,21 +65,34 @@ ng-iam requires these provider interfaces:
|
|
|
64
65
|
|
|
65
66
|
### How Components Use Providers
|
|
66
67
|
|
|
68
|
+
User selection uses `lib-user-select` component from ng-shared (encapsulates USER_PROVIDER):
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<!-- UserActionSelectorComponent / UserRoleSelectorComponent -->
|
|
72
|
+
<lib-user-select
|
|
73
|
+
[(value)]="selectedUserId"
|
|
74
|
+
[isEditMode]="true"
|
|
75
|
+
placeHolder="Select a user"
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Company context is accessed via `LAYOUT_AUTH_STATE`:
|
|
80
|
+
|
|
67
81
|
```typescript
|
|
68
|
-
// UserActionSelectorComponent / UserRoleSelectorComponent
|
|
69
|
-
private readonly userProvider = inject(USER_PROVIDER);
|
|
70
82
|
private readonly companyContext = inject(LAYOUT_AUTH_STATE);
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
// Get current company
|
|
85
|
+
const companyId = this.companyContext.currentCompanyInfo()?.id;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Branch permissions loaded from `USER_PERMISSION_PROVIDER`:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
private readonly userPermissionProvider = inject(USER_PERMISSION_PROVIDER);
|
|
92
|
+
|
|
93
|
+
const response = await this.userPermissionProvider
|
|
94
|
+
.getUserBranchPermissions(userId)
|
|
95
|
+
.toPromise();
|
|
82
96
|
```
|
|
83
97
|
|
|
84
98
|
---
|
|
@@ -363,12 +377,14 @@ Assigns actions directly to users (DIRECT/FULL mode).
|
|
|
363
377
|
| Standalone | Yes |
|
|
364
378
|
| Change Detection | OnPush |
|
|
365
379
|
|
|
366
|
-
**Dependencies:** `APP_CONFIG`, `LAYOUT_AUTH_STATE`, `
|
|
380
|
+
**Dependencies:** `APP_CONFIG`, `LAYOUT_AUTH_STATE`, `USER_PERMISSION_PROVIDER`, `ActionApiService`, `ActionPermissionLogicService`, `PermissionApiService`, `MessageService`
|
|
367
381
|
|
|
368
382
|
**Features:**
|
|
369
|
-
- User dropdown (
|
|
383
|
+
- User dropdown (`lib-user-select` component from ng-shared)
|
|
370
384
|
- Branch selector (conditional, loaded from USER_PERMISSION_PROVIDER)
|
|
371
385
|
- Hierarchical TreeTable with checkboxes
|
|
386
|
+
- Responsive table with `overflow-x-auto` container
|
|
387
|
+
- Responsive columns (Code hidden on mobile, Description hidden until lg)
|
|
372
388
|
- Action type tags (backend/frontend/both)
|
|
373
389
|
- Prerequisite validation with tooltips
|
|
374
390
|
- Change tracking with pending add/remove summary
|
|
@@ -387,11 +403,13 @@ Assigns roles to users (RBAC/FULL mode).
|
|
|
387
403
|
| Standalone | Yes |
|
|
388
404
|
| Change Detection | OnPush |
|
|
389
405
|
|
|
390
|
-
**Dependencies:** `APP_CONFIG`, `LAYOUT_AUTH_STATE`, `
|
|
406
|
+
**Dependencies:** `APP_CONFIG`, `LAYOUT_AUTH_STATE`, `USER_PERMISSION_PROVIDER`, `RoleApiService`, `PermissionApiService`, `MessageService`
|
|
391
407
|
|
|
392
408
|
**Features:**
|
|
393
|
-
- User dropdown
|
|
394
|
-
-
|
|
409
|
+
- User dropdown (`lib-user-select` component from ng-shared)
|
|
410
|
+
- Branch selector (conditional)
|
|
411
|
+
- DataTable with pagination and `overflow-x-auto` container
|
|
412
|
+
- Responsive columns (Code hidden on mobile, Description hidden until md)
|
|
395
413
|
- Change tracking with pending add/remove summary
|
|
396
414
|
- Select/deselect all
|
|
397
415
|
- No prerequisite validation (roles are simple assignments)
|
|
@@ -411,12 +429,13 @@ Assigns actions to roles (RBAC/FULL mode).
|
|
|
411
429
|
**Dependencies:** `RoleApiService`, `ActionApiService`, `PermissionApiService`, `MessageService`, `ActionPermissionLogicService`
|
|
412
430
|
|
|
413
431
|
**Features:**
|
|
414
|
-
- Role dropdown
|
|
415
|
-
- Hierarchical TreeTable with checkboxes
|
|
432
|
+
- Role dropdown with filter
|
|
433
|
+
- Hierarchical TreeTable with checkboxes and `overflow-x-auto` container
|
|
434
|
+
- Responsive columns (Code hidden until md, Requirements hidden until lg)
|
|
416
435
|
- Prerequisite validation with smart dependency handling
|
|
417
436
|
- Request cancellation with AbortController
|
|
418
437
|
- Pre-save validation with auto-fix dialog
|
|
419
|
-
- Change tracking
|
|
438
|
+
- Change tracking with pending add/remove summary
|
|
420
439
|
|
|
421
440
|
---
|
|
422
441
|
|
|
@@ -433,11 +452,14 @@ Manages company action whitelisting (when company feature enabled).
|
|
|
433
452
|
**Dependencies:** `COMPANY_API_PROVIDER`, `ActionApiService`, `PermissionApiService`, `MessageService`, `ConfirmationService`, `ActionPermissionLogicService`
|
|
434
453
|
|
|
435
454
|
**Features:**
|
|
436
|
-
- Company dropdown (loaded from COMPANY_API_PROVIDER)
|
|
455
|
+
- Company dropdown with filter (loaded from COMPANY_API_PROVIDER)
|
|
437
456
|
- Full action tree (not filtered by company whitelist)
|
|
438
|
-
-
|
|
457
|
+
- Hierarchical TreeTable with `overflow-x-auto` container
|
|
458
|
+
- Responsive columns (Code hidden until md, Description hidden until lg)
|
|
459
|
+
- Prerequisite validation with tooltips
|
|
439
460
|
- Backend prerequisite error handling with auto-fix
|
|
440
461
|
- Request cancellation with AbortController
|
|
462
|
+
- Change tracking with pending whitelist/remove summary
|
|
441
463
|
|
|
442
464
|
---
|
|
443
465
|
|
|
@@ -453,8 +475,34 @@ Tabbed navigation container for IAM pages.
|
|
|
453
475
|
|----------|-------|
|
|
454
476
|
| Selector | `lib-iam-container` |
|
|
455
477
|
| Tabs | Actions (always), Roles (RBAC/FULL), Permissions (always) |
|
|
478
|
+
| Change Detection | OnPush |
|
|
479
|
+
|
|
480
|
+
**Features:**
|
|
481
|
+
- Responsive page title (`text-xl sm:text-2xl`)
|
|
482
|
+
- Horizontally scrollable tabs on mobile (hidden scrollbar)
|
|
483
|
+
- Uses `RouterLink` and `RouterLinkActive` for navigation
|
|
484
|
+
- Tab visibility based on permission mode
|
|
456
485
|
|
|
457
|
-
|
|
486
|
+
**Responsive Patterns:**
|
|
487
|
+
|
|
488
|
+
```html
|
|
489
|
+
<!-- Page title -->
|
|
490
|
+
<h1 class="text-xl sm:text-2xl font-bold m-0">Identity & Access Management</h1>
|
|
491
|
+
|
|
492
|
+
<!-- Horizontally scrollable tabs (hidden scrollbar) -->
|
|
493
|
+
<div class="scrollbar-hide flex gap-1 overflow-x-auto flex-nowrap">
|
|
494
|
+
<!-- tabs -->
|
|
495
|
+
</div>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Scrollbar hide CSS:**
|
|
499
|
+
```css
|
|
500
|
+
.scrollbar-hide {
|
|
501
|
+
-ms-overflow-style: none;
|
|
502
|
+
scrollbar-width: none;
|
|
503
|
+
&::-webkit-scrollbar { display: none; }
|
|
504
|
+
}
|
|
505
|
+
```
|
|
458
506
|
|
|
459
507
|
---
|
|
460
508
|
|
|
@@ -467,6 +515,11 @@ Hierarchical action list with CRUD operations.
|
|
|
467
515
|
| Selector | `lib-action-list-page` |
|
|
468
516
|
| Features | TreeTable display, create/edit/delete, read-only indicators, company context awareness |
|
|
469
517
|
|
|
518
|
+
**Responsive features:**
|
|
519
|
+
- TreeTable with `overflow-x-auto -mx-4 sm:mx-0` container
|
|
520
|
+
- Responsive columns: Code (hidden until md), Active (hidden until sm), Read Only (hidden until lg)
|
|
521
|
+
- Responsive header with stacked layout on mobile
|
|
522
|
+
|
|
470
523
|
Reloads actions when company context changes via effect.
|
|
471
524
|
|
|
472
525
|
---
|
|
@@ -493,6 +546,11 @@ Paginated role list with CRUD operations.
|
|
|
493
546
|
| Selector | `lib-role-list-page` |
|
|
494
547
|
| Features | Paginated table (10/25/50), create/edit/delete, company context filtering, read-only indicators |
|
|
495
548
|
|
|
549
|
+
**Responsive features:**
|
|
550
|
+
- Table with `overflow-x-auto -mx-4 sm:mx-0` container
|
|
551
|
+
- Responsive columns: Description (hidden until md), Read Only (hidden until sm)
|
|
552
|
+
- Conditional paginator (only shown when data exists)
|
|
553
|
+
|
|
496
554
|
---
|
|
497
555
|
|
|
498
556
|
### RoleFormPageComponent
|
|
@@ -883,6 +941,96 @@ GET /iam/permissions/company-actions/:id # Get company's whitelisted action
|
|
|
883
941
|
|
|
884
942
|
---
|
|
885
943
|
|
|
944
|
+
## Responsive Table Patterns
|
|
945
|
+
|
|
946
|
+
All IAM tables and tree tables use consistent responsive patterns with Tailwind CSS.
|
|
947
|
+
|
|
948
|
+
### Table Container Pattern
|
|
949
|
+
|
|
950
|
+
```html
|
|
951
|
+
<!-- Wrapper enables horizontal scroll on mobile, full-width on desktop -->
|
|
952
|
+
<div class="overflow-x-auto -mx-4 sm:mx-0">
|
|
953
|
+
<p-treeTable
|
|
954
|
+
[value]="treeNodes()"
|
|
955
|
+
dataKey="id"
|
|
956
|
+
styleClass="p-treetable-sm"
|
|
957
|
+
[tableStyle]="{ 'min-width': '50rem' }"
|
|
958
|
+
>
|
|
959
|
+
<!-- ... -->
|
|
960
|
+
</p-treeTable>
|
|
961
|
+
</div>
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**Key properties:**
|
|
965
|
+
- `overflow-x-auto` - Enables horizontal scroll when content exceeds container
|
|
966
|
+
- `-mx-4 sm:mx-0` - Edge-to-edge on mobile, normal margins on desktop
|
|
967
|
+
- `[tableStyle]="{ 'min-width': '50rem' }"` - Minimum width ensures consistent layout
|
|
968
|
+
|
|
969
|
+
### Responsive Column Visibility
|
|
970
|
+
|
|
971
|
+
```html
|
|
972
|
+
<ng-template #header>
|
|
973
|
+
<tr>
|
|
974
|
+
<th class="w-12"><!-- checkbox --></th>
|
|
975
|
+
<th>Name</th> <!-- Always visible -->
|
|
976
|
+
<th class="hidden sm:table-cell">Code</th> <!-- Hidden on xs -->
|
|
977
|
+
<th>Type</th> <!-- Always visible -->
|
|
978
|
+
<th class="hidden md:table-cell">Status</th> <!-- Hidden until md -->
|
|
979
|
+
<th class="hidden lg:table-cell">Description</th> <!-- Hidden until lg -->
|
|
980
|
+
</tr>
|
|
981
|
+
</ng-template>
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
**Breakpoint classes:**
|
|
985
|
+
| Class | Visibility |
|
|
986
|
+
|-------|------------|
|
|
987
|
+
| (none) | Always visible |
|
|
988
|
+
| `hidden sm:table-cell` | Visible at 640px+ |
|
|
989
|
+
| `hidden md:table-cell` | Visible at 768px+ |
|
|
990
|
+
| `hidden lg:table-cell` | Visible at 1024px+ |
|
|
991
|
+
|
|
992
|
+
### Header Layout Pattern
|
|
993
|
+
|
|
994
|
+
```html
|
|
995
|
+
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-3 mb-4">
|
|
996
|
+
<div>
|
|
997
|
+
<h5 class="m-0 mb-1">Section Title</h5>
|
|
998
|
+
<p class="text-sm text-muted-color m-0">{{ count }} items available</p>
|
|
999
|
+
</div>
|
|
1000
|
+
<div class="flex flex-wrap gap-2">
|
|
1001
|
+
<!-- Action buttons -->
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
### Dark Mode Styling
|
|
1007
|
+
|
|
1008
|
+
```css
|
|
1009
|
+
/* Light mode */
|
|
1010
|
+
.validation-warning {
|
|
1011
|
+
background-color: var(--p-yellow-50, #fefce8);
|
|
1012
|
+
border-left: 4px solid var(--p-yellow-500, #eab308);
|
|
1013
|
+
color: var(--p-yellow-700, #a16207);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/* Dark mode */
|
|
1017
|
+
:host-context(.p-dark) .validation-warning {
|
|
1018
|
+
background-color: rgba(234, 179, 8, 0.1);
|
|
1019
|
+
color: var(--p-yellow-400, #facc15);
|
|
1020
|
+
}
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
**Color classes (Tailwind):**
|
|
1024
|
+
| Purpose | Class |
|
|
1025
|
+
|---------|-------|
|
|
1026
|
+
| Muted text | `text-muted-color` |
|
|
1027
|
+
| Success | `text-green-500` |
|
|
1028
|
+
| Danger | `text-red-500` |
|
|
1029
|
+
| Warning | `text-orange-500` |
|
|
1030
|
+
| Primary | `text-primary` |
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
886
1034
|
## Best Practices
|
|
887
1035
|
|
|
888
1036
|
### Permission Loading
|
|
@@ -986,6 +1134,9 @@ const logic: ILogicNode = {
|
|
|
986
1134
|
| 401 on IAM endpoints with external auth | Auth token not forwarded | Ensure your auth interceptor adds `Authorization` header to IAM requests |
|
|
987
1135
|
| Provider not configured error | Missing DI registration | Add required providers to `app.config.ts` (USER_PROVIDER, LAYOUT_AUTH_STATE) |
|
|
988
1136
|
| Empty user/company dropdowns | API response format mismatch | Ensure adapters return `IListResponse<T>` format with `data` array |
|
|
1137
|
+
| TreeTable has excessive whitespace | Using `scrollHeight="flex"` without fixed-height parent | Remove `scrollHeight="flex"` and `[scrollable]="true"`, use `overflow-x-auto` container instead |
|
|
1138
|
+
| Table not horizontally scrollable on mobile | Missing responsive wrapper | Add `<div class="overflow-x-auto -mx-4 sm:mx-0">` around table |
|
|
1139
|
+
| Columns visible on mobile causing overflow | Missing responsive column classes | Add `hidden sm:table-cell` or `hidden md:table-cell` to non-essential columns |
|
|
989
1140
|
|
|
990
1141
|
---
|
|
991
1142
|
|
|
@@ -1074,5 +1225,5 @@ if (this.permissionProvider) {
|
|
|
1074
1225
|
|
|
1075
1226
|
---
|
|
1076
1227
|
|
|
1077
|
-
**Last Updated:** 2026-02-
|
|
1228
|
+
**Last Updated:** 2026-02-21
|
|
1078
1229
|
**Angular Version:** 21
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, signal, computed, effect, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { form, required, FormField } from '@angular/forms/signals';
|
|
5
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
6
|
+
import { AngularModule, PrimeModule } from '@flusys/ng-shared';
|
|
7
|
+
import { MessageService } from 'primeng/api';
|
|
8
|
+
import { firstValueFrom } from 'rxjs';
|
|
9
|
+
import { A as ActionApiService, a as ActionType, L as LogicBuilderComponent } from './flusys-ng-iam-flusys-ng-iam-DrGHlTiz.mjs';
|
|
10
|
+
import * as i1 from '@angular/forms';
|
|
11
|
+
import * as i2 from 'primeng/button';
|
|
12
|
+
import * as i3 from 'primeng/checkbox';
|
|
13
|
+
import * as i4 from 'primeng/inputtext';
|
|
14
|
+
|
|
15
|
+
class ActionFormPageComponent {
|
|
16
|
+
route = inject(ActivatedRoute);
|
|
17
|
+
router = inject(Router);
|
|
18
|
+
actionApi = inject(ActionApiService);
|
|
19
|
+
messageService = inject(MessageService);
|
|
20
|
+
routeParams = toSignal(this.route.paramMap);
|
|
21
|
+
initialized = false;
|
|
22
|
+
isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
|
|
23
|
+
existingAction = signal(null, ...(ngDevMode ? [{ debugName: "existingAction" }] : []));
|
|
24
|
+
isEditMode = computed(() => !!this.existingAction(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
|
|
25
|
+
allActionsForLogic = signal([], ...(ngDevMode ? [{ debugName: "allActionsForLogic" }] : []));
|
|
26
|
+
allActions = signal([], ...(ngDevMode ? [{ debugName: "allActions" }] : []));
|
|
27
|
+
availableActions = computed(() => {
|
|
28
|
+
const actions = this.allActions();
|
|
29
|
+
const currentId = this.existingAction()?.id;
|
|
30
|
+
return currentId ? actions.filter((a) => a.id !== currentId) : actions;
|
|
31
|
+
}, ...(ngDevMode ? [{ debugName: "availableActions" }] : []));
|
|
32
|
+
formModel = signal({
|
|
33
|
+
id: '',
|
|
34
|
+
name: '',
|
|
35
|
+
description: '',
|
|
36
|
+
code: '',
|
|
37
|
+
actionType: ActionType.BACKEND,
|
|
38
|
+
permissionLogic: null,
|
|
39
|
+
parentId: '',
|
|
40
|
+
serial: '',
|
|
41
|
+
isActive: true,
|
|
42
|
+
metadata: null,
|
|
43
|
+
}, ...(ngDevMode ? [{ debugName: "formModel" }] : []));
|
|
44
|
+
actionTypes = [
|
|
45
|
+
{ label: 'Backend (API Endpoints)', value: ActionType.BACKEND },
|
|
46
|
+
{ label: 'Frontend (UI Features)', value: ActionType.FRONTEND },
|
|
47
|
+
{ label: 'Both (Backend + Frontend)', value: ActionType.BOTH },
|
|
48
|
+
];
|
|
49
|
+
actionForm = form(this.formModel, (f) => {
|
|
50
|
+
required(f.name, { message: 'Name is required' });
|
|
51
|
+
});
|
|
52
|
+
isFormValid = computed(() => {
|
|
53
|
+
const model = this.formModel();
|
|
54
|
+
return model.name.trim().length > 0;
|
|
55
|
+
}, ...(ngDevMode ? [{ debugName: "isFormValid" }] : []));
|
|
56
|
+
constructor() {
|
|
57
|
+
effect(() => {
|
|
58
|
+
const params = this.routeParams();
|
|
59
|
+
if (!params || this.initialized)
|
|
60
|
+
return;
|
|
61
|
+
this.initialized = true;
|
|
62
|
+
this.initializeForm(params.get('id'));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async initializeForm(id) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await firstValueFrom(this.actionApi.getAll('', {
|
|
68
|
+
pagination: { currentPage: 0, pageSize: 10000 },
|
|
69
|
+
select: ['id', 'name', 'code', 'actionType', 'permissionLogic'],
|
|
70
|
+
}));
|
|
71
|
+
if (response?.success && response.data) {
|
|
72
|
+
this.allActionsForLogic.set(response.data.map((a) => ({ id: a.id, name: a.name ?? 'Unnamed' })));
|
|
73
|
+
this.allActions.set(response.data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignored - form will show empty parent dropdown
|
|
78
|
+
}
|
|
79
|
+
if (id && id !== 'new') {
|
|
80
|
+
await this.loadAction(id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async loadAction(id) {
|
|
84
|
+
this.isLoading.set(true);
|
|
85
|
+
try {
|
|
86
|
+
const response = await this.actionApi.findByIdAsync(id, [
|
|
87
|
+
'id', 'name', 'description', 'code', 'actionType',
|
|
88
|
+
'permissionLogic', 'parentId', 'serial', 'isActive', 'metadata',
|
|
89
|
+
]);
|
|
90
|
+
if (response?.success && response.data) {
|
|
91
|
+
const action = response.data;
|
|
92
|
+
this.existingAction.set(action);
|
|
93
|
+
this.formModel.set({
|
|
94
|
+
id: action.id ?? '',
|
|
95
|
+
name: action.name ?? '',
|
|
96
|
+
description: action.description ?? '',
|
|
97
|
+
code: action.code ?? '',
|
|
98
|
+
actionType: action.actionType ?? ActionType.BACKEND,
|
|
99
|
+
permissionLogic: action.permissionLogic ?? null,
|
|
100
|
+
parentId: action.parentId ?? '',
|
|
101
|
+
serial: action.serial?.toString() ?? '',
|
|
102
|
+
isActive: action.isActive ?? true,
|
|
103
|
+
metadata: action.metadata ?? null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.router.navigate(['/iam/actions']);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
this.router.navigate(['/iam/actions']);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.isLoading.set(false);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async onSubmit() {
|
|
118
|
+
if (!this.isFormValid()) {
|
|
119
|
+
this.messageService.add({
|
|
120
|
+
severity: 'error',
|
|
121
|
+
summary: 'Validation Error',
|
|
122
|
+
detail: 'Please fill in all required fields.',
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.isLoading.set(true);
|
|
127
|
+
try {
|
|
128
|
+
const formValue = this.formModel();
|
|
129
|
+
const dto = {
|
|
130
|
+
...formValue,
|
|
131
|
+
description: formValue.description || undefined,
|
|
132
|
+
code: formValue.code || undefined,
|
|
133
|
+
parentId: formValue.parentId || undefined,
|
|
134
|
+
serial: formValue.serial ? parseInt(formValue.serial, 10) : undefined,
|
|
135
|
+
metadata: formValue.metadata ?? undefined,
|
|
136
|
+
permissionLogic: formValue.permissionLogic ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
if (this.isEditMode()) {
|
|
139
|
+
await this.actionApi.updateAsync(dto);
|
|
140
|
+
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Action updated successfully.' });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await this.actionApi.insertAsync(dto);
|
|
144
|
+
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Action created successfully.' });
|
|
145
|
+
}
|
|
146
|
+
this.router.navigate(['/iam/actions']);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Handled by global interceptor
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.isLoading.set(false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
onBack() {
|
|
156
|
+
this.router.navigate(['/iam/actions']);
|
|
157
|
+
}
|
|
158
|
+
onLogicChange(logic) {
|
|
159
|
+
this.formModel.update((model) => ({ ...model, permissionLogic: logic }));
|
|
160
|
+
}
|
|
161
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionFormPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
162
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ActionFormPageComponent, isStandalone: true, selector: "lib-action-form-page", ngImport: i0, template: `
|
|
163
|
+
<div class="card">
|
|
164
|
+
<h3 class="text-lg sm:text-xl font-semibold mb-4">
|
|
165
|
+
{{ isEditMode() ? 'Edit Action' : 'New Action' }}
|
|
166
|
+
</h3>
|
|
167
|
+
|
|
168
|
+
<form (ngSubmit)="onSubmit()" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
169
|
+
<!-- Name -->
|
|
170
|
+
<div class="flex flex-col gap-2">
|
|
171
|
+
<label for="name" class="font-medium">Name *</label>
|
|
172
|
+
<input
|
|
173
|
+
pInputText
|
|
174
|
+
id="name"
|
|
175
|
+
[formField]="actionForm.name"
|
|
176
|
+
placeholder="Enter action name" />
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Code -->
|
|
180
|
+
<div class="flex flex-col gap-2">
|
|
181
|
+
<label for="code" class="font-medium">Code</label>
|
|
182
|
+
<input
|
|
183
|
+
pInputText
|
|
184
|
+
id="code"
|
|
185
|
+
[formField]="actionForm.code"
|
|
186
|
+
placeholder="Enter action code" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Description -->
|
|
190
|
+
<div class="flex flex-col gap-2">
|
|
191
|
+
<label for="description" class="font-medium">Description</label>
|
|
192
|
+
<input
|
|
193
|
+
pInputText
|
|
194
|
+
id="description"
|
|
195
|
+
[formField]="actionForm.description"
|
|
196
|
+
placeholder="Enter description" />
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- Action Type -->
|
|
200
|
+
<div class="flex flex-col gap-2">
|
|
201
|
+
<label for="actionType" class="font-medium">Action Type *</label>
|
|
202
|
+
<select
|
|
203
|
+
id="actionType"
|
|
204
|
+
class="p-inputtext w-full"
|
|
205
|
+
[formField]="actionForm.actionType">
|
|
206
|
+
@for (type of actionTypes; track type.value) {
|
|
207
|
+
<option [value]="type.value">{{ type.label }}</option>
|
|
208
|
+
}
|
|
209
|
+
</select>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Parent Action -->
|
|
213
|
+
<div class="flex flex-col gap-2">
|
|
214
|
+
<label for="parentId" class="font-medium">Parent Action</label>
|
|
215
|
+
<select
|
|
216
|
+
id="parentId"
|
|
217
|
+
class="p-inputtext w-full"
|
|
218
|
+
[formField]="actionForm.parentId">
|
|
219
|
+
<option value="">Select parent action</option>
|
|
220
|
+
@for (action of availableActions(); track action.id) {
|
|
221
|
+
<option [value]="action.id">{{ action.name }}</option>
|
|
222
|
+
}
|
|
223
|
+
</select>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Order -->
|
|
227
|
+
<div class="flex flex-col gap-2">
|
|
228
|
+
<label for="serial" class="font-medium">Display Order</label>
|
|
229
|
+
<input
|
|
230
|
+
pInputText
|
|
231
|
+
id="serial"
|
|
232
|
+
type="number"
|
|
233
|
+
[formField]="actionForm.serial"
|
|
234
|
+
placeholder="Enter display order" />
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- Is Active -->
|
|
238
|
+
<div class="flex items-end gap-2 pb-1 md:col-span-2">
|
|
239
|
+
<p-checkbox
|
|
240
|
+
[formField]="actionForm.isActive"
|
|
241
|
+
[binary]="true"
|
|
242
|
+
inputId="isActive" />
|
|
243
|
+
<label for="isActive">Active</label>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<!-- Permission Logic Builder -->
|
|
247
|
+
<div class="md:col-span-2">
|
|
248
|
+
<lib-logic-builder
|
|
249
|
+
[logic]="formModel().permissionLogic"
|
|
250
|
+
[actions]="allActionsForLogic()"
|
|
251
|
+
(logicChange)="onLogicChange($event)" />
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<!-- Actions -->
|
|
255
|
+
<div class="flex justify-end gap-2 md:col-span-2 pt-4">
|
|
256
|
+
<p-button
|
|
257
|
+
label="Cancel"
|
|
258
|
+
severity="secondary"
|
|
259
|
+
[outlined]="true"
|
|
260
|
+
(onClick)="onBack()" />
|
|
261
|
+
<p-button
|
|
262
|
+
[label]="isEditMode() ? 'Update' : 'Create'"
|
|
263
|
+
type="submit"
|
|
264
|
+
[loading]="isLoading()"
|
|
265
|
+
[disabled]="!isFormValid() || isLoading()" />
|
|
266
|
+
</div>
|
|
267
|
+
</form>
|
|
268
|
+
</div>
|
|
269
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i3.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "component", type: LogicBuilderComponent, selector: "lib-logic-builder", inputs: ["logic", "actions"], outputs: ["logicChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
270
|
+
}
|
|
271
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ActionFormPageComponent, decorators: [{
|
|
272
|
+
type: Component,
|
|
273
|
+
args: [{
|
|
274
|
+
selector: 'lib-action-form-page',
|
|
275
|
+
imports: [AngularModule, PrimeModule, FormField, LogicBuilderComponent],
|
|
276
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
277
|
+
template: `
|
|
278
|
+
<div class="card">
|
|
279
|
+
<h3 class="text-lg sm:text-xl font-semibold mb-4">
|
|
280
|
+
{{ isEditMode() ? 'Edit Action' : 'New Action' }}
|
|
281
|
+
</h3>
|
|
282
|
+
|
|
283
|
+
<form (ngSubmit)="onSubmit()" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
284
|
+
<!-- Name -->
|
|
285
|
+
<div class="flex flex-col gap-2">
|
|
286
|
+
<label for="name" class="font-medium">Name *</label>
|
|
287
|
+
<input
|
|
288
|
+
pInputText
|
|
289
|
+
id="name"
|
|
290
|
+
[formField]="actionForm.name"
|
|
291
|
+
placeholder="Enter action name" />
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<!-- Code -->
|
|
295
|
+
<div class="flex flex-col gap-2">
|
|
296
|
+
<label for="code" class="font-medium">Code</label>
|
|
297
|
+
<input
|
|
298
|
+
pInputText
|
|
299
|
+
id="code"
|
|
300
|
+
[formField]="actionForm.code"
|
|
301
|
+
placeholder="Enter action code" />
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<!-- Description -->
|
|
305
|
+
<div class="flex flex-col gap-2">
|
|
306
|
+
<label for="description" class="font-medium">Description</label>
|
|
307
|
+
<input
|
|
308
|
+
pInputText
|
|
309
|
+
id="description"
|
|
310
|
+
[formField]="actionForm.description"
|
|
311
|
+
placeholder="Enter description" />
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<!-- Action Type -->
|
|
315
|
+
<div class="flex flex-col gap-2">
|
|
316
|
+
<label for="actionType" class="font-medium">Action Type *</label>
|
|
317
|
+
<select
|
|
318
|
+
id="actionType"
|
|
319
|
+
class="p-inputtext w-full"
|
|
320
|
+
[formField]="actionForm.actionType">
|
|
321
|
+
@for (type of actionTypes; track type.value) {
|
|
322
|
+
<option [value]="type.value">{{ type.label }}</option>
|
|
323
|
+
}
|
|
324
|
+
</select>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<!-- Parent Action -->
|
|
328
|
+
<div class="flex flex-col gap-2">
|
|
329
|
+
<label for="parentId" class="font-medium">Parent Action</label>
|
|
330
|
+
<select
|
|
331
|
+
id="parentId"
|
|
332
|
+
class="p-inputtext w-full"
|
|
333
|
+
[formField]="actionForm.parentId">
|
|
334
|
+
<option value="">Select parent action</option>
|
|
335
|
+
@for (action of availableActions(); track action.id) {
|
|
336
|
+
<option [value]="action.id">{{ action.name }}</option>
|
|
337
|
+
}
|
|
338
|
+
</select>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<!-- Order -->
|
|
342
|
+
<div class="flex flex-col gap-2">
|
|
343
|
+
<label for="serial" class="font-medium">Display Order</label>
|
|
344
|
+
<input
|
|
345
|
+
pInputText
|
|
346
|
+
id="serial"
|
|
347
|
+
type="number"
|
|
348
|
+
[formField]="actionForm.serial"
|
|
349
|
+
placeholder="Enter display order" />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<!-- Is Active -->
|
|
353
|
+
<div class="flex items-end gap-2 pb-1 md:col-span-2">
|
|
354
|
+
<p-checkbox
|
|
355
|
+
[formField]="actionForm.isActive"
|
|
356
|
+
[binary]="true"
|
|
357
|
+
inputId="isActive" />
|
|
358
|
+
<label for="isActive">Active</label>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Permission Logic Builder -->
|
|
362
|
+
<div class="md:col-span-2">
|
|
363
|
+
<lib-logic-builder
|
|
364
|
+
[logic]="formModel().permissionLogic"
|
|
365
|
+
[actions]="allActionsForLogic()"
|
|
366
|
+
(logicChange)="onLogicChange($event)" />
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<!-- Actions -->
|
|
370
|
+
<div class="flex justify-end gap-2 md:col-span-2 pt-4">
|
|
371
|
+
<p-button
|
|
372
|
+
label="Cancel"
|
|
373
|
+
severity="secondary"
|
|
374
|
+
[outlined]="true"
|
|
375
|
+
(onClick)="onBack()" />
|
|
376
|
+
<p-button
|
|
377
|
+
[label]="isEditMode() ? 'Update' : 'Create'"
|
|
378
|
+
type="submit"
|
|
379
|
+
[loading]="isLoading()"
|
|
380
|
+
[disabled]="!isFormValid() || isLoading()" />
|
|
381
|
+
</div>
|
|
382
|
+
</form>
|
|
383
|
+
</div>
|
|
384
|
+
`,
|
|
385
|
+
}]
|
|
386
|
+
}], ctorParameters: () => [] });
|
|
387
|
+
|
|
388
|
+
export { ActionFormPageComponent };
|
|
389
|
+
//# sourceMappingURL=flusys-ng-iam-action-form-page.component-CVN8sV-c.mjs.map
|