@flusys/ng-shared 3.0.0-rc → 3.0.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 CHANGED
@@ -9,6 +9,7 @@
9
9
  ## Package Information
10
10
 
11
11
  - **Package:** `@flusys/ng-shared`
12
+ - **Version:** 3.0.1
12
13
  - **Dependencies:** ng-core
13
14
  - **Dependents:** ng-layout, ng-auth, ng-iam, ng-storage, flusysng
14
15
  - **Build Command:** `npm run build:ng-shared`
@@ -37,13 +38,13 @@ interface IBaseEntity {
37
38
 
38
39
  **Entity Mixins:**
39
40
 
40
- | Interface | Fields | Purpose |
41
- | --------------- | ---------------------------------- | ---------------------- |
42
- | `ISoftDeletable`| `deletedAt?: Date \| null` | Soft delete support |
43
- | `ITimestampable`| `createdAt, updatedAt` | Timestamp tracking |
44
- | `IActivatable` | `isActive: boolean` | Active status toggle |
45
- | `IOrderable` | `serial?: number \| null` | Ordering/sorting |
46
- | `IMetadata` | `metadata?: Record<string, unknown>` | JSON metadata field |
41
+ | Interface | Fields | Purpose |
42
+ | ---------------- | ------------------------------------ | -------------------- |
43
+ | `ISoftDeletable` | `deletedAt?: Date \| null` | Soft delete support |
44
+ | `ITimestampable` | `createdAt, updatedAt` | Timestamp tracking |
45
+ | `IActivatable` | `isActive: boolean` | Active status toggle |
46
+ | `IOrderable` | `serial?: number \| null` | Ordering/sorting |
47
+ | `IMetadata` | `metadata?: Record<string, unknown>` | JSON metadata field |
47
48
 
48
49
  #### ILoggedUserInfo
49
50
 
@@ -73,8 +74,14 @@ interface IPagination {
73
74
  currentPage: number;
74
75
  }
75
76
 
76
- interface ISort { [key: string]: 'ASC' | 'DESC' }
77
- interface IFilter { [key: string]: any }
77
+ interface ISort {
78
+ [key: string]: "ASC" | "DESC";
79
+ }
80
+
81
+ // Filter supports primitives and arrays for multi-value filtering
82
+ interface IFilter {
83
+ [key: string]: string | number | boolean | null | undefined | string[] | number[];
84
+ }
78
85
 
79
86
  interface IFilterData {
80
87
  filter?: IFilter;
@@ -86,15 +93,17 @@ interface IFilterData {
86
93
  }
87
94
  ```
88
95
 
96
+ **Note:** `IFilter` supports array values (`string[]`, `number[]`) for multi-value filtering (e.g., filtering by multiple status values or IDs).
97
+
89
98
  #### IDeleteData
90
99
 
91
100
  Delete request payload matching backend DeleteDto.
92
101
 
93
102
  ```typescript
94
- type DeleteType = 'delete' | 'restore' | 'permanent';
103
+ type DeleteType = "delete" | "restore" | "permanent";
95
104
 
96
105
  interface IDeleteData {
97
- id: string | string[]; // Single or batch delete
106
+ id: string | string[]; // Single or batch delete
98
107
  type: DeleteType;
99
108
  }
100
109
  ```
@@ -117,7 +126,6 @@ interface IUserSelectFilter {
117
126
  page: number;
118
127
  pageSize: number;
119
128
  search: string;
120
- [key: string]: unknown;
121
129
  }
122
130
 
123
131
  type LoadUsersFn = (filter: IUserSelectFilter) => Observable<IListResponse<IUserBasicInfo>>;
@@ -144,6 +152,7 @@ interface IFileUploadOptions {
144
152
  }
145
153
 
146
154
  interface IUploadedFile {
155
+ id?: string; // File manager ID (UUID) - available when registered
147
156
  name: string;
148
157
  key: string;
149
158
  size: number;
@@ -156,7 +165,6 @@ interface IFileSelectFilter {
156
165
  search: string;
157
166
  contentTypes?: string[];
158
167
  folderId?: string;
159
- [key: string]: unknown;
160
168
  }
161
169
 
162
170
  type LoadFilesFn = (filter: IFileSelectFilter) => Observable<IListResponse<IFileBasicInfo>>;
@@ -167,18 +175,18 @@ type GetFileUrlsFn = (fileIds: string[]) => Observable<ISingleResponse<IFileBasi
167
175
  **File Type Filters (Constants):**
168
176
 
169
177
  ```typescript
170
- import { FILE_TYPE_FILTERS, getAcceptString, isFileTypeAllowed, getFileIconClass, formatFileSize } from '@flusys/ng-shared';
171
-
172
- FILE_TYPE_FILTERS.IMAGES // ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
173
- FILE_TYPE_FILTERS.DOCUMENTS // ['application/pdf', 'application/msword', ...]
174
- FILE_TYPE_FILTERS.VIDEOS // ['video/mp4', 'video/webm', ...]
175
- FILE_TYPE_FILTERS.AUDIO // ['audio/mpeg', 'audio/wav', ...]
176
- FILE_TYPE_FILTERS.ALL // [] (allows all)
177
-
178
- getAcceptString(['image/*']) // 'image/*'
179
- isFileTypeAllowed(file, ['image/*']) // true/false
180
- getFileIconClass('image/png') // 'pi pi-image'
181
- formatFileSize(1024) // '1 KB'
178
+ import { FILE_TYPE_FILTERS, getAcceptString, isFileTypeAllowed, getFileIconClass, formatFileSize } from "@flusys/ng-shared";
179
+
180
+ FILE_TYPE_FILTERS.IMAGES; // ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
181
+ FILE_TYPE_FILTERS.DOCUMENTS; // ['application/pdf', 'application/msword', ...]
182
+ FILE_TYPE_FILTERS.VIDEOS; // ['video/mp4', 'video/webm', ...]
183
+ FILE_TYPE_FILTERS.AUDIO; // ['audio/mpeg', 'audio/wav', ...]
184
+ FILE_TYPE_FILTERS.ALL; // [] (allows all)
185
+
186
+ getAcceptString(["image/*"]); // 'image/*'
187
+ isFileTypeAllowed(file, ["image/*"]); // true/false
188
+ getFileIconClass("image/png"); // 'pi pi-image'
189
+ formatFileSize(1024); // '1 KB'
182
190
  ```
183
191
 
184
192
  ### Response Interfaces
@@ -234,12 +242,12 @@ type ApiResponse<T> = ISingleResponse<T> | IListResponse<T> | IBulkResponse<T> |
234
242
 
235
243
  **Metadata types:**
236
244
 
237
- | Interface | Fields |
238
- | ------------------ | -------------------------------------------- |
239
- | `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
245
+ | Interface | Fields |
246
+ | ------------------ | ----------------------------------------------------- |
247
+ | `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
240
248
  | `IPaginationMeta` | `total, page, pageSize, count, hasMore?, totalPages?` |
241
- | `IBulkMeta` | `count, failed?, total?` |
242
- | `IValidationError` | `field, message, constraint?` |
249
+ | `IBulkMeta` | `count, failed?, total?` |
250
+ | `IValidationError` | `field, message, constraint?` |
243
251
 
244
252
  ### Auth & Storage Response Types
245
253
 
@@ -267,6 +275,14 @@ interface IFileData {
267
275
  thumbnailUrl?: string;
268
276
  createdAt: Date;
269
277
  }
278
+
279
+ // File URL service response DTO
280
+ interface FilesResponseDto {
281
+ id: string;
282
+ name: string;
283
+ contentType: string;
284
+ url: string | null;
285
+ }
270
286
  ```
271
287
 
272
288
  ### Permission Interfaces
@@ -276,14 +292,14 @@ Discriminated union for building complex permission logic trees.
276
292
  ```typescript
277
293
  // Single permission check
278
294
  interface IActionNode {
279
- type: 'action';
295
+ type: "action";
280
296
  actionId: string;
281
297
  }
282
298
 
283
299
  // Group with AND/OR logic
284
300
  interface IGroupNode {
285
- type: 'group';
286
- operator: 'AND' | 'OR';
301
+ type: "group";
302
+ operator: "AND" | "OR";
287
303
  children: ILogicNode[];
288
304
  }
289
305
 
@@ -310,58 +326,149 @@ enum IconTypeEnum {
310
326
 
311
327
  ---
312
328
 
313
- ## 3. Services
329
+ ## 3. Constants
330
+
331
+ ### Permission Constants
332
+
333
+ Centralized permission codes for type-safe permission checks. Single source of truth to prevent typos.
334
+
335
+ ```typescript
336
+ import { PERMISSIONS, USER_PERMISSIONS, ROLE_PERMISSIONS } from '@flusys/ng-shared';
337
+
338
+ // Use constants instead of strings
339
+ *hasPermission="PERMISSIONS.USER.READ"
340
+
341
+ // Individual permission groups available:
342
+ USER_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
343
+ COMPANY_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
344
+ BRANCH_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
345
+ ACTION_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
346
+ ROLE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
347
+ ROLE_ACTION_PERMISSIONS // { READ, ASSIGN }
348
+ USER_ROLE_PERMISSIONS // { READ, ASSIGN }
349
+ USER_ACTION_PERMISSIONS // { READ, ASSIGN }
350
+ COMPANY_ACTION_PERMISSIONS // { READ, ASSIGN }
351
+ FILE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
352
+ FOLDER_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
353
+ STORAGE_CONFIG_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
354
+ EMAIL_CONFIG_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
355
+ EMAIL_TEMPLATE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
356
+ FORM_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
357
+
358
+ // Aggregated object with all permissions
359
+ PERMISSIONS.USER.READ // 'user.read'
360
+ PERMISSIONS.ROLE.CREATE // 'role.create'
361
+ PERMISSIONS.FILE.DELETE // 'file.delete'
362
+ ```
363
+
364
+ **Type:** `PermissionCode` - Union type of all valid permission code strings.
365
+
366
+ ---
367
+
368
+ ## 4. Services
314
369
 
315
370
  ### ApiResourceService
316
371
 
317
- Signal-based CRUD service using Angular 21 `resource()` API. All endpoints use POST (RPC-style).
372
+ Signal-based CRUD service using Angular 21 `resource()` API with **lazy initialization**. All endpoints use POST (RPC-style).
373
+
374
+ **ServiceName Type:**
375
+
376
+ ```typescript
377
+ type ServiceName = "auth" | "administration" | "iam" | "storage" | "formBuilder" | "email";
378
+ ```
318
379
 
319
380
  **Define a service:**
320
381
 
321
382
  ```typescript
322
- import { Injectable } from '@angular/core';
323
- import { HttpClient } from '@angular/common/http';
324
- import { ApiResourceService } from '@flusys/ng-shared';
383
+ import { Injectable } from "@angular/core";
384
+ import { HttpClient } from "@angular/common/http";
385
+ import { ApiResourceService } from "@flusys/ng-shared";
325
386
 
326
- @Injectable({ providedIn: 'root' })
387
+ @Injectable({ providedIn: "root" })
327
388
  export class UserService extends ApiResourceService<UserDto, IUser> {
328
389
  constructor(http: HttpClient) {
329
- super('users', http); // Base URL: APP_CONFIG.apiBaseUrl + '/users'
390
+ // Option 1: Use global apiBaseUrl (default)
391
+ super("users", http);
392
+ // Base URL: APP_CONFIG.apiBaseUrl + '/users'
393
+
394
+ // Option 2: Use feature-specific service URL (recommended)
395
+ super("users", http, "administration");
396
+ // Base URL: APP_CONFIG.services.administration.baseUrl + '/users'
330
397
  }
331
398
  }
332
399
  ```
333
400
 
401
+ **Constructor Parameters:**
402
+
403
+ | Parameter | Type | Required | Description |
404
+ | --------------- | ------------- | -------- | ------------------------------------------------ |
405
+ | `moduleApiName` | `string` | Yes | API path segment (e.g., 'users', 'file-manager') |
406
+ | `http` | `HttpClient` | Yes | Angular HttpClient instance |
407
+ | `serviceName` | `ServiceName` | No | Feature service name for URL resolution |
408
+
409
+ **Service URL Resolution:**
410
+
411
+ - If `serviceName` is provided, uses `getServiceUrl(config, serviceName)` to resolve the base URL
412
+ - Falls back to `APP_CONFIG.apiBaseUrl` if service not found or `serviceName` not provided
413
+
414
+ **Lazy Initialization:**
415
+
416
+ The list resource is **lazy-initialized** to avoid unnecessary HTTP requests on service construction. The resource is only created when first needed:
417
+
418
+ ```typescript
419
+ // Resource is NOT created until one of these is called:
420
+ userService.fetchList("", { pagination: { currentPage: 0, pageSize: 10 } });
421
+ // OR
422
+ userService.initListResource(); // Explicit initialization
423
+ ```
424
+
425
+ **Internal resource structure:**
426
+
427
+ ```typescript
428
+ private _listResource: ResourceRef<IListResponse<InterfaceT> | undefined> | null = null;
429
+ private _resourceInitialized = false;
430
+ private readonly _resourceInitSignal = signal(false);
431
+
432
+ // Initialize the list resource (called automatically by fetchList)
433
+ initListResource(): void {
434
+ if (this._resourceInitialized) return;
435
+ this._resourceInitialized = true;
436
+ this._resourceInitSignal.set(true);
437
+ // Creates the resource with linkedSignal for data, isLoading, etc.
438
+ }
439
+ ```
440
+
334
441
  **Endpoint mapping:**
335
442
 
336
- | Method | HTTP | Endpoint | Response |
337
- | -------------- | ---- | ------------------------- | ------------------------ |
338
- | `insert(dto)` | POST | `/{resource}/insert` | `ISingleResponse<T>` |
339
- | `insertMany(dtos)` | POST | `/{resource}/insert-many` | `IBulkResponse<T>` |
340
- | `findById(id, select?)` | POST | `/{resource}/get/:id` | `ISingleResponse<T>` |
341
- | `getAll(search, filter)` | POST | `/{resource}/get-all?q=` | `IListResponse<T>` |
342
- | `update(dto)` | POST | `/{resource}/update` | `ISingleResponse<T>` |
343
- | `updateMany(dtos)` | POST | `/{resource}/update-many` | `IBulkResponse<T>` |
344
- | `delete(deleteDto)` | POST | `/{resource}/delete` | `IMessageResponse` |
443
+ | Method | HTTP | Endpoint | Response |
444
+ | ------------------------ | ---- | ------------------------- | -------------------- |
445
+ | `insert(dto)` | POST | `/{resource}/insert` | `ISingleResponse<T>` |
446
+ | `insertMany(dtos)` | POST | `/{resource}/insert-many` | `IBulkResponse<T>` |
447
+ | `findById(id, select?)` | POST | `/{resource}/get/:id` | `ISingleResponse<T>` |
448
+ | `getAll(search, filter)` | POST | `/{resource}/get-all?q=` | `IListResponse<T>` |
449
+ | `update(dto)` | POST | `/{resource}/update` | `ISingleResponse<T>` |
450
+ | `updateMany(dtos)` | POST | `/{resource}/update-many` | `IBulkResponse<T>` |
451
+ | `delete(deleteDto)` | POST | `/{resource}/delete` | `IMessageResponse` |
345
452
 
346
453
  All methods above return `Observable`. Async (Promise) variants are also available: `insertAsync`, `insertManyAsync`, `findByIdAsync`, `updateAsync`, `updateManyAsync`, `deleteAsync`.
347
454
 
348
455
  **Reactive signals:**
349
456
 
350
- | Signal | Type | Description |
351
- | ------------ | ------------------------- | ------------------------ |
352
- | `isLoading` | `Signal<boolean>` | Whether data is loading |
353
- | `data` | `Signal<T[]>` | Current list data |
354
- | `total` | `Signal<number>` | Total item count |
355
- | `pageInfo` | `Signal<IPaginationMeta>` | Pagination metadata |
356
- | `hasMore` | `Signal<boolean>` | More pages available |
357
- | `searchTerm` | `WritableSignal<string>` | Current search term |
457
+ | Signal | Type | Description |
458
+ | ------------ | ----------------------------- | ----------------------- |
459
+ | `isLoading` | `Signal<boolean>` | Whether data is loading |
460
+ | `data` | `Signal<T[]>` | Current list data |
461
+ | `total` | `Signal<number>` | Total item count |
462
+ | `pageInfo` | `Signal<IPaginationMeta>` | Pagination metadata |
463
+ | `hasMore` | `Signal<boolean>` | More pages available |
464
+ | `searchTerm` | `WritableSignal<string>` | Current search term |
358
465
  | `filterData` | `WritableSignal<IFilterData>` | Filter/pagination state |
359
466
 
360
467
  **List management:**
361
468
 
362
469
  ```typescript
363
470
  // Trigger data fetch (updates searchTerm and filterData signals, resource auto-reloads)
364
- userService.fetchList('search term', { pagination: { currentPage: 0, pageSize: 10 } });
471
+ userService.fetchList("search term", { pagination: { currentPage: 0, pageSize: 10 } });
365
472
 
366
473
  // Pagination helpers
367
474
  userService.setPagination({ currentPage: 1, pageSize: 20 });
@@ -424,18 +531,18 @@ export class ProductComponent {
424
531
 
425
532
  **Methods:**
426
533
 
427
- | Method | Returns | Description |
428
- | ----------------------------- | ----------------------------------- | ---------------------------------- |
429
- | `getFileUrl(fileId)` | `string \| null` | Get cached URL (synchronous) |
430
- | `fileUrlSignal(fileId)` | `Signal<string \| null>` | Computed signal from cache |
431
- | `fetchFileUrls(fileIds[])` | `Observable<FilesResponseDto[]>` | Fetch from backend, updates cache |
432
- | `fetchSingleFileUrl(fileId)` | `Observable<FilesResponseDto \| null>` | Fetch single file |
433
- | `clearCache()` | `void` | Clear all cached URLs |
434
- | `removeFromCache(fileId)` | `void` | Remove specific entry from cache |
534
+ | Method | Returns | Description |
535
+ | ---------------------------- | -------------------------------------- | --------------------------------- |
536
+ | `getFileUrl(fileId)` | `string \| null` | Get cached URL (synchronous) |
537
+ | `fileUrlSignal(fileId)` | `Signal<string \| null>` | Computed signal from cache |
538
+ | `fetchFileUrls(fileIds[])` | `Observable<FilesResponseDto[]>` | Fetch from backend, updates cache |
539
+ | `fetchSingleFileUrl(fileId)` | `Observable<FilesResponseDto \| null>` | Fetch single file |
540
+ | `clearCache()` | `void` | Clear all cached URLs |
541
+ | `removeFromCache(fileId)` | `void` | Remove specific entry from cache |
435
542
 
436
543
  ### PermissionValidatorService
437
544
 
438
- Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM.
545
+ Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM. Supports **wildcard permissions**.
439
546
 
440
547
  ```typescript
441
548
  import { PermissionValidatorService } from '@flusys/ng-shared';
@@ -453,8 +560,8 @@ export class MyComponent {
453
560
  // User has permission
454
561
  }
455
562
 
456
- // Check loaded state
457
- if (this.permissionValidator.isPermissionsLoaded()) {
563
+ // Check loaded state (signal-based)
564
+ if (this.permissionValidator.isLoaded()) {
458
565
  // Permissions have been loaded
459
566
  }
460
567
  }
@@ -463,25 +570,41 @@ export class MyComponent {
463
570
 
464
571
  **Methods:**
465
572
 
466
- | Method | Returns | Description |
467
- | ---------------------------- | --------- | ---------------------------------- |
468
- | `setPermissions(codes[])` | `void` | Replace all permissions |
469
- | `clearPermissions()` | `void` | Clear all permissions |
470
- | `hasPermission(code)` | `boolean` | Check single permission |
471
- | `isPermissionsLoaded()` | `boolean` | Whether permissions have been set |
573
+ | Method | Returns | Description |
574
+ | ------------------------- | --------- | -------------------------------------------- |
575
+ | `setPermissions(codes[])` | `void` | Replace all permissions |
576
+ | `clearPermissions()` | `void` | Clear all permissions |
577
+ | `hasPermission(code)` | `boolean` | Check single permission (supports wildcards) |
578
+ | `isPermissionsLoaded()` | `boolean` | **Deprecated** - Use `isLoaded()` signal |
472
579
 
473
580
  **Signals:**
474
581
 
475
- | Signal | Type | Description |
476
- | ------------- | ------------------ | ------------------------------ |
477
- | `permissions` | `Signal<string[]>` | Readonly current permissions |
582
+ | Signal | Type | Description |
583
+ | ------------- | ------------------ | --------------------------------- |
584
+ | `permissions` | `Signal<string[]>` | Readonly current permissions |
585
+ | `isLoaded` | `Signal<boolean>` | Whether permissions have been set |
586
+
587
+ **Wildcard Support:**
588
+
589
+ The `hasPermission()` method uses the permission evaluator utility which supports wildcards:
590
+
591
+ ```typescript
592
+ // Exact match
593
+ hasPermission("user.read"); // true if permissions include 'user.read'
594
+
595
+ // Global wildcard
596
+ hasPermission("user.read"); // true if permissions include '*'
597
+
598
+ // Module wildcard
599
+ hasPermission("user.read"); // true if permissions include 'user.*'
600
+ ```
478
601
 
479
602
  ### CookieService
480
603
 
481
604
  SSR-aware cookie reading service.
482
605
 
483
606
  ```typescript
484
- import { CookieService } from '@flusys/ng-shared';
607
+ import { CookieService } from "@flusys/ng-shared";
485
608
 
486
609
  const cookies = inject(CookieService).get(); // Returns document.cookie (browser) or request header cookie (server)
487
610
  ```
@@ -491,7 +614,7 @@ const cookies = inject(CookieService).get(); // Returns document.cookie (browser
491
614
  SSR environment detection service.
492
615
 
493
616
  ```typescript
494
- import { PlatformService } from '@flusys/ng-shared';
617
+ import { PlatformService } from "@flusys/ng-shared";
495
618
 
496
619
  const platform = inject(PlatformService);
497
620
  if (!platform.isServer) {
@@ -501,7 +624,7 @@ if (!platform.isServer) {
501
624
 
502
625
  ---
503
626
 
504
- ## 4. Components
627
+ ## 5. Components
505
628
 
506
629
  ### IconComponent
507
630
 
@@ -532,21 +655,7 @@ Single-select dropdown with lazy loading, search, and scroll pagination.
532
655
  ```typescript
533
656
  @Component({
534
657
  imports: [LazySelectComponent],
535
- template: `
536
- <lib-lazy-select
537
- [(value)]="selectedId"
538
- [selectDataList]="items()"
539
- [optionLabel]="'label'"
540
- [optionValue]="'value'"
541
- [isEditMode]="true"
542
- [isLoading]="loading()"
543
- [total]="total()"
544
- [pagination]="pagination()"
545
- [placeHolder]="'Select item'"
546
- (onSearch)="handleSearch($event)"
547
- (onPagination)="handlePagination($event)"
548
- />
549
- `,
658
+ template: ` <lib-lazy-select [(value)]="selectedId" [selectDataList]="items()" [optionLabel]="'label'" [optionValue]="'value'" [isEditMode]="true" [isLoading]="loading()" [total]="total()" [pagination]="pagination()" [placeHolder]="'Select item'" (onSearch)="handleSearch($event)" (onPagination)="handlePagination($event)" /> `,
550
659
  })
551
660
  export class MyComponent {
552
661
  readonly selectedId = signal<string | null>(null);
@@ -559,16 +668,16 @@ export class MyComponent {
559
668
 
560
669
  **Inputs:**
561
670
 
562
- | Input | Type | Description |
563
- | ---------------- | ----------------- | -------------------------------- |
564
- | `selectDataList` | `Array<IDropDown>` | Dropdown options (required) |
565
- | `optionLabel` | `string` | Label field name (required) |
566
- | `optionValue` | `string` | Value field name (required) |
567
- | `isEditMode` | `boolean` | Enable/disable editing (required)|
568
- | `isLoading` | `boolean` | Loading state (required) |
569
- | `total` | `number \| undefined` | Total items for pagination |
570
- | `pagination` | `IPagination` | Current pagination state |
571
- | `placeHolder` | `string` | Placeholder text (default: `'Select Option'`) |
671
+ | Input | Type | Description |
672
+ | ---------------- | --------------------- | --------------------------------------------- |
673
+ | `selectDataList` | `Array<IDropDown>` | Dropdown options (required) |
674
+ | `optionLabel` | `string` | Label field name (required) |
675
+ | `optionValue` | `string` | Value field name (required) |
676
+ | `isEditMode` | `boolean` | Enable/disable editing (required) |
677
+ | `isLoading` | `boolean` | Loading state (required) |
678
+ | `total` | `number \| undefined` | Total items for pagination |
679
+ | `pagination` | `IPagination` | Current pagination state |
680
+ | `placeHolder` | `string` | Placeholder text (default: `'Select Option'`) |
572
681
 
573
682
  **Model:** `value` - Two-way bound selected value (`string | null`)
574
683
 
@@ -585,19 +694,7 @@ Multi-select dropdown with lazy loading, search, select-all, and scroll paginati
585
694
  ```typescript
586
695
  @Component({
587
696
  imports: [LazyMultiSelectComponent],
588
- template: `
589
- <lib-lazy-multi-select
590
- [(value)]="selectedIds"
591
- [selectDataList]="items()"
592
- [isEditMode]="true"
593
- [isLoading]="loading()"
594
- [total]="total()"
595
- [pagination]="pagination()"
596
- [placeHolder]="'Select items'"
597
- (onSearch)="handleSearch($event)"
598
- (onPagination)="handlePagination($event)"
599
- />
600
- `,
697
+ template: ` <lib-lazy-multi-select [(value)]="selectedIds" [selectDataList]="items()" [isEditMode]="true" [isLoading]="loading()" [total]="total()" [pagination]="pagination()" [placeHolder]="'Select items'" (onSearch)="handleSearch($event)" (onPagination)="handlePagination($event)" /> `,
601
698
  })
602
699
  export class MyComponent {
603
700
  readonly selectedIds = signal<string[] | null>(null);
@@ -622,17 +719,10 @@ Single user selection with lazy loading. Uses `USER_PROVIDER` internally or acce
622
719
  imports: [UserSelectComponent],
623
720
  template: `
624
721
  <!-- Simple usage - uses USER_PROVIDER internally -->
625
- <lib-user-select
626
- [(value)]="selectedUserId"
627
- [isEditMode]="true"
628
- />
722
+ <lib-user-select [(value)]="selectedUserId" [isEditMode]="true" />
629
723
 
630
724
  <!-- With custom loadUsers function -->
631
- <lib-user-select
632
- [(value)]="selectedUserId"
633
- [isEditMode]="true"
634
- [loadUsers]="customLoadUsers"
635
- />
725
+ <lib-user-select [(value)]="selectedUserId" [isEditMode]="true" [loadUsers]="customLoadUsers" />
636
726
  `,
637
727
  })
638
728
  export class MyComponent {
@@ -642,14 +732,14 @@ export class MyComponent {
642
732
 
643
733
  **Inputs:**
644
734
 
645
- | Input | Type | Default | Description |
646
- | ----- | ---- | ------- | ----------- |
647
- | `isEditMode` | `boolean` | required | Enable/disable editing |
648
- | `placeHolder` | `string` | `'Select User'` | Placeholder text |
649
- | `filterActive` | `boolean` | `true` | Filter active users only |
650
- | `additionalFilters` | `Record<string, unknown>` | `{}` | Extra filter params |
651
- | `pageSize` | `number` | `20` | Page size for pagination |
652
- | `loadUsers` | `LoadUsersFn` | - | Custom user loading function |
735
+ | Input | Type | Default | Description |
736
+ | ------------------- | ------------------------- | --------------- | ---------------------------- |
737
+ | `isEditMode` | `boolean` | required | Enable/disable editing |
738
+ | `placeHolder` | `string` | `'Select User'` | Placeholder text |
739
+ | `filterActive` | `boolean` | `true` | Filter active users only |
740
+ | `additionalFilters` | `Record<string, unknown>` | `{}` | Extra filter params |
741
+ | `pageSize` | `number` | `20` | Page size for pagination |
742
+ | `loadUsers` | `LoadUsersFn` | - | Custom user loading function |
653
743
 
654
744
  **Model:** `value` - Two-way bound selected user ID (`string | null`)
655
745
 
@@ -665,12 +755,7 @@ Multiple user selection with lazy loading. Uses `USER_PROVIDER` internally or ac
665
755
  ```typescript
666
756
  @Component({
667
757
  imports: [UserMultiSelectComponent],
668
- template: `
669
- <lib-user-multi-select
670
- [(value)]="selectedUserIds"
671
- [isEditMode]="true"
672
- />
673
- `,
758
+ template: ` <lib-user-multi-select [(value)]="selectedUserIds" [isEditMode]="true" /> `,
674
759
  })
675
760
  export class MyComponent {
676
761
  readonly selectedUserIds = signal<string[] | null>(null);
@@ -694,44 +779,32 @@ Drag & drop file upload with type filtering. Pass your own `uploadFile` function
694
779
  imports: [FileUploaderComponent],
695
780
  template: `
696
781
  <!-- Single image upload -->
697
- <lib-file-uploader
698
- [uploadFile]="uploadFile"
699
- [acceptTypes]="['image/*']"
700
- [multiple]="false"
701
- (fileUploaded)="onFileUploaded($event)"
702
- />
782
+ <lib-file-uploader [uploadFile]="uploadFile" [acceptTypes]="['image/*']" [multiple]="false" (fileUploaded)="onFileUploaded($event)" />
703
783
 
704
784
  <!-- Multiple document upload -->
705
- <lib-file-uploader
706
- [uploadFile]="uploadFile"
707
- [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
708
- [multiple]="true"
709
- [maxFiles]="5"
710
- (filesUploaded)="onFilesUploaded($event)"
711
- />
785
+ <lib-file-uploader [uploadFile]="uploadFile" [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS" [multiple]="true" [maxFiles]="5" (filesUploaded)="onFilesUploaded($event)" />
712
786
  `,
713
787
  })
714
788
  export class MyComponent {
715
789
  readonly uploadService = inject(UploadService);
716
790
 
717
- readonly uploadFile: UploadFileFn = (file, options) =>
718
- this.uploadService.uploadSingleFile(file, options);
791
+ readonly uploadFile: UploadFileFn = (file, options) => this.uploadService.uploadSingleFile(file, options);
719
792
  }
720
793
  ```
721
794
 
722
795
  **Inputs:**
723
796
 
724
- | Input | Type | Default | Description |
725
- | ----- | ---- | ------- | ----------- |
726
- | `uploadFile` | `UploadFileFn` | required | Upload function |
727
- | `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
728
- | `multiple` | `boolean` | `false` | Allow multiple files |
729
- | `maxFiles` | `number` | `10` | Max files for multiple |
730
- | `maxSizeMb` | `number` | `10` | Max file size in MB |
731
- | `uploadOptions` | `IFileUploadOptions` | `{}` | Upload options |
732
- | `disabled` | `boolean` | `false` | Disable uploader |
733
- | `showPreview` | `boolean` | `true` | Show selected files preview |
734
- | `autoUpload` | `boolean` | `true` | Upload immediately on selection |
797
+ | Input | Type | Default | Description |
798
+ | --------------- | -------------------- | -------- | ------------------------------- |
799
+ | `uploadFile` | `UploadFileFn` | required | Upload function |
800
+ | `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
801
+ | `multiple` | `boolean` | `false` | Allow multiple files |
802
+ | `maxFiles` | `number` | `10` | Max files for multiple |
803
+ | `maxSizeMb` | `number` | `10` | Max file size in MB |
804
+ | `uploadOptions` | `IFileUploadOptions` | `{}` | Upload options |
805
+ | `disabled` | `boolean` | `false` | Disable uploader |
806
+ | `showPreview` | `boolean` | `true` | Show selected files preview |
807
+ | `autoUpload` | `boolean` | `true` | Upload immediately on selection |
735
808
 
736
809
  **Outputs:** `fileUploaded` (IUploadedFile), `filesUploaded` (IUploadedFile[]), `onError` (Error), `fileSelected` (File[])
737
810
 
@@ -744,15 +817,7 @@ Dialog to browse and select existing files with filtering.
744
817
  ```typescript
745
818
  @Component({
746
819
  imports: [FileSelectorDialogComponent],
747
- template: `
748
- <lib-file-selector-dialog
749
- [(visible)]="showFileSelector"
750
- [loadFiles]="loadFiles"
751
- [acceptTypes]="['image/*']"
752
- [multiple]="false"
753
- (fileSelected)="onFileSelected($event)"
754
- />
755
- `,
820
+ template: ` <lib-file-selector-dialog [(visible)]="showFileSelector" [loadFiles]="loadFiles" [acceptTypes]="['image/*']" [multiple]="false" (fileSelected)="onFileSelected($event)" /> `,
756
821
  })
757
822
  export class MyComponent {
758
823
  readonly showFileSelector = signal(false);
@@ -767,14 +832,14 @@ export class MyComponent {
767
832
 
768
833
  **Inputs:**
769
834
 
770
- | Input | Type | Default | Description |
771
- | ----- | ---- | ------- | ----------- |
772
- | `loadFiles` | `LoadFilesFn` | required | File loading function |
773
- | `header` | `string` | `'Select File'` | Dialog header |
774
- | `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
775
- | `multiple` | `boolean` | `false` | Allow multiple selection |
776
- | `maxSelection` | `number` | `10` | Max files for multiple |
777
- | `pageSize` | `number` | `20` | Page size for pagination |
835
+ | Input | Type | Default | Description |
836
+ | -------------- | ------------- | --------------- | ------------------------ |
837
+ | `loadFiles` | `LoadFilesFn` | required | File loading function |
838
+ | `header` | `string` | `'Select File'` | Dialog header |
839
+ | `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
840
+ | `multiple` | `boolean` | `false` | Allow multiple selection |
841
+ | `maxSelection` | `number` | `10` | Max files for multiple |
842
+ | `pageSize` | `number` | `20` | Page size for pagination |
778
843
 
779
844
  **Model:** `visible` - Two-way bound dialog visibility (`boolean`)
780
845
 
@@ -782,7 +847,7 @@ export class MyComponent {
782
847
 
783
848
  ---
784
849
 
785
- ## 5. Directives
850
+ ## 6. Directives
786
851
 
787
852
  ### HasPermissionDirective
788
853
 
@@ -792,7 +857,7 @@ Structural directive for permission-based rendering. Fail-closed: hides content
792
857
  - **Input:** `hasPermission` - `string | ILogicNode | null`
793
858
 
794
859
  ```typescript
795
- import { HasPermissionDirective, ILogicNode } from '@flusys/ng-shared';
860
+ import { HasPermissionDirective, ILogicNode } from "@flusys/ng-shared";
796
861
 
797
862
  @Component({
798
863
  imports: [HasPermissionDirective],
@@ -806,11 +871,11 @@ import { HasPermissionDirective, ILogicNode } from '@flusys/ng-shared';
806
871
  })
807
872
  export class MyComponent {
808
873
  readonly editLogic: ILogicNode = {
809
- type: 'group',
810
- operator: 'AND',
874
+ type: "group",
875
+ operator: "AND",
811
876
  children: [
812
- { type: 'action', actionId: 'user.view' },
813
- { type: 'action', actionId: 'user.update' },
877
+ { type: "action", actionId: "user.view" },
878
+ { type: "action", actionId: "user.update" },
814
879
  ],
815
880
  };
816
881
  }
@@ -824,8 +889,7 @@ Toggles readonly/disabled state for form controls based on edit mode. Supports `
824
889
  - **Input:** `isEditMode` (required boolean)
825
890
 
826
891
  ```html
827
- <input [appEditModeElementChanger] [isEditMode]="isEditing()" />
828
- <p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
892
+ <input [appEditModeElementChanger] [isEditMode]="isEditing()" /> <p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
829
893
  ```
830
894
 
831
895
  ### IsEmptyImageDirective
@@ -850,13 +914,12 @@ Prevents default browser behavior on specified events and emits the event.
850
914
  - **Output:** `action` - Emits the prevented event
851
915
 
852
916
  ```html
853
- <a href="#" appPreventDefault (action)="handleClick($event)">Click me</a>
854
- <input appPreventDefault eventType="keydown" preventKey="Enter" (action)="onEnter($event)" />
917
+ <a href="#" appPreventDefault (action)="handleClick($event)">Click me</a> <input appPreventDefault eventType="keydown" preventKey="Enter" (action)="onEnter($event)" />
855
918
  ```
856
919
 
857
920
  ---
858
921
 
859
- ## 6. Guards
922
+ ## 7. Guards
860
923
 
861
924
  Route-level guards for permission-based access control. All guards deny access when permissions are not loaded (fail-closed).
862
925
 
@@ -865,24 +928,29 @@ Route-level guards for permission-based access control. All guards deny access w
865
928
  Single permission or complex logic check.
866
929
 
867
930
  ```typescript
868
- import { permissionGuard } from '@flusys/ng-shared';
931
+ import { permissionGuard } from "@flusys/ng-shared";
869
932
 
870
933
  const routes: Routes = [
871
934
  // Simple permission
872
- { path: 'users', canActivate: [permissionGuard('user.view')] },
935
+ { path: "users", canActivate: [permissionGuard("user.view")] },
873
936
 
874
937
  // Complex logic (ILogicNode)
875
- { path: 'admin', canActivate: [permissionGuard({
876
- type: 'group',
877
- operator: 'AND',
878
- children: [
879
- { type: 'action', actionId: 'admin.view' },
880
- { type: 'action', actionId: 'admin.manage' },
938
+ {
939
+ path: "admin",
940
+ canActivate: [
941
+ permissionGuard({
942
+ type: "group",
943
+ operator: "AND",
944
+ children: [
945
+ { type: "action", actionId: "admin.view" },
946
+ { type: "action", actionId: "admin.manage" },
947
+ ],
948
+ }),
881
949
  ],
882
- })] },
950
+ },
883
951
 
884
952
  // Custom redirect on deny
885
- { path: 'settings', canActivate: [permissionGuard('settings.view', '/access-denied')] },
953
+ { path: "settings", canActivate: [permissionGuard("settings.view", "/access-denied")] },
886
954
  ];
887
955
  ```
888
956
 
@@ -904,42 +972,103 @@ AND logic - allows access only if user has ALL specified permissions.
904
972
 
905
973
  ---
906
974
 
907
- ## 7. Utilities
975
+ ## 8. Utilities
908
976
 
909
977
  ### Permission Evaluator
910
978
 
911
- Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards.
979
+ Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards. **Supports wildcard permissions.**
912
980
 
913
981
  ```typescript
914
- import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions } from '@flusys/ng-shared';
982
+ import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions, hasPermission } from "@flusys/ng-shared";
983
+
984
+ const userPermissions = ["user.view", "user.create", "admin.*"];
915
985
 
916
- const userPermissions = ['user.view', 'user.create'];
986
+ // Low-level hasPermission check with wildcard support
987
+ hasPermission("user.view", userPermissions); // true (exact match)
988
+ hasPermission("admin.manage", userPermissions); // true (matches 'admin.*')
989
+ hasPermission("settings.view", ["*"]); // true (global wildcard)
917
990
 
918
991
  // Evaluate string or ILogicNode
919
- evaluatePermission('user.view', userPermissions); // true
920
- evaluatePermission(null, userPermissions); // false
992
+ evaluatePermission("user.view", userPermissions); // true
993
+ evaluatePermission(null, userPermissions); // false
921
994
 
922
995
  // Evaluate ILogicNode tree recursively
923
996
  evaluateLogicNode(logicNode, userPermissions);
924
997
 
925
- // Simple OR/AND checks
926
- hasAnyPermission(['user.view', 'user.delete'], userPermissions); // true (has user.view)
927
- hasAllPermissions(['user.view', 'user.delete'], userPermissions); // false (missing user.delete)
998
+ // Simple OR/AND checks (also support wildcards)
999
+ hasAnyPermission(["user.view", "user.delete"], userPermissions); // true (has user.view)
1000
+ hasAllPermissions(["user.view", "user.delete"], userPermissions); // false (missing user.delete)
1001
+ ```
1002
+
1003
+ **Wildcard Rules:**
1004
+
1005
+ | Pattern | Matches |
1006
+ | ----------- | --------------------------------------- |
1007
+ | `*` | All permissions (global wildcard) |
1008
+ | `module.*` | All permissions starting with `module.` |
1009
+ | `user.read` | Exact match only |
1010
+
1011
+ **Implementation Details:**
1012
+
1013
+ ```typescript
1014
+ export function hasPermission(requiredPermission: string, userPermissions: string[]): boolean {
1015
+ // Exact match
1016
+ if (userPermissions.includes(requiredPermission)) return true;
1017
+
1018
+ // Wildcard matching
1019
+ for (const permission of userPermissions) {
1020
+ // Global wildcard
1021
+ if (permission === "*") return true;
1022
+
1023
+ // Module wildcard (e.g., 'user.*' matches 'user.read')
1024
+ if (permission.endsWith(".*")) {
1025
+ const prefix = permission.slice(0, -1); // 'user.'
1026
+ if (requiredPermission.startsWith(prefix)) return true;
1027
+ }
1028
+ }
1029
+
1030
+ return false;
1031
+ }
1032
+ ```
1033
+
1034
+ ### Scroll Pagination
1035
+
1036
+ Utility for lazy-loading dropdowns with scroll detection.
1037
+
1038
+ ```typescript
1039
+ import { checkScrollPagination, ScrollPaginationConfig } from '@flusys/ng-shared';
1040
+
1041
+ // In a component
1042
+ onScroll(event: Event): void {
1043
+ const nextPagination = checkScrollPagination(event, {
1044
+ pagination: this.pagination(),
1045
+ total: this.total(),
1046
+ isLoading: this.isLoading(),
1047
+ threshold: 50, // pixels from bottom (default: 50)
1048
+ });
1049
+ if (nextPagination) {
1050
+ this.onPagination.emit(nextPagination);
1051
+ }
1052
+ }
928
1053
  ```
929
1054
 
1055
+ **Interface:** `ScrollPaginationConfig` - `{ threshold?, pagination, total, isLoading }`
1056
+
1057
+ **Returns:** `IPagination | null` - Next page pagination or null if not needed.
1058
+
930
1059
  ---
931
1060
 
932
- ## 8. Classes
1061
+ ## 9. Classes
933
1062
 
934
1063
  ### BaseFormControl
935
1064
 
936
1065
  Abstract base class for custom form controls. Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
937
1066
 
938
1067
  ```typescript
939
- import { BaseFormControl, provideValueAccessor } from '@flusys/ng-shared';
1068
+ import { BaseFormControl, provideValueAccessor } from "@flusys/ng-shared";
940
1069
 
941
1070
  @Component({
942
- selector: 'my-select',
1071
+ selector: "my-select",
943
1072
  providers: [provideValueAccessor(MySelectComponent)],
944
1073
  })
945
1074
  export class MySelectComponent extends BaseFormControl<string | null> {
@@ -965,17 +1094,87 @@ export class MySelectComponent extends BaseFormControl<string | null> {
965
1094
 
966
1095
  **Helper:** `provideValueAccessor(ComponentClass)` - Factory for `NG_VALUE_ACCESSOR` provider
967
1096
 
1097
+ ### BaseFormPage
1098
+
1099
+ Abstract directive for form page components (create/edit).
1100
+
1101
+ ```typescript
1102
+ import { BaseFormPage } from '@flusys/ng-shared';
1103
+
1104
+ @Component({ ... })
1105
+ export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
1106
+ private readonly productService = inject(ProductApiService);
1107
+ private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
1108
+ readonly formModel = this._formModel.asReadonly();
1109
+
1110
+ getFormModel(): Signal<IProductFormModel> { return this.formModel; }
1111
+ getResourceRoute(): string { return '/products'; }
1112
+ getResourceName(): string { return 'Product'; }
1113
+ isFormValid(): boolean { return this.formModel().name.trim().length > 0; }
1114
+
1115
+ loadItem(id: string): void {
1116
+ this.productService.findById(id).subscribe(res => {
1117
+ if (res.success && res.data) {
1118
+ this.existingItem.set(res.data);
1119
+ this._formModel.set({ name: res.data.name, price: res.data.price });
1120
+ }
1121
+ });
1122
+ }
1123
+
1124
+ createItem(model: IProductFormModel): Observable<unknown> {
1125
+ return this.productService.insert(model);
1126
+ }
1127
+
1128
+ updateItem(model: IProductFormModel): Observable<unknown> {
1129
+ return this.productService.update({ id: this.existingItem()!.id, ...model });
1130
+ }
1131
+ }
1132
+ ```
1133
+
1134
+ **Signals:** `isLoading`, `existingItem`, `isEditMode` (computed)
1135
+
1136
+ **Methods:** `onSubmit()`, `onCancel()`, `showSuccess()`, `showError()`, `showValidationError()`
1137
+
1138
+ ### BaseListPage
1139
+
1140
+ Abstract directive for list page components with pagination and CRUD operations.
1141
+
1142
+ ```typescript
1143
+ import { BaseListPage } from '@flusys/ng-shared';
1144
+
1145
+ @Component({ ... })
1146
+ export class UserListComponent extends BaseListPage<IUser> {
1147
+ private readonly userService = inject(UserApiService);
1148
+
1149
+ getResourceRoute(): string { return '/users'; }
1150
+ getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
1151
+
1152
+ async loadData(): Promise<void> {
1153
+ this.isLoading.set(true);
1154
+ const res = await this.userService.findByIdAsync(...);
1155
+ this.items.set(res.data ?? []);
1156
+ this.total.set(res.meta?.total ?? 0);
1157
+ this.isLoading.set(false);
1158
+ }
1159
+ }
1160
+ ```
1161
+
1162
+ **Signals:** `items`, `isLoading`, `total`, `pageSize`, `first`, `currentPage` (computed), `showCompanyInfo` (computed)
1163
+
1164
+ **Methods:** `onCreate()`, `onEdit(id)`, `onPageChange(event)`, `onDelete()`, `onDeleteAsync()`, `showSuccess()`, `showError()`, `showInfo()`, `showWarn()`
1165
+
968
1166
  ---
969
1167
 
970
- ## 9. Modules
1168
+ ## 10. Modules
971
1169
 
972
1170
  ### AngularModule
973
1171
 
974
1172
  Re-exports common Angular modules for convenience.
975
1173
 
976
1174
  ```typescript
977
- import { AngularModule } from '@flusys/ng-shared';
978
- // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet, + directives
1175
+ import { AngularModule } from "@flusys/ng-shared";
1176
+ // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet,
1177
+ // RouterLinkActive, NgOptimizedImage, NgComponentOutlet, + directives (IsEmptyImageDirective, PreventDefaultDirective)
979
1178
  // Providers: DatePipe
980
1179
  ```
981
1180
 
@@ -984,14 +1183,22 @@ import { AngularModule } from '@flusys/ng-shared';
984
1183
  Re-exports PrimeNG component modules for convenience.
985
1184
 
986
1185
  ```typescript
987
- import { PrimeModule } from '@flusys/ng-shared';
988
- // Includes: Button, Table, Card, Dialog, InputText, Select, MultiSelect,
989
- // DatePicker, Checkbox, FileUpload, Image, Tag, Tabs, TreeTable, and more (27 modules)
1186
+ import { PrimeModule } from "@flusys/ng-shared";
1187
+ // Includes 30 modules:
1188
+ // - Layout: AccordionModule, CardModule, DividerModule, PanelModule, SplitterModule, TabsModule, ToolbarModule
1189
+ // - Form: AutoCompleteModule, CheckboxModule, DatePickerModule, InputTextModule, InputTextareaModule,
1190
+ // MultiSelectModule, RadioButtonModule, SelectModule, ToggleSwitchModule
1191
+ // - Button: ButtonModule, SpeedDialModule, SplitButtonModule
1192
+ // - Data: PaginatorModule, TableModule, TreeTableModule
1193
+ // - Overlay: DialogModule, DrawerModule, PopoverModule, TooltipModule
1194
+ // - File: FileUploadModule
1195
+ // - Media: ImageModule
1196
+ // - Misc: BadgeModule, TagModule
990
1197
  ```
991
1198
 
992
1199
  ---
993
1200
 
994
- ## 10. Provider Interfaces (Package Independence)
1201
+ ## 11. Provider Interfaces (Package Independence)
995
1202
 
996
1203
  ng-shared defines **provider interfaces** to enable feature packages (ng-iam, ng-storage) to access auth functionality without direct dependencies.
997
1204
 
@@ -1012,50 +1219,65 @@ ng-iam/ng-storage (consume interfaces via DI)
1012
1219
  User list access for IAM user selection.
1013
1220
 
1014
1221
  ```typescript
1015
- interface IUserBasicInfo { id: string; name: string; email: string }
1222
+ interface IUserBasicInfo {
1223
+ id: string;
1224
+ name: string;
1225
+ email: string;
1226
+ }
1016
1227
 
1017
1228
  interface IUserProvider {
1018
- getUsers(filter?: {
1019
- page?: number; pageSize?: number; search?: string;
1020
- companyId?: string; branchId?: string;
1021
- }): Observable<IListResponse<IUserBasicInfo>>;
1229
+ getUsers(filter?: { page?: number; pageSize?: number; search?: string; companyId?: string; branchId?: string }): Observable<IListResponse<IUserBasicInfo>>;
1022
1230
  }
1023
1231
  ```
1024
1232
 
1233
+ **Token Error Message:** `'USER_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1234
+
1025
1235
  #### ICompanyApiProvider / `COMPANY_API_PROVIDER`
1026
1236
 
1027
1237
  Company list access for IAM company selection.
1028
1238
 
1029
1239
  ```typescript
1030
- interface ICompanyBasicInfo { id: string; name: string; slug?: string }
1240
+ interface ICompanyBasicInfo {
1241
+ id: string;
1242
+ name: string;
1243
+ slug?: string;
1244
+ }
1031
1245
 
1032
1246
  interface ICompanyApiProvider {
1033
- getCompanies(filter?: {
1034
- page?: number; pageSize?: number; search?: string;
1035
- }): Observable<IListResponse<ICompanyBasicInfo>>;
1247
+ getCompanies(filter?: { page?: number; pageSize?: number; search?: string }): Observable<IListResponse<ICompanyBasicInfo>>;
1036
1248
  }
1037
1249
  ```
1038
1250
 
1251
+ **Token Error Message:** `'COMPANY_API_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1252
+
1039
1253
  #### IUserPermissionProvider / `USER_PERMISSION_PROVIDER`
1040
1254
 
1041
1255
  User permission queries for IAM.
1042
1256
 
1043
1257
  ```typescript
1258
+ interface IUserBranchPermission {
1259
+ branchId: string;
1260
+ branchName: string;
1261
+ permissions: string[];
1262
+ }
1263
+
1044
1264
  interface IUserPermissionProvider {
1045
- getUserBranchPermissions(userId: string): Observable<ISingleResponse<any>>;
1265
+ getUserBranchPermissions(userId: string): Observable<ISingleResponse<IUserBranchPermission[]>>;
1046
1266
  }
1047
1267
  ```
1048
1268
 
1269
+ **Token Error Message:** `'USER_PERMISSION_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1270
+
1049
1271
  #### IProfileUploadProvider / `PROFILE_UPLOAD_PROVIDER`
1050
1272
 
1051
- Profile picture upload for ng-auth profile page. Implemented by ng-storage.
1273
+ Profile picture upload for ng-auth profile page. Implemented by ng-storage. **Optional token** - use with `inject(..., { optional: true })`.
1052
1274
 
1053
1275
  ```typescript
1054
1276
  interface IProfileUploadResult {
1055
- id: string; // File manager ID (UUID)
1056
- name: string; // Original file name
1057
- key: string; // Storage key/path
1058
- size: number; // File size in bytes
1277
+ id: string; // File manager ID (UUID)
1278
+ name: string; // Original file name
1279
+ key: string; // Storage key/path
1280
+ size: number; // File size in bytes
1059
1281
  contentType: string; // MIME type
1060
1282
  }
1061
1283
 
@@ -1067,16 +1289,13 @@ interface IProfileUploadOptions {
1067
1289
  }
1068
1290
 
1069
1291
  interface IProfileUploadProvider {
1070
- uploadProfilePicture(
1071
- file: File,
1072
- options?: IProfileUploadOptions
1073
- ): Observable<ISingleResponse<IProfileUploadResult>>;
1292
+ uploadProfilePicture(file: File, options?: IProfileUploadOptions): Observable<ISingleResponse<IProfileUploadResult>>;
1074
1293
  }
1075
1294
  ```
1076
1295
 
1077
1296
  #### IProfilePermissionProvider / `PROFILE_PERMISSION_PROVIDER`
1078
1297
 
1079
- User permission queries for ng-auth profile page. Implemented by ng-iam.
1298
+ User permission queries for ng-auth profile page. Implemented by ng-iam. **Optional token** - use with `inject(..., { optional: true })`.
1080
1299
 
1081
1300
  ```typescript
1082
1301
  interface IProfileRoleInfo {
@@ -1112,6 +1331,8 @@ interface IAuthStateProvider {
1112
1331
  }
1113
1332
  ```
1114
1333
 
1334
+ **Token Error Message:** `'AUTH_STATE_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1335
+
1115
1336
  **Usage:**
1116
1337
 
1117
1338
  ```typescript
@@ -1133,7 +1354,7 @@ export class PublicFormComponent {
1133
1354
 
1134
1355
  #### IUserListProvider / `USER_LIST_PROVIDER`
1135
1356
 
1136
- Extends user list pages with extra columns, actions, and data enrichment. Optional provider.
1357
+ Extends user list pages with extra columns, actions, and data enrichment. **Optional token** - use with `inject(..., { optional: true })`.
1137
1358
 
1138
1359
  ```typescript
1139
1360
  interface IUserListItem {
@@ -1142,14 +1363,13 @@ interface IUserListItem {
1142
1363
  email: string;
1143
1364
  phone?: string;
1144
1365
  isActive?: boolean;
1145
- [key: string]: unknown;
1146
1366
  }
1147
1367
 
1148
1368
  interface IUserListAction<T = IUserListItem> {
1149
1369
  id: string;
1150
1370
  label: string;
1151
1371
  icon?: string;
1152
- severity?: 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger';
1372
+ severity?: "primary" | "secondary" | "success" | "info" | "warn" | "danger";
1153
1373
  permission?: string;
1154
1374
  tooltip?: string;
1155
1375
  disabled?: boolean | ((user: T) => boolean);
@@ -1161,7 +1381,16 @@ interface IUserListColumn {
1161
1381
  header: string;
1162
1382
  width?: string;
1163
1383
  sortable?: boolean;
1164
- templateType?: 'text' | 'badge' | 'date' | 'boolean' | 'custom';
1384
+ templateType?: "text" | "badge" | "date" | "boolean" | "custom";
1385
+ }
1386
+
1387
+ interface IUserListFilter {
1388
+ page?: number;
1389
+ pageSize?: number;
1390
+ search?: string;
1391
+ isActive?: boolean;
1392
+ companyId?: string;
1393
+ branchId?: string;
1165
1394
  }
1166
1395
 
1167
1396
  interface IUserListProvider<T extends IUserListItem = IUserListItem> {
@@ -1178,17 +1407,13 @@ interface IUserListProvider<T extends IUserListItem = IUserListItem> {
1178
1407
 
1179
1408
  ```typescript
1180
1409
  // In app.config.ts
1181
- providers: [
1182
- { provide: USER_LIST_PROVIDER, useClass: MyUserListProvider },
1183
- ]
1410
+ providers: [{ provide: USER_LIST_PROVIDER, useClass: MyUserListProvider }];
1184
1411
 
1185
1412
  // Implementation
1186
- @Injectable({ providedIn: 'root' })
1413
+ @Injectable({ providedIn: "root" })
1187
1414
  export class MyUserListProvider implements IUserListProvider {
1188
1415
  getExtraRowActions() {
1189
- return [
1190
- { id: 'assign-role', label: 'Assign Role', icon: 'pi pi-users' },
1191
- ];
1416
+ return [{ id: "assign-role", label: "Assign Role", icon: "pi pi-users" }];
1192
1417
  }
1193
1418
  }
1194
1419
  ```
@@ -1196,13 +1421,13 @@ export class MyUserListProvider implements IUserListProvider {
1196
1421
  ### Usage in Consuming Packages
1197
1422
 
1198
1423
  ```typescript
1199
- import { USER_PROVIDER } from '@flusys/ng-shared';
1424
+ import { USER_PROVIDER } from "@flusys/ng-shared";
1200
1425
 
1201
1426
  export class UserSelectorComponent {
1202
1427
  private readonly userProvider = inject(USER_PROVIDER);
1203
1428
 
1204
1429
  loadUsers() {
1205
- this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe(response => {
1430
+ this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe((response) => {
1206
1431
  this.users.set(response.data ?? []);
1207
1432
  });
1208
1433
  }
@@ -1213,7 +1438,7 @@ export class UserSelectorComponent {
1213
1438
 
1214
1439
  ```typescript
1215
1440
  // app.config.ts
1216
- import { provideAuthProviders } from '@flusys/ng-auth';
1441
+ import { provideAuthProviders } from "@flusys/ng-auth";
1217
1442
 
1218
1443
  export const appConfig: ApplicationConfig = {
1219
1444
  providers: [
@@ -1236,8 +1461,9 @@ export const appConfig: ApplicationConfig = {
1236
1461
 
1237
1462
  - **Extend `ApiResourceService`** for new services (signal-based)
1238
1463
  - Use reactive signals (`data`, `isLoading`, `total`) in templates
1239
- - Use `fetchList()` to trigger queries, `reload()` to refresh
1464
+ - Use `fetchList()` to trigger queries (also initializes resource), `reload()` to refresh
1240
1465
  - Use async methods (`insertAsync`, `updateAsync`) for one-off operations
1466
+ - Resource is lazy-initialized - no HTTP requests until first `fetchList()` or `initListResource()`
1241
1467
 
1242
1468
  ### File URLs
1243
1469
 
@@ -1263,6 +1489,7 @@ export const appConfig: ApplicationConfig = {
1263
1489
  - Use permission guards for route-level access control
1264
1490
  - Use `PermissionValidatorService` for programmatic checks in services
1265
1491
  - Permissions follow fail-closed model: no access by default
1492
+ - Wildcards supported: `*` (all), `module.*` (module-scoped)
1266
1493
 
1267
1494
  ---
1268
1495
 
@@ -1298,89 +1525,142 @@ export const appConfig: ApplicationConfig = {
1298
1525
 
1299
1526
  **Solution:** ng-shared must NEVER import from ng-layout. Move shared components to ng-shared, layout-specific components to ng-layout.
1300
1527
 
1528
+ ### Provider Token Errors
1529
+
1530
+ **Problem:** `'XXX_PROVIDER not configured'` error at runtime.
1531
+
1532
+ **Solution:** Ensure the provider is registered in `app.config.ts`:
1533
+
1534
+ ```typescript
1535
+ // Required providers
1536
+ providers: [
1537
+ { provide: USER_PROVIDER, useClass: AuthUserProvider },
1538
+ { provide: COMPANY_API_PROVIDER, useClass: AuthCompanyApiProvider },
1539
+ { provide: USER_PERMISSION_PROVIDER, useClass: AuthUserPermissionProvider },
1540
+ { provide: AUTH_STATE_PROVIDER, useClass: AuthStateProviderAdapter },
1541
+ ];
1542
+
1543
+ // OR use the convenience function
1544
+ providers: [...provideAuthProviders()];
1545
+ ```
1546
+
1547
+ ### Permissions Not Working
1548
+
1549
+ **Problem:** `hasPermission()` returns false even though user should have access.
1550
+
1551
+ **Solution:**
1552
+
1553
+ 1. Check if permissions are loaded: `permissionValidator.isLoaded()`
1554
+ 2. Verify permission codes match exactly (case-sensitive)
1555
+ 3. For wildcard access, ensure user has `*` or `module.*` in their permissions
1556
+
1301
1557
  ---
1302
1558
 
1303
1559
  ## API Reference
1304
1560
 
1305
1561
  ### Services
1306
1562
 
1307
- | Service | Description |
1308
- | ------------------------------ | ------------------------------------- |
1309
- | `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API |
1310
- | `FileUrlService` | Cloud storage URL fetching |
1311
- | `PermissionValidatorService` | Permission state management |
1312
- | `CookieService` | SSR-aware cookie reading |
1313
- | `PlatformService` | SSR environment detection |
1563
+ | Service | Description |
1564
+ | ---------------------------- | ---------------------------------------------------------------------------------------- |
1565
+ | `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API (lazy-initialized, accepts optional `serviceName`) |
1566
+ | `FileUrlService` | Cloud storage URL fetching |
1567
+ | `PermissionValidatorService` | Permission state management with wildcards |
1568
+ | `CookieService` | SSR-aware cookie reading |
1569
+ | `PlatformService` | SSR environment detection |
1570
+
1571
+ ### Classes
1572
+
1573
+ | Class | Description |
1574
+ | -------------------- | -------------------------------------------------- |
1575
+ | `ApiResourceService` | Signal-based CRUD base class (alias: `ApiService`) |
1576
+ | `BaseFormControl` | Abstract base for custom form controls |
1577
+ | `BaseFormPage` | Abstract directive for create/edit pages |
1578
+ | `BaseListPage` | Abstract directive for list pages |
1579
+
1580
+ ### Constants
1581
+
1582
+ | Constant | Description |
1583
+ | ------------------- | ----------------------------------------------------- |
1584
+ | `PERMISSIONS` | Aggregated permission codes by module |
1585
+ | `USER_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for users |
1586
+ | `ROLE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for roles |
1587
+ | `FILE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for files |
1588
+ | `FILE_TYPE_FILTERS` | Predefined MIME type arrays (IMAGES, DOCUMENTS, etc.) |
1314
1589
 
1315
1590
  ### Components
1316
1591
 
1317
- | Component | Selector | Description |
1318
- | -------------------------- | ---------------------- | ------------------------- |
1319
- | `IconComponent` | `lib-icon` | Flexible icon renderer |
1320
- | `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
1321
- | `LazyMultiSelectComponent` | `lib-lazy-multi-select`| Lazy-loading multi-select |
1322
- | `UserSelectComponent` | `lib-user-select` | Single user selector |
1323
- | `UserMultiSelectComponent` | `lib-user-multi-select`| Multiple user selector |
1324
- | `FileUploaderComponent` | `lib-file-uploader` | Drag & drop file upload |
1325
- | `FileSelectorDialogComponent` | `lib-file-selector-dialog` | File browser dialog |
1592
+ | Component | Selector | Description |
1593
+ | ----------------------------- | -------------------------- | ------------------------- |
1594
+ | `IconComponent` | `lib-icon` | Flexible icon renderer |
1595
+ | `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
1596
+ | `LazyMultiSelectComponent` | `lib-lazy-multi-select` | Lazy-loading multi-select |
1597
+ | `UserSelectComponent` | `lib-user-select` | Single user selector |
1598
+ | `UserMultiSelectComponent` | `lib-user-multi-select` | Multiple user selector |
1599
+ | `FileUploaderComponent` | `lib-file-uploader` | Drag & drop file upload |
1600
+ | `FileSelectorDialogComponent` | `lib-file-selector-dialog` | File browser dialog |
1326
1601
 
1327
1602
  ### Directives
1328
1603
 
1329
- | Directive | Selector | Description |
1330
- | --------------------------------- | --------------------------- | ----------------------------------- |
1331
- | `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
1604
+ | Directive | Selector | Description |
1605
+ | --------------------------------- | ----------------------------- | --------------------------------- |
1606
+ | `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
1332
1607
  | `EditModeElementChangerDirective` | `[appEditModeElementChanger]` | Toggle edit mode on form controls |
1333
- | `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
1334
- | `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
1608
+ | `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
1609
+ | `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
1335
1610
 
1336
1611
  ### Guards
1337
1612
 
1338
- | Guard | Description |
1339
- | ---------------------- | -------------------------------------- |
1340
- | `permissionGuard` | Single permission or ILogicNode check |
1341
- | `anyPermissionGuard` | OR logic (any of listed permissions) |
1342
- | `allPermissionsGuard` | AND logic (all of listed permissions) |
1613
+ | Guard | Description |
1614
+ | --------------------- | ------------------------------------- |
1615
+ | `permissionGuard` | Single permission or ILogicNode check |
1616
+ | `anyPermissionGuard` | OR logic (any of listed permissions) |
1617
+ | `allPermissionsGuard` | AND logic (all of listed permissions) |
1343
1618
 
1344
1619
  ### Interfaces
1345
1620
 
1346
- | Interface | Description |
1347
- | -------------------- | ------------------------------------ |
1348
- | `IBaseEntity` | Base entity with ID and timestamps |
1349
- | `ILoggedUserInfo` | Current user info with company ctx |
1350
- | `IFilterData` | Filter, pagination, sort payload |
1351
- | `IDeleteData` | Delete request payload |
1352
- | `IDropDown` | Simple label/value pair |
1353
- | `ISingleResponse<T>` | Single item response |
1354
- | `IListResponse<T>` | List with pagination |
1355
- | `IBulkResponse<T>` | Bulk operation response |
1356
- | `IMessageResponse` | Message-only response |
1357
- | `IErrorResponse` | Error with validation details |
1358
- | `ILogicNode` | Permission logic tree (AND/OR nodes) |
1359
- | `IUserSelectFilter` | User select filter params |
1360
- | `LoadUsersFn` | User loading function type |
1361
- | `IFileBasicInfo` | Basic file info for selectors |
1362
- | `IFileUploadOptions`| Upload options (compression, etc.) |
1363
- | `IUploadedFile` | Uploaded file response |
1364
- | `IFileSelectFilter` | File select filter params |
1365
- | `LoadFilesFn` | File loading function type |
1366
- | `UploadFileFn` | File upload function type |
1367
- | `IAuthStateProvider`| Auth state provider interface |
1368
- | `IUserListProvider` | User list extensions provider |
1369
- | `IUserListItem` | Base user for list operations |
1370
- | `IUserListAction` | User list action definition |
1371
- | `IUserListColumn` | Extra column for user list |
1621
+ | Interface | Description |
1622
+ | ----------------------- | ------------------------------------------------------------------------------ |
1623
+ | `IBaseEntity` | Base entity with ID and timestamps |
1624
+ | `ILoggedUserInfo` | Current user info with company ctx |
1625
+ | `IFilterData` | Filter, pagination, sort payload |
1626
+ | `IFilter` | Filter object (supports arrays) |
1627
+ | `IDeleteData` | Delete request payload |
1628
+ | `IDropDown` | Simple label/value pair |
1629
+ | `ISingleResponse<T>` | Single item response |
1630
+ | `IListResponse<T>` | List with pagination |
1631
+ | `IBulkResponse<T>` | Bulk operation response |
1632
+ | `IMessageResponse` | Message-only response |
1633
+ | `IErrorResponse` | Error with validation details |
1634
+ | `ILogicNode` | Permission logic tree (AND/OR nodes) |
1635
+ | `IUserSelectFilter` | User select filter params |
1636
+ | `LoadUsersFn` | User loading function type |
1637
+ | `IFileBasicInfo` | Basic file info for selectors |
1638
+ | `IFileUploadOptions` | Upload options (compression, etc.) |
1639
+ | `IUploadedFile` | Uploaded file response |
1640
+ | `IFileSelectFilter` | File select filter params |
1641
+ | `LoadFilesFn` | File loading function type |
1642
+ | `UploadFileFn` | File upload function type |
1643
+ | `FilesResponseDto` | File URL service response |
1644
+ | `IAuthStateProvider` | Auth state provider interface |
1645
+ | `IUserListProvider` | User list extensions provider |
1646
+ | `IUserListItem` | Base user for list operations |
1647
+ | `IUserListAction` | User list action definition |
1648
+ | `IUserListColumn` | Extra column for user list |
1649
+ | `IUserListFilter` | User list filter parameters |
1650
+ | `IUserBranchPermission` | User permissions per branch |
1651
+ | `ServiceName` | `'auth' \| 'administration' \| 'iam' \| 'storage' \| 'formBuilder' \| 'email'` |
1372
1652
 
1373
1653
  ### Injection Tokens
1374
1654
 
1375
- | Token | Interface | Description |
1376
- | -------------------------- | ------------------------- | ---------------------------- |
1377
- | `USER_PROVIDER` | `IUserProvider` | User list for IAM |
1378
- | `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | Company list for IAM |
1379
- | `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | User permission queries |
1380
- | `PROFILE_UPLOAD_PROVIDER` | `IProfileUploadProvider` | Profile picture upload (ng-storage) |
1381
- | `PROFILE_PERMISSION_PROVIDER` | `IProfilePermissionProvider` | User permissions for profile (ng-iam) |
1382
- | `AUTH_STATE_PROVIDER` | `IAuthStateProvider` | Auth state for feature packages |
1383
- | `USER_LIST_PROVIDER` | `IUserListProvider` | User list extensions (optional) |
1655
+ | Token | Interface | Optional | Description |
1656
+ | ----------------------------- | ---------------------------- | -------- | ------------------------------------- |
1657
+ | `USER_PROVIDER` | `IUserProvider` | No | User list for IAM |
1658
+ | `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | No | Company list for IAM |
1659
+ | `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | No | User permission queries |
1660
+ | `AUTH_STATE_PROVIDER` | `IAuthStateProvider` | No | Auth state for feature packages |
1661
+ | `PROFILE_UPLOAD_PROVIDER` | `IProfileUploadProvider` | Yes | Profile picture upload (ng-storage) |
1662
+ | `PROFILE_PERMISSION_PROVIDER` | `IProfilePermissionProvider` | Yes | User permissions for profile (ng-iam) |
1663
+ | `USER_LIST_PROVIDER` | `IUserListProvider` | Yes | User list extensions |
1384
1664
 
1385
1665
  ## See Also
1386
1666
 
@@ -1391,5 +1671,6 @@ export const appConfig: ApplicationConfig = {
1391
1671
 
1392
1672
  ---
1393
1673
 
1394
- **Last Updated:** 2026-02-21
1674
+ **Last Updated:** 2026-02-25
1675
+ **Version:** 3.0.1
1395
1676
  **Angular Version:** 21