@flusys/ng-shared 4.0.2 → 4.1.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.
Files changed (2) hide show
  1. package/README.md +323 -1607
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,1814 +1,530 @@
1
- # @flusys/ng-shared Package Guide
1
+ # @flusys/ng-shared
2
2
 
3
- ## Overview
4
-
5
- `@flusys/ng-shared` provides reusable components, directives, API services, and utilities used across all FLUSYS applications. This package extends ng-core with UI components, API integration patterns, and provider interfaces for package independence.
6
-
7
- **Key Principle:** ng-shared depends only on ng-core, never on ng-layout or feature packages.
8
-
9
- ## Package Information
3
+ > Shared utilities, response interfaces, provider interfaces, base classes, and reusable UI components for the FLUSYS Angular platform.
10
4
 
11
- - **Package:** `@flusys/ng-shared`
12
- - **Version:** 4.0.2
13
- - **Dependencies:** ng-core
14
- - **Dependents:** ng-layout, ng-auth, ng-iam, ng-storage, flusysng
15
- - **Build Command:** `npm run build:ng-shared`
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/ng-shared.svg)](https://www.npmjs.com/package/@flusys/ng-shared)
6
+ [![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
16
9
 
17
10
  ---
18
11
 
19
- ## 1. Interfaces
12
+ ## Table of Contents
13
+
14
+ - [Overview](#overview)
15
+ - [Features](#features)
16
+ - [Compatibility](#compatibility)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [Response Interfaces](#response-interfaces)
20
+ - [Provider Interface Pattern](#provider-interface-pattern)
21
+ - [Injection Tokens](#injection-tokens)
22
+ - [USER_PROVIDER](#user_provider)
23
+ - [COMPANY_PROVIDER](#company_provider)
24
+ - [FILE_PROVIDER](#file_provider)
25
+ - [Base Classes](#base-classes)
26
+ - [ApiResourceService](#apiresourceservice)
27
+ - [BaseListPage](#baselistpage)
28
+ - [BaseFormPage](#baseformpage)
29
+ - [Services](#services)
30
+ - [FileUrlService](#fileurlservice)
31
+ - [PermissionValidatorService](#permissionvalidatorservice)
32
+ - [Reusable Components](#reusable-components)
33
+ - [Directives](#directives)
34
+ - [Pipes](#pipes)
35
+ - [Guards](#guards)
36
+ - [Modules](#modules)
37
+ - [Troubleshooting](#troubleshooting)
38
+ - [License](#license)
20
39
 
21
- ### Data Interfaces
22
-
23
- #### IBaseEntity
24
-
25
- Base entity interface matching backend Identity entity.
40
+ ---
26
41
 
27
- ```typescript
28
- interface IBaseEntity {
29
- id: string;
30
- createdAt: Date;
31
- updatedAt: Date;
32
- deletedAt?: Date | null;
33
- createdById?: string | null;
34
- updatedById?: string | null;
35
- deletedById?: string | null;
36
- }
37
- ```
42
+ ## Overview
38
43
 
39
- **Entity Mixins:**
44
+ `@flusys/ng-shared` is the second layer in the FLUSYS Angular dependency hierarchy. It depends on `@flusys/ng-core` and provides everything that feature packages (`ng-auth`, `ng-iam`, `ng-storage`, etc.) need to function **independently** of each other.
40
45
 
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 |
46
+ The cornerstone of `ng-shared` is the **Provider Interface Pattern**: injection tokens that define contracts between feature packages. `ng-auth` provides implementations; `ng-iam`, `ng-storage`, and other packages inject the interfaces — never the concrete implementations.
48
47
 
49
- #### ILoggedUserInfo
48
+ ---
50
49
 
51
- Current logged-in user info with optional company context.
50
+ ## Features
52
51
 
53
- ```typescript
54
- interface ILoggedUserInfo {
55
- id: string;
56
- email: string;
57
- name?: string;
58
- phone?: string;
59
- profilePictureId?: string;
60
- companyId?: string;
61
- branchId?: string;
62
- companyLogoId?: string;
63
- branchLogoId?: string;
64
- }
65
- ```
52
+ - ✅ Typed response interfaces (`ISingleResponse`, `IListResponse`, `IBulkResponse`, `IMessageResponse`)
53
+ - Provider Interface Pattern — `USER_PROVIDER`, `COMPANY_PROVIDER`, `FILE_PROVIDER`
54
+ - ✅ `ApiResourceService` — typed CRUD service base class
55
+ - ✅ `BaseListPage` / `BaseFormPage` — page scaffolding with signal state
56
+ - ✅ `FileUrlService` — safe presigned URL fetching (never construct URLs manually)
57
+ - ✅ `PermissionValidatorService` — client-side permission evaluation
58
+ - ✅ `LazySelectComponent` — virtualized dropdown with server-side search
59
+ - ✅ `FileSelectorDialogComponent` — file picker dialog
60
+ - ✅ `HasPermissionDirective` — structural directive for permission-gated content
61
+ - ✅ `TranslatePipe` — i18n pipe with parameter interpolation
62
+ - ✅ `AngularModule` / `PrimeModule` — pre-aggregated Angular and PrimeNG imports
66
63
 
67
- #### IFilterData
64
+ ---
68
65
 
69
- Filter, pagination, and sort payload for list queries.
66
+ ## Compatibility
70
67
 
71
- ```typescript
72
- interface IPagination {
73
- pageSize: number;
74
- currentPage: number;
75
- }
68
+ | Package | Version |
69
+ |---------|---------|
70
+ | Angular | 21+ |
71
+ | @flusys/ng-core | 4.x |
72
+ | PrimeNG | 18+ |
76
73
 
77
- interface ISort {
78
- [key: string]: "ASC" | "DESC";
79
- }
74
+ ---
80
75
 
81
- // Filter supports primitives and arrays for multi-value filtering
82
- interface IFilter {
83
- [key: string]: string | number | boolean | null | undefined | string[] | number[];
84
- }
76
+ ## Installation
85
77
 
86
- interface IFilterData {
87
- filter?: IFilter;
88
- pagination?: IPagination;
89
- select?: string[];
90
- sort?: ISort;
91
- withDeleted?: boolean;
92
- extraKey?: string[];
93
- }
78
+ ```bash
79
+ npm install @flusys/ng-shared @flusys/ng-core
94
80
  ```
95
81
 
96
- **Note:** `IFilter` supports array values (`string[]`, `number[]`) for multi-value filtering (e.g., filtering by multiple status values or IDs).
97
-
98
- #### IDeleteData
82
+ ---
99
83
 
100
- Delete request payload matching backend DeleteDto.
84
+ ## Quick Start
101
85
 
102
86
  ```typescript
103
- type DeleteType = "delete" | "restore" | "permanent";
87
+ // app.config.ts
88
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
89
+ import { APP_CONFIG } from '@flusys/ng-core';
90
+ import { environment } from './environments/environment';
104
91
 
105
- interface IDeleteData {
106
- id: string | string[]; // Single or batch delete
107
- type: DeleteType;
108
- }
92
+ export const appConfig: ApplicationConfig = {
93
+ providers: [
94
+ { provide: APP_CONFIG, useValue: environment },
95
+ provideHttpClient(),
96
+ // Feature providers register against ng-shared tokens (see below)
97
+ ],
98
+ };
109
99
  ```
110
100
 
111
- #### IDropDown
101
+ ---
112
102
 
113
- Simple dropdown item for select components.
103
+ ## Response Interfaces
114
104
 
115
- ```typescript
116
- interface IDropDown {
117
- label: string;
118
- value: string;
119
- }
120
- ```
105
+ All FLUSYS backend responses conform to one of four shapes:
121
106
 
122
- #### User Select Interfaces
107
+ ### ISingleResponse\<T\>
123
108
 
124
109
  ```typescript
125
- interface IUserSelectFilter {
126
- page: number;
127
- pageSize: number;
128
- search: string;
110
+ interface ISingleResponse<T> {
111
+ data: T;
129
112
  }
130
-
131
- type LoadUsersFn = (filter: IUserSelectFilter) => Observable<IListResponse<IUserBasicInfo>>;
132
113
  ```
133
114
 
134
- #### File Select Interfaces
115
+ ### IListResponse\<T\>
135
116
 
136
117
  ```typescript
137
- interface IFileBasicInfo {
138
- id: string;
139
- name: string;
140
- contentType: string;
141
- size: string;
142
- url: string | null;
143
- }
144
-
145
- interface IFileUploadOptions {
146
- storageConfigId?: string;
147
- folderPath?: string;
148
- maxWidth?: number;
149
- maxHeight?: number;
150
- quality?: number;
151
- compress?: boolean;
152
- }
153
-
154
- interface IUploadedFile {
155
- id?: string; // File manager ID (UUID) - available when registered
156
- name: string;
157
- key: string;
158
- size: number;
159
- contentType: string;
160
- }
161
-
162
- interface IFileSelectFilter {
118
+ interface IListResponse<T> {
119
+ data: T[];
120
+ total: number;
163
121
  page: number;
164
122
  pageSize: number;
165
- search: string;
166
- contentTypes?: string[];
167
- folderId?: string;
168
123
  }
169
-
170
- type LoadFilesFn = (filter: IFileSelectFilter) => Observable<IListResponse<IFileBasicInfo>>;
171
- type UploadFileFn = (file: File, options?: IFileUploadOptions) => Observable<ISingleResponse<IUploadedFile>>;
172
- type GetFileUrlsFn = (fileIds: string[]) => Observable<ISingleResponse<IFileBasicInfo[]>>;
173
124
  ```
174
125
 
175
- **File Type Filters (Constants):**
126
+ ### IBulkResponse\<T\>
176
127
 
177
128
  ```typescript
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'
129
+ interface IBulkResponse<T> {
130
+ data: T[];
131
+ successCount: number;
132
+ failureCount: number;
133
+ errors?: string[];
134
+ }
190
135
  ```
191
136
 
192
- ### Response Interfaces
193
-
194
- Type-safe interfaces matching FLUSYS_NEST backend response DTOs.
137
+ ### IMessageResponse
195
138
 
196
139
  ```typescript
197
- // Single item response (POST /resource/insert, /update, /get/:id)
198
- interface ISingleResponse<T> {
199
- success: boolean;
200
- message: string;
201
- data?: T;
202
- _meta?: IRequestMeta;
203
- }
204
-
205
- // List response with pagination (POST /resource/get-all)
206
- interface IListResponse<T> {
207
- success: boolean;
208
- message: string;
209
- data?: T[];
210
- meta: IPaginationMeta;
211
- _meta?: IRequestMeta;
212
- }
213
-
214
- // Bulk operation response (POST /resource/insert-many, /update-many)
215
- interface IBulkResponse<T> {
216
- success: boolean;
217
- message: string;
218
- data?: T[];
219
- meta: IBulkMeta;
220
- _meta?: IRequestMeta;
221
- }
222
-
223
- // Message-only response (POST /resource/delete, /logout)
224
140
  interface IMessageResponse {
225
- success: boolean;
226
- message: string;
227
- _meta?: IRequestMeta;
228
- }
229
-
230
- // Error response (validation errors, exceptions)
231
- interface IErrorResponse {
232
- success: false;
233
141
  message: string;
234
- code?: string;
235
- errors?: IValidationError[];
236
- _meta?: IRequestMeta;
237
142
  }
238
-
239
- // Union type
240
- type ApiResponse<T> = ISingleResponse<T> | IListResponse<T> | IBulkResponse<T> | IMessageResponse | IErrorResponse;
241
143
  ```
242
144
 
243
- **Metadata types:**
244
-
245
- | Interface | Fields |
246
- | ------------------ | ----------------------------------------------------- |
247
- | `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
248
- | `IPaginationMeta` | `total, page, pageSize, count, hasMore?, totalPages?` |
249
- | `IBulkMeta` | `count, failed?, total?` |
250
- | `IValidationError` | `field, message, constraint?` |
251
-
252
- ### Auth & Storage Response Types
145
+ ### IErrorResponse
253
146
 
254
147
  ```typescript
255
- interface ILoginResponse {
256
- success: boolean;
257
- message: string;
258
- data: { accessToken: string; refreshToken: string; user: ILoginUserData };
259
- }
260
-
261
- interface IRefreshTokenResponse {
262
- success: boolean;
148
+ interface IErrorResponse {
149
+ statusCode: number;
263
150
  message: string;
264
- data: { accessToken: string; refreshToken?: string };
265
- }
266
-
267
- interface IFileData {
268
- id: string;
269
- name: string;
270
- originalName: string;
271
- contentType: string;
272
- size: number;
273
- key: string;
274
- url?: string;
275
- thumbnailUrl?: string;
276
- createdAt: Date;
277
- }
278
-
279
- // File URL service response DTO
280
- interface FilesResponseDto {
281
- id: string;
282
- name: string;
283
- contentType: string;
284
- url: string | null;
151
+ error?: string;
285
152
  }
286
153
  ```
287
154
 
288
- ### Permission Interfaces
289
-
290
- Discriminated union for building complex permission logic trees.
155
+ ### IListParams (Common Request Shape)
291
156
 
292
157
  ```typescript
293
- // Single permission check
294
- interface IActionNode {
295
- type: "action";
296
- actionId: string;
297
- }
298
-
299
- // Group with AND/OR logic
300
- interface IGroupNode {
301
- type: "group";
302
- operator: "AND" | "OR";
303
- children: ILogicNode[];
158
+ interface IListParams {
159
+ page?: number;
160
+ pageSize?: number;
161
+ search?: string;
162
+ sortBy?: string;
163
+ sortOrder?: 'ASC' | 'DESC';
164
+ filters?: Record<string, unknown>;
304
165
  }
305
-
306
- // Union type
307
- type ILogicNode = IActionNode | IGroupNode;
308
166
  ```
309
167
 
310
168
  ---
311
169
 
312
- ## 2. Enums
170
+ ## Provider Interface Pattern
313
171
 
314
- ```typescript
315
- enum ContactTypeEnum {
316
- PHONE = 1,
317
- EMAIL = 2,
318
- }
172
+ Feature packages never import each other directly. Instead, `ng-shared` defines **injection token interfaces** that decouple consumers from providers.
319
173
 
320
- enum IconTypeEnum {
321
- PRIMENG_ICON = 1,
322
- IMAGE_FILE_LINK = 2,
323
- DIRECT_TAG_SVG = 3,
324
- }
325
174
  ```
326
-
327
- ---
328
-
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'
175
+ ng-auth ──implements──► USER_PROVIDER token ◄──injects── ng-iam
176
+ ──implements──► COMPANY_PROVIDER ◄──injects── ng-storage
177
+ ng-storage ──implements──► FILE_PROVIDER ◄──injects── ng-shared components
362
178
  ```
363
179
 
364
- **Type:** `PermissionCode` - Union type of all valid permission code strings.
365
-
366
- ---
367
-
368
- ## 4. Services
369
-
370
- ### ApiResourceService
180
+ ### Injection Tokens
371
181
 
372
- Signal-based CRUD service using Angular 21 `resource()` API with **lazy initialization**. All endpoints use POST (RPC-style).
182
+ | Token | Interface | Provided By | Used By |
183
+ |-------|-----------|-------------|---------|
184
+ | `USER_PROVIDER` | `IUserProvider` | `ng-auth` | `ng-iam`, `ng-storage` |
185
+ | `COMPANY_PROVIDER` | `ICompanyProvider` | `ng-auth` | `ng-storage`, `ng-notification` |
186
+ | `FILE_PROVIDER` | `IFileProvider` | `ng-storage` | `ng-shared` components |
187
+ | `LAYOUT_AUTH_STATE` | `ILayoutAuthState` | `ng-auth` | `ng-layout` |
188
+ | `LAYOUT_AUTH_API` | `ILayoutAuthApi` | `ng-auth` | `ng-layout` |
189
+ | `LAYOUT_NOTIFICATION_BELL` | `INotificationBellProvider` | `ng-notification` | `ng-layout` |
190
+ | `LAYOUT_LANGUAGE_SELECTOR` | `ILanguageSelectorProvider` | `ng-localization` | `ng-layout` |
373
191
 
374
- **ServiceName Type:**
192
+ ### USER_PROVIDER
375
193
 
376
194
  ```typescript
377
- type ServiceName = "auth" | "administration" | "iam" | "storage" | "formBuilder" | "email";
378
- ```
195
+ interface IUserProvider {
196
+ user: Signal<ICurrentUser | null>;
197
+ isAuthenticated: Signal<boolean>;
198
+ }
379
199
 
380
- **Define a service:**
200
+ // Consuming (in ng-iam)
201
+ import { USER_PROVIDER, IUserProvider } from '@flusys/ng-shared';
381
202
 
382
- ```typescript
383
- import { Injectable } from "@angular/core";
384
- import { HttpClient } from "@angular/common/http";
385
- import { ApiResourceService } from "@flusys/ng-shared";
386
-
387
- @Injectable({ providedIn: "root" })
388
- export class UserService extends ApiResourceService<UserDto, IUser> {
389
- constructor(http: HttpClient) {
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'
203
+ @Injectable({ providedIn: 'root' })
204
+ export class IamService {
205
+ private userProvider = inject<IUserProvider>(USER_PROVIDER);
206
+
207
+ getCurrentUserId(): string | null {
208
+ return this.userProvider.user()?.id ?? null;
397
209
  }
398
210
  }
399
211
  ```
400
212
 
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:
213
+ ### COMPANY_PROVIDER
417
214
 
418
215
  ```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
216
+ interface ICompanyProvider {
217
+ company: Signal<ICompany | null>;
218
+ branch: Signal<IBranch | null>;
219
+ companyId: Signal<string | null>;
220
+ }
423
221
  ```
424
222
 
425
- **Internal resource structure:**
223
+ ### FILE_PROVIDER
426
224
 
427
225
  ```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.
226
+ interface IFileProvider {
227
+ getFileUrl(fileId: string): Observable<string | null>;
228
+ uploadFile(file: File, folder?: string): Observable<IUploadedFile>;
438
229
  }
439
230
  ```
440
231
 
441
- **Endpoint mapping:**
232
+ ---
442
233
 
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` |
234
+ ## Base Classes
452
235
 
453
- All methods above return `Observable`. Async (Promise) variants are also available: `insertAsync`, `insertManyAsync`, `findByIdAsync`, `updateAsync`, `updateManyAsync`, `deleteAsync`.
236
+ ### ApiResourceService
454
237
 
455
- **Reactive signals:**
238
+ Generic typed CRUD service that maps to FLUSYS POST-only RPC endpoints. Extend this to create a fully-typed API service in seconds.
456
239
 
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 |
465
- | `filterData` | `WritableSignal<IFilterData>` | Filter/pagination state |
240
+ ```typescript
241
+ import { ApiResourceService } from '@flusys/ng-shared';
466
242
 
467
- **List management:**
243
+ @Injectable({ providedIn: 'root' })
244
+ export class ProductApiService extends ApiResourceService<Product, CreateProductDto, UpdateProductDto> {
245
+ protected override resource = 'product';
246
+ }
468
247
 
469
- ```typescript
470
- // Trigger data fetch (updates searchTerm and filterData signals, resource auto-reloads)
471
- userService.fetchList("search term", { pagination: { currentPage: 0, pageSize: 10 } });
472
-
473
- // Pagination helpers
474
- userService.setPagination({ currentPage: 1, pageSize: 20 });
475
- userService.nextPage();
476
- userService.resetPagination();
477
- userService.reload();
248
+ // Automatically provides:
249
+ // - getAll(params): Observable<IListResponse<Product>>
250
+ // - getById(id): Observable<ISingleResponse<Product>>
251
+ // - insert(dto): Observable<ISingleResponse<Product>>
252
+ // - update(dto): Observable<ISingleResponse<Product>>
253
+ // - delete(id): Observable<IMessageResponse>
254
+ // - bulkDelete(ids): Observable<IBulkResponse<Product>>
478
255
  ```
479
256
 
480
- **Component usage:**
257
+ ### BaseListPage
481
258
 
482
- ```typescript
483
- @Component({...})
484
- export class UserListComponent {
485
- private readonly userService = inject(UserService);
259
+ Scaffold for list pages with built-in pagination, search, and loading state using signals.
486
260
 
487
- readonly users = this.userService.data; // Signal<IUser[]>
488
- readonly isLoading = this.userService.isLoading; // Signal<boolean>
489
- readonly total = this.userService.total; // Signal<number>
261
+ ```typescript
262
+ import { BaseListPage } from '@flusys/ng-shared';
490
263
 
491
- constructor() {
492
- this.userService.fetchList('', { pagination: { currentPage: 0, pageSize: 10 } });
493
- }
264
+ @Component({
265
+ selector: 'app-product-list',
266
+ templateUrl: './product-list.component.html',
267
+ })
268
+ export class ProductListComponent extends BaseListPage<Product> {
269
+ protected apiService = inject(ProductApiService);
494
270
 
495
- async createUser(dto: UserDto) {
496
- await this.userService.insertAsync(dto);
497
- this.userService.reload(); // Refresh list
498
- }
271
+ // Inherited signals:
272
+ // items Signal<Product[]>
273
+ // total Signal<number>
274
+ // page Signal<number>
275
+ // pageSize Signal<number>
276
+ // isLoading Signal<boolean>
277
+ // searchQuery Signal<string>
499
278
  }
500
279
  ```
501
280
 
502
- > **Note:** `ApiService` is exported as an alias for `ApiResourceService` for backward compatibility.
503
-
504
- ### FileUrlService
505
-
506
- Fetches file URLs from the backend. Supports presigned URLs for cloud storage (S3, Azure).
281
+ ### BaseFormPage
507
282
 
508
- **CRITICAL:** Never construct file URLs manually. Always use this service.
283
+ Scaffold for create/edit forms with validation state and submission handling.
509
284
 
510
285
  ```typescript
511
- import { FileUrlService } from '@flusys/ng-shared';
512
-
513
- @Component({...})
514
- export class ProductComponent {
515
- private readonly fileUrlService = inject(FileUrlService);
286
+ import { BaseFormPage } from '@flusys/ng-shared';
516
287
 
517
- loadImage(fileId: string) {
518
- // Fetch from backend: POST /file-manager/get-files
519
- this.fileUrlService.fetchSingleFileUrl(fileId).subscribe(file => {
520
- this.imageUrl.set(file?.url ?? null);
521
- });
522
- }
288
+ @Component({
289
+ selector: 'app-product-form',
290
+ templateUrl: './product-form.component.html',
291
+ })
292
+ export class ProductFormComponent extends BaseFormPage<Product> {
293
+ protected apiService = inject(ProductApiService);
523
294
 
524
- loadMultiple(fileIds: string[]) {
525
- this.fileUrlService.fetchFileUrls(fileIds).subscribe(files => {
526
- // files: FilesResponseDto[] with { id, name, contentType, url }
527
- });
528
- }
295
+ // Inherited signals:
296
+ // isSaving Signal<boolean>
297
+ // isEditMode Signal<boolean>
298
+ // formErrors Signal<Record<string, string>>
529
299
  }
530
300
  ```
531
301
 
532
- **Methods:**
302
+ ---
533
303
 
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 |
304
+ ## Services
542
305
 
543
- ### PermissionValidatorService
306
+ ### FileUrlService
544
307
 
545
- Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM. Supports **wildcard permissions**.
308
+ **CRITICAL:** Always use `FileUrlService` to fetch file URLs. Never construct URLs manually. FLUSYS supports S3, Azure, and SFTP with presigned URLs that expire — only the backend knows the current URL.
546
309
 
547
310
  ```typescript
548
- import { PermissionValidatorService } from '@flusys/ng-shared';
549
-
550
- @Component({...})
551
- export class MyComponent {
552
- private readonly permissionValidator = inject(PermissionValidatorService);
553
-
554
- ngOnInit() {
555
- // Set permissions (typically done by IAM PermissionStateService)
556
- this.permissionValidator.setPermissions(['user.view', 'user.create']);
311
+ import { FileUrlService } from '@flusys/ng-shared';
557
312
 
558
- // Check single permission
559
- if (this.permissionValidator.hasPermission('user.view')) {
560
- // User has permission
561
- }
313
+ @Component({ ... })
314
+ export class AvatarComponent {
315
+ private fileUrlService = inject(FileUrlService);
316
+ avatarUrl = signal<string | null>(null);
562
317
 
563
- // Check loaded state (signal-based)
564
- if (this.permissionValidator.isLoaded()) {
565
- // Permissions have been loaded
566
- }
318
+ async loadAvatar(fileId: string): Promise<void> {
319
+ const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
320
+ this.avatarUrl.set(file?.url ?? null);
567
321
  }
568
322
  }
569
323
  ```
570
324
 
571
- **Methods:**
572
-
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 |
579
-
580
- **Signals:**
581
-
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
325
  ```typescript
592
- // Exact match
593
- hasPermission("user.read"); // true if permissions include 'user.read'
326
+ // WRONG — never construct file URLs manually
327
+ const url = `${apiBaseUrl}/storage/${fileId}`;
594
328
 
595
- // Global wildcard
596
- hasPermission("user.read"); // true if permissions include '*'
597
-
598
- // Module wildcard
599
- hasPermission("user.read"); // true if permissions include 'user.*'
329
+ // CORRECT — always use FileUrlService
330
+ const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
331
+ const url = file?.url ?? null;
600
332
  ```
601
333
 
602
- ### CookieService
603
-
604
- SSR-aware cookie reading service.
605
-
606
- ```typescript
607
- import { CookieService } from "@flusys/ng-shared";
334
+ **Methods:**
608
335
 
609
- const cookies = inject(CookieService).get(); // Returns document.cookie (browser) or request header cookie (server)
610
- ```
336
+ | Method | Description |
337
+ |--------|-------------|
338
+ | `fetchSingleFileUrl(fileId)` | Fetch presigned URL for a single file |
339
+ | `fetchMultipleFileUrls(fileIds)` | Batch fetch presigned URLs |
340
+ | `clearCache(fileId?)` | Invalidate cached URL(s) |
611
341
 
612
- ### PlatformService
342
+ ### PermissionValidatorService
613
343
 
614
- SSR environment detection service.
344
+ Client-side permission evaluation without direct ng-iam dependency.
615
345
 
616
346
  ```typescript
617
- import { PlatformService } from "@flusys/ng-shared";
347
+ import { PermissionValidatorService } from '@flusys/ng-shared';
348
+
349
+ @Component({ ... })
350
+ export class MyComponent {
351
+ private permValidator = inject(PermissionValidatorService);
618
352
 
619
- const platform = inject(PlatformService);
620
- if (!platform.isServer) {
621
- // Browser-only code (localStorage, window, etc.)
353
+ canEdit = computed(() =>
354
+ this.permValidator.hasPermission('product:update')
355
+ );
622
356
  }
623
357
  ```
624
358
 
625
359
  ---
626
360
 
627
- ## 5. Components
628
-
629
- ### IconComponent
361
+ ## Reusable Components
630
362
 
631
- Flexible icon renderer supporting PrimeNG icons, image files, and SVG.
363
+ ### LazySelectComponent
632
364
 
633
- - **Selector:** `lib-icon`
634
- - **Inputs:** `icon` (required string), `iconType` (optional `IconTypeEnum`, default: `PRIMENG_ICON`)
365
+ Virtualized dropdown with server-side search. Ideal for large datasets.
635
366
 
636
367
  ```html
637
- <!-- PrimeNG icon (default) -->
638
- <lib-icon icon="pi pi-user" />
639
-
640
- <!-- Image file -->
641
- <lib-icon icon="/assets/logo.png" [iconType]="IconTypeEnum.IMAGE_FILE_LINK" />
642
-
643
- <!-- SVG tag (shows fallback icon - full SVG rendering coming soon) -->
644
- <lib-icon icon="<svg>...</svg>" [iconType]="IconTypeEnum.DIRECT_TAG_SVG" />
368
+ <flusys-lazy-select
369
+ [apiService]="productApiService"
370
+ [labelField]="'name'"
371
+ [valueField]="'id'"
372
+ [(ngModel)]="selectedProductId"
373
+ placeholder="Search products..."
374
+ />
645
375
  ```
646
376
 
647
- ### LazySelectComponent
648
-
649
- Single-select dropdown with lazy loading, search, and scroll pagination.
377
+ ### FileSelectorDialogComponent
650
378
 
651
- - **Selector:** `lib-lazy-select`
652
- - **Extends:** `BaseFormControl<string | null>`
653
- - **Supports:** Template-driven, reactive forms, signal forms
379
+ File picker dialog that integrates with `@flusys/ng-storage`.
654
380
 
655
- ```typescript
656
- @Component({
657
- imports: [LazySelectComponent],
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)" /> `,
659
- })
660
- export class MyComponent {
661
- readonly selectedId = signal<string | null>(null);
662
- readonly items = signal<IDropDown[]>([]);
663
- readonly loading = signal(false);
664
- readonly total = signal<number | undefined>(undefined);
665
- readonly pagination = signal<IPagination>({ currentPage: 0, pageSize: 20 });
666
- }
381
+ ```html
382
+ <flusys-file-selector
383
+ [accept]="'image/*'"
384
+ [maxSize]="5242880"
385
+ (fileSelected)="onFileSelected($event)"
386
+ />
667
387
  ```
668
388
 
669
- **Inputs:**
389
+ ---
670
390
 
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'`) |
391
+ ## Directives
681
392
 
682
- **Model:** `value` - Two-way bound selected value (`string | null`)
393
+ ### HasPermissionDirective
683
394
 
684
- **Outputs:** `onSearch` (debounced 500ms), `onPagination` (scroll-triggered)
395
+ Structural directive that removes elements from the DOM if the user lacks the required permission:
685
396
 
686
- ### LazyMultiSelectComponent
397
+ ```html
398
+ <!-- Single permission -->
399
+ <button *hasPermission="'product:delete'">Delete</button>
687
400
 
688
- Multi-select dropdown with lazy loading, search, select-all, and scroll pagination.
401
+ <!-- Multiple permissions (AND logic) -->
402
+ <div *hasPermission="['product:update', 'product:read']">Edit Panel</div>
689
403
 
690
- - **Selector:** `lib-lazy-multi-select`
691
- - **Extends:** `BaseFormControl<string[] | null>`
692
- - **Supports:** Template-driven, reactive forms, signal forms
404
+ <!-- OR logic -->
405
+ <div *hasPermission="'product:update'" [permissionOr]="true">Edit Panel</div>
693
406
 
694
- ```typescript
695
- @Component({
696
- imports: [LazyMultiSelectComponent],
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)" /> `,
698
- })
699
- export class MyComponent {
700
- readonly selectedIds = signal<string[] | null>(null);
701
- }
407
+ <!-- Show fallback content -->
408
+ <button *hasPermission="'admin:manage'; else noAccess">Admin</button>
409
+ <ng-template #noAccess>
410
+ <span>No access</span>
411
+ </ng-template>
702
412
  ```
703
413
 
704
- **Inputs:** `selectDataList`, `isEditMode`, `isLoading`, `total`, `pagination`, `placeHolder` (same as `LazySelectComponent`). Does not have `optionLabel`/`optionValue` (uses `IDropDown.label`/`IDropDown.value` directly).
705
-
706
- **Model:** `value` - Two-way bound selected values (`string[] | null`)
414
+ ---
707
415
 
708
- **Computed signals:** `selectedValueDisplay` (display text), `isSelectAll` (all items selected)
416
+ ## Pipes
709
417
 
710
- ### UserSelectComponent
418
+ ### TranslatePipe
711
419
 
712
- Single user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
420
+ Translate i18n keys with optional parameter interpolation:
713
421
 
714
- - **Selector:** `lib-user-select`
715
- - **Supports:** Template-driven, reactive forms, signal forms
422
+ ```html
423
+ <!-- Basic translation -->
424
+ <span>{{ 'common.save' | translate }}</span>
716
425
 
717
- ```typescript
718
- @Component({
719
- imports: [UserSelectComponent],
720
- template: `
721
- <!-- Simple usage - uses USER_PROVIDER internally -->
722
- <lib-user-select [(value)]="selectedUserId" [isEditMode]="true" />
723
-
724
- <!-- With custom loadUsers function -->
725
- <lib-user-select [(value)]="selectedUserId" [isEditMode]="true" [loadUsers]="customLoadUsers" />
726
- `,
727
- })
728
- export class MyComponent {
729
- readonly selectedUserId = signal<string | null>(null);
730
- }
426
+ <!-- With parameters -->
427
+ <span>{{ 'pagination.showing' | translate: { from: 1, to: 10, total: 100 } }}</span>
731
428
  ```
732
429
 
733
- **Inputs:**
734
-
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 |
743
-
744
- **Model:** `value` - Two-way bound selected user ID (`string | null`)
430
+ The pipe automatically subscribes to language change events and re-renders on switch.
745
431
 
746
- **Outputs:** `userSelected` (IUserBasicInfo | null), `onError` (Error)
747
-
748
- ### UserMultiSelectComponent
432
+ ---
749
433
 
750
- Multiple user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
434
+ ## Guards
751
435
 
752
- - **Selector:** `lib-user-multi-select`
753
- - **Supports:** Template-driven, reactive forms, signal forms
436
+ | Guard | Description |
437
+ |-------|-------------|
438
+ | `AuthenticatedGuard` | Redirects unauthenticated users to `/auth/login` |
439
+ | `GuestGuard` | Redirects authenticated users away from auth pages |
440
+ | `PermissionGuard` | Blocks route if user lacks required permission |
754
441
 
755
442
  ```typescript
756
- @Component({
757
- imports: [UserMultiSelectComponent],
758
- template: ` <lib-user-multi-select [(value)]="selectedUserIds" [isEditMode]="true" /> `,
759
- })
760
- export class MyComponent {
761
- readonly selectedUserIds = signal<string[] | null>(null);
762
- }
443
+ // app.routes.ts
444
+ export const routes: Routes = [
445
+ {
446
+ path: 'products',
447
+ canActivate: [AuthenticatedGuard],
448
+ loadComponent: () => import('./pages/product-list.component'),
449
+ },
450
+ {
451
+ path: 'admin',
452
+ canActivate: [PermissionGuard],
453
+ data: { permission: 'admin:manage' },
454
+ loadComponent: () => import('./pages/admin.component'),
455
+ },
456
+ ];
763
457
  ```
764
458
 
765
- **Inputs:** Same as `UserSelectComponent`.
766
-
767
- **Model:** `value` - Two-way bound selected user IDs (`string[] | null`)
768
-
769
- **Outputs:** `usersSelected` (IUserBasicInfo[]), `onError` (Error)
459
+ ---
770
460
 
771
- ### FileUploaderComponent
461
+ ## Modules
772
462
 
773
- Drag & drop file upload with type filtering. Pass your own `uploadFile` function.
463
+ ### AngularModule
774
464
 
775
- - **Selector:** `lib-file-uploader`
465
+ Pre-aggregated Angular common imports for use in components:
776
466
 
777
467
  ```typescript
468
+ import { AngularModule } from '@flusys/ng-shared';
469
+
778
470
  @Component({
779
- imports: [FileUploaderComponent],
780
- template: `
781
- <!-- Single image upload -->
782
- <lib-file-uploader [uploadFile]="uploadFile" [acceptTypes]="['image/*']" [multiple]="false" (fileUploaded)="onFileUploaded($event)" />
783
-
784
- <!-- Multiple document upload -->
785
- <lib-file-uploader [uploadFile]="uploadFile" [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS" [multiple]="true" [maxFiles]="5" (filesUploaded)="onFilesUploaded($event)" />
786
- `,
471
+ imports: [AngularModule],
472
+ // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterModule, etc.
787
473
  })
788
- export class MyComponent {
789
- readonly uploadService = inject(UploadService);
790
-
791
- readonly uploadFile: UploadFileFn = (file, options) => this.uploadService.uploadSingleFile(file, options);
792
- }
474
+ export class MyComponent {}
793
475
  ```
794
476
 
795
- **Inputs:**
796
-
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 |
808
-
809
- **Outputs:** `fileUploaded` (IUploadedFile), `filesUploaded` (IUploadedFile[]), `onError` (Error), `fileSelected` (File[])
810
-
811
- ### FileSelectorDialogComponent
812
-
813
- Dialog to browse and select existing files with filtering.
477
+ ### PrimeModule
814
478
 
815
- - **Selector:** `lib-file-selector-dialog`
479
+ Pre-aggregated PrimeNG component imports:
816
480
 
817
481
  ```typescript
482
+ import { PrimeModule } from '@flusys/ng-shared';
483
+
818
484
  @Component({
819
- imports: [FileSelectorDialogComponent],
820
- template: ` <lib-file-selector-dialog [(visible)]="showFileSelector" [loadFiles]="loadFiles" [acceptTypes]="['image/*']" [multiple]="false" (fileSelected)="onFileSelected($event)" /> `,
485
+ imports: [PrimeModule],
486
+ // Includes: ButtonModule, TableModule, DialogModule, InputTextModule, etc.
821
487
  })
822
- export class MyComponent {
823
- readonly showFileSelector = signal(false);
824
- readonly fileService = inject(FileManagerApiService);
825
-
826
- readonly loadFiles: LoadFilesFn = (filter) =>
827
- this.fileService.getAll(filter.search, {
828
- pagination: { currentPage: filter.page, pageSize: filter.pageSize },
829
- });
830
- }
488
+ export class MyComponent {}
831
489
  ```
832
490
 
833
- **Inputs:**
834
-
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 |
843
-
844
- **Model:** `visible` - Two-way bound dialog visibility (`boolean`)
845
-
846
- **Outputs:** `fileSelected` (IFileBasicInfo), `filesSelected` (IFileBasicInfo[]), `closed` (void), `onError` (Error)
847
-
848
491
  ---
849
492
 
850
- ## 6. Directives
851
-
852
- ### HasPermissionDirective
493
+ ## Troubleshooting
853
494
 
854
- Structural directive for permission-based rendering. Fail-closed: hides content when permissions not loaded.
495
+ **`No provider for USER_PROVIDER`**
855
496
 
856
- - **Selector:** `[hasPermission]`
857
- - **Input:** `hasPermission` - `string | ILogicNode | null`
497
+ You need to register the auth providers in `app.config.ts`:
858
498
 
859
499
  ```typescript
860
- import { HasPermissionDirective, ILogicNode } from "@flusys/ng-shared";
861
-
862
- @Component({
863
- imports: [HasPermissionDirective],
864
- template: `
865
- <!-- Simple permission check -->
866
- <button *hasPermission="'user.create'">Create User</button>
867
-
868
- <!-- Complex AND/OR logic -->
869
- <div *hasPermission="editLogic">Edit Panel</div>
870
- `,
871
- })
872
- export class MyComponent {
873
- readonly editLogic: ILogicNode = {
874
- type: "group",
875
- operator: "AND",
876
- children: [
877
- { type: "action", actionId: "user.view" },
878
- { type: "action", actionId: "user.update" },
879
- ],
880
- };
881
- }
882
- ```
883
-
884
- ### EditModeElementChangerDirective
885
-
886
- Toggles readonly/disabled state for form controls based on edit mode. Supports `<input>`, `<p-select>`, and `<p-calendar>`.
887
-
888
- - **Selector:** `[appEditModeElementChanger]`
889
- - **Input:** `isEditMode` (required boolean)
500
+ import { provideAuthProviders } from '@flusys/ng-auth';
890
501
 
891
- ```html
892
- <input [appEditModeElementChanger] [isEditMode]="isEditing()" /> <p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
502
+ providers: [
503
+ ...provideAuthProviders(),
504
+ ]
893
505
  ```
894
506
 
895
- ### IsEmptyImageDirective
507
+ **`FileUrlService returns null for valid fileId`**
896
508
 
897
- Automatically replaces broken or empty image `src` with a default fallback image.
509
+ The file may not exist or the storage module isn't registered:
898
510
 
899
- - **Selector:** `img` (applies to all `<img>` elements)
900
- - **Input:** `src` (standard img src)
901
- - **Default image:** `lib/assets/images/default/default-image.jpg`
902
-
903
- ```html
904
- <img [src]="imageUrl()" alt="Product" />
905
- <!-- Falls back to default image on error or empty src -->
511
+ ```typescript
512
+ // Ensure storage is enabled in APP_CONFIG
513
+ services: {
514
+ storage: { enabled: true }
515
+ }
906
516
  ```
907
517
 
908
- ### PreventDefaultDirective
518
+ **`TranslatePipe shows raw key (e.g., "common.save")`**
909
519
 
910
- Prevents default browser behavior on specified events and emits the event.
520
+ Localization isn't initialized or the key doesn't exist in translation files. Provide `TRANSLATE_ADAPTER` in `app.config.ts` via `provideLocalization()`.
911
521
 
912
- - **Selector:** `[appPreventDefault]`
913
- - **Inputs:** `eventType` (`'click' | 'keydown' | 'keyup'`, default: `'click'`), `preventKey` (optional key filter)
914
- - **Output:** `action` - Emits the prevented event
522
+ **`HasPermissionDirective always hides content`**
915
523
 
916
- ```html
917
- <a href="#" appPreventDefault (action)="handleClick($event)">Click me</a> <input appPreventDefault eventType="keydown" preventKey="Enter" (action)="onEnter($event)" />
918
- ```
524
+ `PermissionValidatorService` has no data — ensure `ng-iam` is enabled and permissions are loaded after login.
919
525
 
920
526
  ---
921
527
 
922
- ## 7. Guards
923
-
924
- Route-level guards for permission-based access control. All guards deny access when permissions are not loaded (fail-closed).
925
-
926
- ### permissionGuard
927
-
928
- Single permission or complex logic check.
929
-
930
- ```typescript
931
- import { permissionGuard } from "@flusys/ng-shared";
932
-
933
- const routes: Routes = [
934
- // Simple permission
935
- { path: "users", canActivate: [permissionGuard("user.view")] },
936
-
937
- // Complex logic (ILogicNode)
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
- }),
949
- ],
950
- },
951
-
952
- // Custom redirect on deny
953
- { path: "settings", canActivate: [permissionGuard("settings.view", "/access-denied")] },
954
- ];
955
- ```
956
-
957
- ### anyPermissionGuard
958
-
959
- OR logic - allows access if user has ANY of the specified permissions.
960
-
961
- ```typescript
962
- { path: 'reports', canActivate: [anyPermissionGuard(['report.view', 'report.export'])] }
963
- ```
964
-
965
- ### allPermissionsGuard
966
-
967
- AND logic - allows access only if user has ALL specified permissions.
968
-
969
- ```typescript
970
- { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
971
- ```
972
-
973
- ---
974
-
975
- ## 8. Utilities
976
-
977
- ### Permission Evaluator
978
-
979
- Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards. **Supports wildcard permissions.**
980
-
981
- ```typescript
982
- import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions, hasPermission } from "@flusys/ng-shared";
983
-
984
- const userPermissions = ["user.view", "user.create", "admin.*"];
985
-
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)
990
-
991
- // Evaluate string or ILogicNode
992
- evaluatePermission("user.view", userPermissions); // true
993
- evaluatePermission(null, userPermissions); // false
994
-
995
- // Evaluate ILogicNode tree recursively
996
- evaluateLogicNode(logicNode, userPermissions);
997
-
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
- }
1053
- ```
1054
-
1055
- **Interface:** `ScrollPaginationConfig` - `{ threshold?, pagination, total, isLoading }`
1056
-
1057
- **Returns:** `IPagination | null` - Next page pagination or null if not needed.
1058
-
1059
- ---
1060
-
1061
- ## 9. Classes
1062
-
1063
- ### BaseFormControl
1064
-
1065
- Abstract base class for custom form controls. Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
1066
-
1067
- ```typescript
1068
- import { BaseFormControl, provideValueAccessor } from "@flusys/ng-shared";
1069
-
1070
- @Component({
1071
- selector: "my-select",
1072
- providers: [provideValueAccessor(MySelectComponent)],
1073
- })
1074
- export class MySelectComponent extends BaseFormControl<string | null> {
1075
- override readonly value = model<string | null>(null);
1076
-
1077
- constructor() {
1078
- super();
1079
- this.initializeFormControl(); // Required for ControlValueAccessor sync
1080
- }
1081
- }
1082
-
1083
- // Usage in all form patterns:
1084
- // Template-driven: <my-select [(value)]="selectedId" />
1085
- // Reactive forms: <my-select [formControl]="myControl" />
1086
- // Signal forms: <my-select [formField]="formTree.myField" />
1087
- ```
1088
-
1089
- **Abstract property:** `value: ModelSignal<T>` - Must override with `model<T>()`
1090
-
1091
- **Models:** `disabled` (boolean), `touched` (boolean)
1092
-
1093
- **Methods:** `initializeFormControl()` (call in constructor), `markAsTouched()` (call on blur)
1094
-
1095
- **Helper:** `provideValueAccessor(ComponentClass)` - Factory for `NG_VALUE_ACCESSOR` provider
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
-
1166
- ---
1167
-
1168
- ## 10. Modules
1169
-
1170
- ### AngularModule
1171
-
1172
- Re-exports common Angular modules for convenience.
1173
-
1174
- ```typescript
1175
- import { AngularModule } from "@flusys/ng-shared";
1176
- // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet,
1177
- // RouterLinkActive, NgOptimizedImage, NgComponentOutlet, + directives (IsEmptyImageDirective, PreventDefaultDirective)
1178
- // Providers: DatePipe
1179
- ```
1180
-
1181
- ### PrimeModule
1182
-
1183
- Re-exports PrimeNG component modules for convenience.
1184
-
1185
- ```typescript
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
1197
- ```
1198
-
1199
- ---
1200
-
1201
- ## 11. Provider Interfaces (Package Independence)
1202
-
1203
- ng-shared defines **provider interfaces** to enable feature packages (ng-iam, ng-storage) to access auth functionality without direct dependencies.
1204
-
1205
- ### Architecture
1206
-
1207
- ```
1208
- ng-shared (defines interfaces + tokens)
1209
- |
1210
- ng-auth (implements interfaces with adapters)
1211
- |
1212
- ng-iam/ng-storage (consume interfaces via DI)
1213
- ```
1214
-
1215
- ### Available Providers
1216
-
1217
- #### IUserProvider / `USER_PROVIDER`
1218
-
1219
- User list access for IAM user selection.
1220
-
1221
- ```typescript
1222
- interface IUserBasicInfo {
1223
- id: string;
1224
- name: string;
1225
- email: string;
1226
- }
1227
-
1228
- interface IUserProvider {
1229
- getUsers(filter?: { page?: number; pageSize?: number; search?: string; companyId?: string; branchId?: string }): Observable<IListResponse<IUserBasicInfo>>;
1230
- }
1231
- ```
1232
-
1233
- **Token Error Message:** `'USER_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1234
-
1235
- #### ICompanyApiProvider / `COMPANY_API_PROVIDER`
1236
-
1237
- Company list access for IAM company selection.
1238
-
1239
- ```typescript
1240
- interface ICompanyBasicInfo {
1241
- id: string;
1242
- name: string;
1243
- slug?: string;
1244
- }
1245
-
1246
- interface ICompanyApiProvider {
1247
- getCompanies(filter?: { page?: number; pageSize?: number; search?: string }): Observable<IListResponse<ICompanyBasicInfo>>;
1248
- }
1249
- ```
1250
-
1251
- **Token Error Message:** `'COMPANY_API_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1252
-
1253
- #### IUserPermissionProvider / `USER_PERMISSION_PROVIDER`
1254
-
1255
- User permission queries for IAM.
1256
-
1257
- ```typescript
1258
- interface IUserBranchPermission {
1259
- branchId: string;
1260
- branchName: string;
1261
- permissions: string[];
1262
- }
1263
-
1264
- interface IUserPermissionProvider {
1265
- getUserBranchPermissions(userId: string): Observable<ISingleResponse<IUserBranchPermission[]>>;
1266
- }
1267
- ```
1268
-
1269
- **Token Error Message:** `'USER_PERMISSION_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1270
-
1271
- #### IFileProvider / `FILE_PROVIDER`
1272
-
1273
- File operations provider for file selection and upload. Implemented by ng-storage. **Optional token** - use with `inject(..., { optional: true })`.
1274
-
1275
- ```typescript
1276
- interface IFileProvider {
1277
- loadFiles(filter: IFileSelectFilter): Observable<IListResponse<IFileBasicInfo>>;
1278
- uploadFile(file: File, options?: IFileUploadOptions): Observable<ISingleResponse<IUploadedFile>>;
1279
- uploadMultipleFiles?(files: File[], options?: IFileUploadOptions): Observable<ISingleResponse<IUploadedFile[]>>;
1280
- getFileUrls?(fileIds: string[]): Observable<ISingleResponse<IFileBasicInfo[]>>;
1281
- loadFolders?(filter: ISelectFilter): Observable<IListResponse<IFolderBasicInfo>>;
1282
- loadStorageConfigs?(filter: ISelectFilter): Observable<IListResponse<IStorageConfigBasicInfo>>;
1283
- }
1284
- ```
1285
-
1286
- #### IProfilePermissionProvider / `PROFILE_PERMISSION_PROVIDER`
1287
-
1288
- User permission queries for ng-auth profile page. Implemented by ng-iam. **Optional token** - use with `inject(..., { optional: true })`.
1289
-
1290
- ```typescript
1291
- interface IProfileRoleInfo {
1292
- id: string;
1293
- name: string;
1294
- description?: string | null;
1295
- }
1296
-
1297
- interface IProfileActionInfo {
1298
- id: string;
1299
- code: string;
1300
- name: string;
1301
- description?: string | null;
1302
- }
1303
-
1304
- interface IProfilePermissionProvider {
1305
- getUserRoles(userId: string, branchId?: string): Observable<ISingleResponse<IProfileRoleInfo[]>>;
1306
- getUserActions(userId: string, branchId?: string): Observable<ISingleResponse<IProfileActionInfo[]>>;
1307
- }
1308
- ```
1309
-
1310
- #### IAuthStateProvider / `AUTH_STATE_PROVIDER`
1311
-
1312
- Auth state access for feature packages (form-builder, etc.) that need to check authentication without depending on ng-auth directly.
1313
-
1314
- ```typescript
1315
- interface IAuthStateProvider {
1316
- /** Signal indicating if user is currently authenticated */
1317
- isAuthenticated: Signal<boolean>;
1318
-
1319
- /** Initialize auth state (restore session from cookies/storage) */
1320
- initialize(): Observable<void>;
1321
- }
1322
- ```
1323
-
1324
- **Token Error Message:** `'AUTH_STATE_PROVIDER not configured. Please provide an implementation in app.config.ts'`
1325
-
1326
- **Usage:**
1327
-
1328
- ```typescript
1329
- import { AUTH_STATE_PROVIDER } from '@flusys/ng-shared';
1330
-
1331
- @Component({...})
1332
- export class PublicFormComponent {
1333
- private readonly authState = inject(AUTH_STATE_PROVIDER);
1334
-
1335
- ngOnInit() {
1336
- this.authState.initialize().subscribe(() => {
1337
- if (this.authState.isAuthenticated()) {
1338
- // User is logged in
1339
- }
1340
- });
1341
- }
1342
- }
1343
- ```
1344
-
1345
- #### IUserListProvider / `USER_LIST_PROVIDER`
1346
-
1347
- Extends user list pages with extra columns, actions, and data enrichment. **Optional token** - use with `inject(..., { optional: true })`.
1348
-
1349
- ```typescript
1350
- interface IUserListItem {
1351
- id: string;
1352
- name: string;
1353
- email: string;
1354
- phone?: string;
1355
- isActive?: boolean;
1356
- }
1357
-
1358
- interface IUserListAction<T = IUserListItem> {
1359
- id: string;
1360
- label: string;
1361
- icon?: string;
1362
- severity?: "primary" | "secondary" | "success" | "info" | "warn" | "danger";
1363
- permission?: string;
1364
- tooltip?: string;
1365
- disabled?: boolean | ((user: T) => boolean);
1366
- visible?: boolean | ((user: T) => boolean);
1367
- }
1368
-
1369
- interface IUserListColumn {
1370
- field: string;
1371
- header: string;
1372
- width?: string;
1373
- sortable?: boolean;
1374
- templateType?: "text" | "badge" | "date" | "boolean" | "custom";
1375
- }
1376
-
1377
- interface IUserListFilter {
1378
- page?: number;
1379
- pageSize?: number;
1380
- search?: string;
1381
- isActive?: boolean;
1382
- companyId?: string;
1383
- branchId?: string;
1384
- }
1385
-
1386
- interface IUserListProvider<T extends IUserListItem = IUserListItem> {
1387
- getExtraColumns?(): IUserListColumn[];
1388
- getExtraRowActions?(): IUserListAction<T>[];
1389
- getExtraToolbarActions?(): IUserListAction<T>[];
1390
- onRowAction?(actionId: string, user: T): void;
1391
- onToolbarAction?(actionId: string, selectedUsers: T[]): void;
1392
- enrichListData?(users: T[]): Observable<T[]>;
1393
- }
1394
- ```
1395
-
1396
- **Usage:**
1397
-
1398
- ```typescript
1399
- // In app.config.ts
1400
- providers: [{ provide: USER_LIST_PROVIDER, useClass: MyUserListProvider }];
1401
-
1402
- // Implementation
1403
- @Injectable({ providedIn: "root" })
1404
- export class MyUserListProvider implements IUserListProvider {
1405
- getExtraRowActions() {
1406
- return [{ id: "assign-role", label: "Assign Role", icon: "pi pi-users" }];
1407
- }
1408
- }
1409
- ```
1410
-
1411
- ### Usage in Consuming Packages
1412
-
1413
- ```typescript
1414
- import { USER_PROVIDER } from "@flusys/ng-shared";
1415
-
1416
- export class UserSelectorComponent {
1417
- private readonly userProvider = inject(USER_PROVIDER);
1418
-
1419
- loadUsers() {
1420
- this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe((response) => {
1421
- this.users.set(response.data ?? []);
1422
- });
1423
- }
1424
- }
1425
- ```
1426
-
1427
- ### Wiring in App
1428
-
1429
- ```typescript
1430
- // app.config.ts
1431
- import { provideAuthProviders } from "@flusys/ng-auth";
1432
-
1433
- export const appConfig: ApplicationConfig = {
1434
- providers: [
1435
- ...provideAuthProviders(), // Registers all auth adapters for provider tokens
1436
- ],
1437
- };
1438
- ```
1439
-
1440
- ### See Also
1441
-
1442
- - [AUTH-GUIDE.md](AUTH-GUIDE.md) - Adapter implementations
1443
- - [IAM-GUIDE.md](IAM-GUIDE.md) - IAM usage examples
1444
- - [../CLAUDE.md](../CLAUDE.md) - Complete pattern documentation
1445
-
1446
- ---
1447
-
1448
- ## Best Practices
1449
-
1450
- ### API Services
1451
-
1452
- - **Extend `ApiResourceService`** for new services (signal-based)
1453
- - Use reactive signals (`data`, `isLoading`, `total`) in templates
1454
- - Use `fetchList()` to trigger queries (also initializes resource), `reload()` to refresh
1455
- - Use async methods (`insertAsync`, `updateAsync`) for one-off operations
1456
- - Resource is lazy-initialized - no HTTP requests until first `fetchList()` or `initListResource()`
1457
-
1458
- ### File URLs
1459
-
1460
- - **Always use `FileUrlService`** - never construct URLs manually
1461
- - Use `fetchSingleFileUrl()` for one-off fetches, `fetchFileUrls()` for batches
1462
- - Use `fileUrlSignal()` for reactive template bindings
1463
-
1464
- ### Components
1465
-
1466
- - Use `AngularModule` and `PrimeModule` for common imports
1467
- - Use signal inputs via `input()` and `input.required()`
1468
- - Components use `lib-` prefix for selectors
1469
-
1470
- ### Directives
1471
-
1472
- - Directives use `app` prefix for selectors (`appPreventDefault`, `appEditModeElementChanger`)
1473
- - Exception: `HasPermissionDirective` uses `[hasPermission]`
1474
- - Exception: `IsEmptyImageDirective` uses `img` selector (auto-applies to all images)
1475
-
1476
- ### Permissions
1477
-
1478
- - Use `HasPermissionDirective` for template-level permission checks
1479
- - Use permission guards for route-level access control
1480
- - Use `PermissionValidatorService` for programmatic checks in services
1481
- - Permissions follow fail-closed model: no access by default
1482
- - Wildcards supported: `*` (all), `module.*` (module-scoped)
1483
-
1484
- ---
1485
-
1486
- ## Common Issues
1487
-
1488
- ### ApiResourceService Not Updating UI
1489
-
1490
- **Problem:** UI doesn't update after operations.
1491
-
1492
- **Solution:** Use signal syntax in templates:
1493
-
1494
- ```html
1495
- <!-- Correct -->
1496
- <div>{{ users() }}</div>
1497
-
1498
- <!-- Wrong -->
1499
- <div>{{ users }}</div>
1500
- ```
1501
-
1502
- ### FileUrlService Returns Error
1503
-
1504
- **Problem:** File URL fetching fails.
1505
-
1506
- **Solution:**
1507
-
1508
- 1. Ensure storage service is enabled in environment config
1509
- 2. Verify file ID exists in database
1510
- 3. Check storage provider configuration (local/S3/Azure)
1511
-
1512
- ### Circular Dependency with ng-layout
1513
-
1514
- **Problem:** Build fails with circular dependency.
1515
-
1516
- **Solution:** ng-shared must NEVER import from ng-layout. Move shared components to ng-shared, layout-specific components to ng-layout.
1517
-
1518
- ### Provider Token Errors
1519
-
1520
- **Problem:** `'XXX_PROVIDER not configured'` error at runtime.
1521
-
1522
- **Solution:** Ensure the provider is registered in `app.config.ts`:
1523
-
1524
- ```typescript
1525
- // Required providers
1526
- providers: [
1527
- { provide: USER_PROVIDER, useClass: AuthUserProvider },
1528
- { provide: COMPANY_API_PROVIDER, useClass: AuthCompanyApiProvider },
1529
- { provide: USER_PERMISSION_PROVIDER, useClass: AuthUserPermissionProvider },
1530
- { provide: AUTH_STATE_PROVIDER, useClass: AuthStateProviderAdapter },
1531
- ];
1532
-
1533
- // OR use the convenience function
1534
- providers: [...provideAuthProviders()];
1535
- ```
1536
-
1537
- ### Permissions Not Working
1538
-
1539
- **Problem:** `hasPermission()` returns false even though user should have access.
1540
-
1541
- **Solution:**
1542
-
1543
- 1. Check if permissions are loaded: `permissionValidator.isLoaded()`
1544
- 2. Verify permission codes match exactly (case-sensitive)
1545
- 3. For wildcard access, ensure user has `*` or `module.*` in their permissions
1546
-
1547
- ---
1548
-
1549
- ## API Reference
1550
-
1551
- ### Services
1552
-
1553
- | Service | Description |
1554
- | ---------------------------- | ---------------------------------------------------------------------------------------- |
1555
- | `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API (lazy-initialized, accepts optional `serviceName`) |
1556
- | `FileUrlService` | Cloud storage URL fetching |
1557
- | `PermissionValidatorService` | Permission state management with wildcards |
1558
- | `CookieService` | SSR-aware cookie reading |
1559
- | `PlatformService` | SSR environment detection |
1560
-
1561
- ### Classes
1562
-
1563
- | Class | Description |
1564
- | -------------------- | -------------------------------------------------- |
1565
- | `ApiResourceService` | Signal-based CRUD base class (alias: `ApiService`) |
1566
- | `BaseFormControl` | Abstract base for custom form controls |
1567
- | `BaseFormPage` | Abstract directive for create/edit pages |
1568
- | `BaseListPage` | Abstract directive for list pages |
1569
-
1570
- ### Constants
1571
-
1572
- | Constant | Description |
1573
- | ------------------- | ----------------------------------------------------- |
1574
- | `PERMISSIONS` | Aggregated permission codes by module |
1575
- | `USER_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for users |
1576
- | `ROLE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for roles |
1577
- | `FILE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for files |
1578
- | `FILE_TYPE_FILTERS` | Predefined MIME type arrays (IMAGES, DOCUMENTS, etc.) |
1579
-
1580
- ### Components
1581
-
1582
- | Component | Selector | Description |
1583
- | ----------------------------- | -------------------------- | ------------------------- |
1584
- | `IconComponent` | `lib-icon` | Flexible icon renderer |
1585
- | `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
1586
- | `LazyMultiSelectComponent` | `lib-lazy-multi-select` | Lazy-loading multi-select |
1587
- | `UserSelectComponent` | `lib-user-select` | Single user selector |
1588
- | `UserMultiSelectComponent` | `lib-user-multi-select` | Multiple user selector |
1589
- | `FileUploaderComponent` | `lib-file-uploader` | Drag & drop file upload |
1590
- | `FileSelectorDialogComponent` | `lib-file-selector-dialog` | File browser dialog |
1591
-
1592
- ### Directives
1593
-
1594
- | Directive | Selector | Description |
1595
- | --------------------------------- | ----------------------------- | --------------------------------- |
1596
- | `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
1597
- | `EditModeElementChangerDirective` | `[appEditModeElementChanger]` | Toggle edit mode on form controls |
1598
- | `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
1599
- | `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
1600
-
1601
- ### Guards
1602
-
1603
- | Guard | Description |
1604
- | --------------------- | ------------------------------------- |
1605
- | `permissionGuard` | Single permission or ILogicNode check |
1606
- | `anyPermissionGuard` | OR logic (any of listed permissions) |
1607
- | `allPermissionsGuard` | AND logic (all of listed permissions) |
1608
-
1609
- ### Interfaces
1610
-
1611
- | Interface | Description |
1612
- | ----------------------- | ------------------------------------------------------------------------------ |
1613
- | `IBaseEntity` | Base entity with ID and timestamps |
1614
- | `ILoggedUserInfo` | Current user info with company ctx |
1615
- | `IFilterData` | Filter, pagination, sort payload |
1616
- | `IFilter` | Filter object (supports arrays) |
1617
- | `IDeleteData` | Delete request payload |
1618
- | `IDropDown` | Simple label/value pair |
1619
- | `ISingleResponse<T>` | Single item response |
1620
- | `IListResponse<T>` | List with pagination |
1621
- | `IBulkResponse<T>` | Bulk operation response |
1622
- | `IMessageResponse` | Message-only response |
1623
- | `IErrorResponse` | Error with validation details |
1624
- | `ILogicNode` | Permission logic tree (AND/OR nodes) |
1625
- | `IUserSelectFilter` | User select filter params |
1626
- | `LoadUsersFn` | User loading function type |
1627
- | `IFileBasicInfo` | Basic file info for selectors |
1628
- | `IFolderBasicInfo` | Folder info for selectors |
1629
- | `IStorageConfigBasicInfo` | Storage config info for selectors |
1630
- | `IFileUploadOptions` | Upload options (compression, etc.) |
1631
- | `IUploadedFile` | Uploaded file response |
1632
- | `IFileSelectFilter` | File select filter params |
1633
- | `ISelectFilter` | Filter params for folder/config selectors |
1634
- | `LoadFilesFn` | File loading function type |
1635
- | `UploadFileFn` | Single file upload function type |
1636
- | `UploadMultipleFilesFn` | Multiple files upload function type |
1637
- | `IFileProvider` | File operations provider interface |
1638
- | `FilesResponseDto` | File URL service response |
1639
- | `IAuthStateProvider` | Auth state provider interface |
1640
- | `IUserListProvider` | User list extensions provider |
1641
- | `IUserListItem` | Base user for list operations |
1642
- | `IUserListAction` | User list action definition |
1643
- | `IUserListColumn` | Extra column for user list |
1644
- | `IUserListFilter` | User list filter parameters |
1645
- | `IUserBranchPermission` | User permissions per branch |
1646
- | `ServiceName` | `'auth' \| 'administration' \| 'iam' \| 'storage' \| 'formBuilder' \| 'email'` |
1647
-
1648
- ### Injection Tokens
1649
-
1650
- | Token | Interface | Optional | Description |
1651
- | ----------------------------- | ---------------------------- | -------- | ------------------------------------- |
1652
- | `USER_PROVIDER` | `IUserProvider` | No | User list for IAM |
1653
- | `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | No | Company list for IAM |
1654
- | `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | No | User permission queries |
1655
- | `AUTH_STATE_PROVIDER` | `IAuthStateProvider` | No | Auth state for feature packages |
1656
- | `FILE_PROVIDER` | `IFileProvider` | Yes | File operations (ng-storage) |
1657
- | `PROFILE_PERMISSION_PROVIDER` | `IProfilePermissionProvider` | Yes | User permissions for profile (ng-iam) |
1658
- | `USER_LIST_PROVIDER` | `IUserListProvider` | Yes | User list extensions |
1659
-
1660
- ---
1661
-
1662
- ## 10. Translation & Localization
1663
-
1664
- ### Overview
1665
-
1666
- The ng-shared package provides optional localization support via two modes:
1667
-
1668
- 1. **Fallback-Only Mode** - Hardcoded fallback messages (no API)
1669
- 2. **Full API Mode** - Dynamic translations from API with fallback safety net
1670
-
1671
- ### Fallback-Only Mode
1672
-
1673
- Use when you want simple hardcoded translations without a localization system:
1674
-
1675
- ```typescript
1676
- // app.config.ts
1677
- import { provideFallbackLocalization } from '@flusys/ng-shared';
1678
-
1679
- export const appConfig: ApplicationConfig = {
1680
- providers: [
1681
- // ... other providers
1682
- ...provideFallbackLocalization(), // ← One-liner setup
1683
- ],
1684
- };
1685
- ```
1686
-
1687
- **What it provides:**
1688
- - `FALLBACK_MESSAGES_REGISTRY` - Token for storing hardcoded messages
1689
- - `TRANSLATE_ADAPTER` - Fallback implementation that reads from registry
1690
- - Works with route resolvers that register fallback messages
1691
-
1692
- **Message flow:**
1693
- ```
1694
- Route with resolveTranslationModule({
1695
- modules: ['email'],
1696
- fallbackMessages: EMAIL_MESSAGES
1697
- })
1698
-
1699
- Resolver registers in FALLBACK_MESSAGES_REGISTRY
1700
-
1701
- TRANSLATE_ADAPTER reads from registry
1702
-
1703
- Components/TranslatePipe render values
1704
- ```
1705
-
1706
- ### Full API Mode
1707
-
1708
- Use when you need dynamic translations, language switching, and management UI:
1709
-
1710
- ```typescript
1711
- // app.config.ts
1712
- import { provideLocalization, getLocalizationConfig } from '@flusys/ng-localization';
1713
-
1714
- export const appConfig: ApplicationConfig = {
1715
- providers: [
1716
- // ... other providers
1717
- ...provideLocalization(
1718
- getLocalizationConfig({
1719
- defaultLanguageCode: 'en',
1720
- enableLayoutSelector: true, // Show language switcher
1721
- })
1722
- ),
1723
- ],
1724
- };
1725
- ```
1726
-
1727
- **What it provides:**
1728
- - `LocalizationStateService` - Manages current language and translations
1729
- - `LocalizationApiService` - Fetches translations from backend
1730
- - `TranslateService` - Registered as `TRANSLATE_ADAPTER`
1731
- - Language selector component (if `enableLayoutSelector: true`)
1732
- - Management pages for languages and translations
1733
-
1734
- ### Route Resolver Setup
1735
-
1736
- All routes should use `resolveTranslationModule` with fallback messages:
1737
-
1738
- ```typescript
1739
- // email.routes.ts
1740
- import { resolveTranslationModule, EMAIL_MESSAGES, SHARED_MESSAGES } from '@flusys/ng-shared';
1741
-
1742
- export const EMAIL_ROUTES: Routes = [
1743
- {
1744
- path: '',
1745
- resolve: {
1746
- translations: resolveTranslationModule({
1747
- modules: ['email'],
1748
- fallbackMessages: { ...EMAIL_MESSAGES, ...SHARED_MESSAGES }
1749
- })
1750
- },
1751
- // ... route definition
1752
- }
1753
- ];
1754
- ```
1755
-
1756
- ### TranslatePipe Usage
1757
-
1758
- Works identically in both modes:
1759
-
1760
- ```typescript
1761
- // Template
1762
- <h1>{{ 'email.title' | translate }}</h1>
1763
- <p>{{ 'email.message' | translate: { count: itemCount } }}</p>
1764
-
1765
- // Component
1766
- const title = this.translateAdapter.translate('email.title');
1767
- ```
1768
-
1769
- ### Configuration (Full API Mode Only)
1770
-
1771
- ```typescript
1772
- interface ILocalizationConfig {
1773
- defaultLanguageCode: string; // Required: 'en', 'ar', etc.
1774
- loadStrategy?: 'all' | 'modules'; // Default: 'modules'
1775
- initialModules?: string[]; // Default: []
1776
- enableLayoutSelector?: boolean; // Default: false
1777
- }
1778
- ```
1779
-
1780
- ### Key Files
1781
-
1782
- | File | Purpose |
1783
- |------|---------|
1784
- | `providers/fallback-localization.providers.ts` | Fallback mode setup |
1785
- | `resolvers/translation-module.resolver.ts` | Route resolver for both modes |
1786
- | `pipes/translate.pipe.ts` | Translation pipe |
1787
-
1788
- ### When to Use Each Mode
1789
-
1790
- **Use Fallback-Only:**
1791
- - Simple applications with static content
1792
- - No language switching needed
1793
- - No translation management UI
1794
- - Lightweight setup
1795
-
1796
- **Use Full API:**
1797
- - Multi-language support required
1798
- - User can switch languages
1799
- - Admin manages translations
1800
- - Real-time translation updates
1801
- - Translation management UI needed
1802
-
1803
- ## See Also
1804
-
1805
- - **[CORE-GUIDE.md](./CORE-GUIDE.md)** - Configuration, interceptors
1806
- - **[LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md)** - Layout system
1807
- - **[AUTH-GUIDE.md](./AUTH-GUIDE.md)** - Auth adapters for provider interfaces
1808
- - **[IAM-GUIDE.md](./IAM-GUIDE.md)** - IAM permission usage
1809
-
1810
- ---
528
+ ## License
1811
529
 
1812
- **Last Updated:** 2026-03-01
1813
- **Version:** 3.0.1
1814
- **Angular Version:** 21
530
+ MIT © FLUSYS