@flusys/ng-shared 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +323 -1607
- package/fesm2022/flusys-ng-shared.mjs +26 -12
- package/fesm2022/flusys-ng-shared.mjs.map +1 -1
- package/package.json +2 -2
- package/types/flusys-ng-shared.d.ts +9 -4
package/README.md
CHANGED
|
@@ -1,1814 +1,530 @@
|
|
|
1
|
-
# @flusys/ng-shared
|
|
1
|
+
# @flusys/ng-shared
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
- **Build Command:** `npm run build:ng-shared`
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/ng-shared)
|
|
6
|
+
[](https://angular.io)
|
|
7
|
+
[](https://www.typescriptlang.org)
|
|
8
|
+
[](LICENSE)
|
|
16
9
|
|
|
17
10
|
---
|
|
18
11
|
|
|
19
|
-
##
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
#### IBaseEntity
|
|
24
|
-
|
|
25
|
-
Base entity interface matching backend Identity entity.
|
|
40
|
+
---
|
|
26
41
|
|
|
27
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
---
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
## Features
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
64
|
+
---
|
|
68
65
|
|
|
69
|
-
|
|
66
|
+
## Compatibility
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
| Package | Version |
|
|
69
|
+
|---------|---------|
|
|
70
|
+
| Angular | 21+ |
|
|
71
|
+
| @flusys/ng-core | 4.x |
|
|
72
|
+
| PrimeNG | 18+ |
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
[key: string]: "ASC" | "DESC";
|
|
79
|
-
}
|
|
74
|
+
---
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
interface IFilter {
|
|
83
|
-
[key: string]: string | number | boolean | null | undefined | string[] | number[];
|
|
84
|
-
}
|
|
76
|
+
## Installation
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
#### IDeleteData
|
|
82
|
+
---
|
|
99
83
|
|
|
100
|
-
|
|
84
|
+
## Quick Start
|
|
101
85
|
|
|
102
86
|
```typescript
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
101
|
+
---
|
|
112
102
|
|
|
113
|
-
|
|
103
|
+
## Response Interfaces
|
|
114
104
|
|
|
115
|
-
|
|
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
|
-
|
|
107
|
+
### ISingleResponse\<T\>
|
|
123
108
|
|
|
124
109
|
```typescript
|
|
125
|
-
interface
|
|
126
|
-
|
|
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
|
-
|
|
115
|
+
### IListResponse\<T\>
|
|
135
116
|
|
|
136
117
|
```typescript
|
|
137
|
-
interface
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
126
|
+
### IBulkResponse\<T\>
|
|
176
127
|
|
|
177
128
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
289
|
-
|
|
290
|
-
Discriminated union for building complex permission logic trees.
|
|
155
|
+
### IListParams (Common Request Shape)
|
|
291
156
|
|
|
292
157
|
```typescript
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
##
|
|
170
|
+
## Provider Interface Pattern
|
|
313
171
|
|
|
314
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## 4. Services
|
|
369
|
-
|
|
370
|
-
### ApiResourceService
|
|
180
|
+
### Injection Tokens
|
|
371
181
|
|
|
372
|
-
|
|
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
|
-
|
|
192
|
+
### USER_PROVIDER
|
|
375
193
|
|
|
376
194
|
```typescript
|
|
377
|
-
|
|
378
|
-
|
|
195
|
+
interface IUserProvider {
|
|
196
|
+
user: Signal<ICurrentUser | null>;
|
|
197
|
+
isAuthenticated: Signal<boolean>;
|
|
198
|
+
}
|
|
379
199
|
|
|
380
|
-
|
|
200
|
+
// Consuming (in ng-iam)
|
|
201
|
+
import { USER_PROVIDER, IUserProvider } from '@flusys/ng-shared';
|
|
381
202
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
216
|
+
interface ICompanyProvider {
|
|
217
|
+
company: Signal<ICompany | null>;
|
|
218
|
+
branch: Signal<IBranch | null>;
|
|
219
|
+
companyId: Signal<string | null>;
|
|
220
|
+
}
|
|
423
221
|
```
|
|
424
222
|
|
|
425
|
-
|
|
223
|
+
### FILE_PROVIDER
|
|
426
224
|
|
|
427
225
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
232
|
+
---
|
|
442
233
|
|
|
443
|
-
|
|
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
|
-
|
|
236
|
+
### ApiResourceService
|
|
454
237
|
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
+
@Injectable({ providedIn: 'root' })
|
|
244
|
+
export class ProductApiService extends ApiResourceService<Product, CreateProductDto, UpdateProductDto> {
|
|
245
|
+
protected override resource = 'product';
|
|
246
|
+
}
|
|
468
247
|
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
257
|
+
### BaseListPage
|
|
481
258
|
|
|
482
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
readonly total = this.userService.total; // Signal<number>
|
|
261
|
+
```typescript
|
|
262
|
+
import { BaseListPage } from '@flusys/ng-shared';
|
|
490
263
|
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
### FileUrlService
|
|
505
|
-
|
|
506
|
-
Fetches file URLs from the backend. Supports presigned URLs for cloud storage (S3, Azure).
|
|
281
|
+
### BaseFormPage
|
|
507
282
|
|
|
508
|
-
|
|
283
|
+
Scaffold for create/edit forms with validation state and submission handling.
|
|
509
284
|
|
|
510
285
|
```typescript
|
|
511
|
-
import {
|
|
512
|
-
|
|
513
|
-
@Component({...})
|
|
514
|
-
export class ProductComponent {
|
|
515
|
-
private readonly fileUrlService = inject(FileUrlService);
|
|
286
|
+
import { BaseFormPage } from '@flusys/ng-shared';
|
|
516
287
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
302
|
+
---
|
|
533
303
|
|
|
534
|
-
|
|
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
|
-
###
|
|
306
|
+
### FileUrlService
|
|
544
307
|
|
|
545
|
-
|
|
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 {
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
313
|
+
@Component({ ... })
|
|
314
|
+
export class AvatarComponent {
|
|
315
|
+
private fileUrlService = inject(FileUrlService);
|
|
316
|
+
avatarUrl = signal<string | null>(null);
|
|
562
317
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
//
|
|
593
|
-
|
|
326
|
+
// ❌ WRONG — never construct file URLs manually
|
|
327
|
+
const url = `${apiBaseUrl}/storage/${fileId}`;
|
|
594
328
|
|
|
595
|
-
//
|
|
596
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
SSR-aware cookie reading service.
|
|
605
|
-
|
|
606
|
-
```typescript
|
|
607
|
-
import { CookieService } from "@flusys/ng-shared";
|
|
334
|
+
**Methods:**
|
|
608
335
|
|
|
609
|
-
|
|
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
|
-
###
|
|
342
|
+
### PermissionValidatorService
|
|
613
343
|
|
|
614
|
-
|
|
344
|
+
Client-side permission evaluation without direct ng-iam dependency.
|
|
615
345
|
|
|
616
346
|
```typescript
|
|
617
|
-
import {
|
|
347
|
+
import { PermissionValidatorService } from '@flusys/ng-shared';
|
|
348
|
+
|
|
349
|
+
@Component({ ... })
|
|
350
|
+
export class MyComponent {
|
|
351
|
+
private permValidator = inject(PermissionValidatorService);
|
|
618
352
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
353
|
+
canEdit = computed(() =>
|
|
354
|
+
this.permValidator.hasPermission('product:update')
|
|
355
|
+
);
|
|
622
356
|
}
|
|
623
357
|
```
|
|
624
358
|
|
|
625
359
|
---
|
|
626
360
|
|
|
627
|
-
##
|
|
628
|
-
|
|
629
|
-
### IconComponent
|
|
361
|
+
## Reusable Components
|
|
630
362
|
|
|
631
|
-
|
|
363
|
+
### LazySelectComponent
|
|
632
364
|
|
|
633
|
-
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
###
|
|
648
|
-
|
|
649
|
-
Single-select dropdown with lazy loading, search, and scroll pagination.
|
|
377
|
+
### FileSelectorDialogComponent
|
|
650
378
|
|
|
651
|
-
|
|
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
|
-
```
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
389
|
+
---
|
|
670
390
|
|
|
671
|
-
|
|
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
|
-
|
|
393
|
+
### HasPermissionDirective
|
|
683
394
|
|
|
684
|
-
|
|
395
|
+
Structural directive that removes elements from the DOM if the user lacks the required permission:
|
|
685
396
|
|
|
686
|
-
|
|
397
|
+
```html
|
|
398
|
+
<!-- Single permission -->
|
|
399
|
+
<button *hasPermission="'product:delete'">Delete</button>
|
|
687
400
|
|
|
688
|
-
|
|
401
|
+
<!-- Multiple permissions (AND logic) -->
|
|
402
|
+
<div *hasPermission="['product:update', 'product:read']">Edit Panel</div>
|
|
689
403
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
404
|
+
<!-- OR logic -->
|
|
405
|
+
<div *hasPermission="'product:update'" [permissionOr]="true">Edit Panel</div>
|
|
693
406
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
**Model:** `value` - Two-way bound selected values (`string[] | null`)
|
|
414
|
+
---
|
|
707
415
|
|
|
708
|
-
|
|
416
|
+
## Pipes
|
|
709
417
|
|
|
710
|
-
###
|
|
418
|
+
### TranslatePipe
|
|
711
419
|
|
|
712
|
-
|
|
420
|
+
Translate i18n keys with optional parameter interpolation:
|
|
713
421
|
|
|
714
|
-
|
|
715
|
-
|
|
422
|
+
```html
|
|
423
|
+
<!-- Basic translation -->
|
|
424
|
+
<span>{{ 'common.save' | translate }}</span>
|
|
716
425
|
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
### UserMultiSelectComponent
|
|
432
|
+
---
|
|
749
433
|
|
|
750
|
-
|
|
434
|
+
## Guards
|
|
751
435
|
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
766
|
-
|
|
767
|
-
**Model:** `value` - Two-way bound selected user IDs (`string[] | null`)
|
|
768
|
-
|
|
769
|
-
**Outputs:** `usersSelected` (IUserBasicInfo[]), `onError` (Error)
|
|
459
|
+
---
|
|
770
460
|
|
|
771
|
-
|
|
461
|
+
## Modules
|
|
772
462
|
|
|
773
|
-
|
|
463
|
+
### AngularModule
|
|
774
464
|
|
|
775
|
-
-
|
|
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: [
|
|
780
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
479
|
+
Pre-aggregated PrimeNG component imports:
|
|
816
480
|
|
|
817
481
|
```typescript
|
|
482
|
+
import { PrimeModule } from '@flusys/ng-shared';
|
|
483
|
+
|
|
818
484
|
@Component({
|
|
819
|
-
imports: [
|
|
820
|
-
|
|
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
|
-
##
|
|
851
|
-
|
|
852
|
-
### HasPermissionDirective
|
|
493
|
+
## Troubleshooting
|
|
853
494
|
|
|
854
|
-
|
|
495
|
+
**`No provider for USER_PROVIDER`**
|
|
855
496
|
|
|
856
|
-
|
|
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 {
|
|
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
|
-
|
|
892
|
-
|
|
502
|
+
providers: [
|
|
503
|
+
...provideAuthProviders(),
|
|
504
|
+
]
|
|
893
505
|
```
|
|
894
506
|
|
|
895
|
-
|
|
507
|
+
**`FileUrlService returns null for valid fileId`**
|
|
896
508
|
|
|
897
|
-
|
|
509
|
+
The file may not exist or the storage module isn't registered:
|
|
898
510
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
518
|
+
**`TranslatePipe shows raw key (e.g., "common.save")`**
|
|
909
519
|
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1813
|
-
**Version:** 3.0.1
|
|
1814
|
-
**Angular Version:** 21
|
|
530
|
+
MIT © FLUSYS
|