@flusys/ng-shared 1.1.0 → 1.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -1501
- package/fesm2022/flusys-ng-shared.mjs +3371 -2302
- package/fesm2022/flusys-ng-shared.mjs.map +1 -1
- package/package.json +9 -8
- package/types/flusys-ng-shared.d.ts +643 -1031
package/README.md
CHANGED
|
@@ -1,1575 +1,165 @@
|
|
|
1
|
-
# @flusys/ng-shared
|
|
1
|
+
# @flusys/ng-shared
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared utilities, provider tokens, base page classes, and reusable components for the FLUSYS Angular platform.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**Key Principle:** ng-shared depends only on ng-core, never on ng-layout or feature packages.
|
|
8
|
-
|
|
9
|
-
## Package Information
|
|
10
|
-
|
|
11
|
-
- **Package:** `@flusys/ng-shared`
|
|
12
|
-
- **Version:** 1.1.0
|
|
13
|
-
- **Dependencies:** ng-core
|
|
14
|
-
- **Dependents:** ng-layout, ng-auth, ng-iam, ng-storage, flusysng
|
|
15
|
-
- **Build Command:** `npm run build:ng-shared`
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/ng-shared)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
7
|
|
|
17
8
|
---
|
|
18
9
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
### Data Interfaces
|
|
22
|
-
|
|
23
|
-
#### IBaseEntity
|
|
24
|
-
|
|
25
|
-
Base entity interface matching backend Identity entity.
|
|
26
|
-
|
|
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
|
-
```
|
|
38
|
-
|
|
39
|
-
**Entity Mixins:**
|
|
40
|
-
|
|
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 |
|
|
48
|
-
|
|
49
|
-
#### ILoggedUserInfo
|
|
50
|
-
|
|
51
|
-
Current logged-in user info with optional company context.
|
|
52
|
-
|
|
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
|
-
```
|
|
66
|
-
|
|
67
|
-
#### IFilterData
|
|
68
|
-
|
|
69
|
-
Filter, pagination, and sort payload for list queries.
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
interface IPagination {
|
|
73
|
-
pageSize: number;
|
|
74
|
-
currentPage: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface ISort { [key: string]: 'ASC' | 'DESC' }
|
|
78
|
-
interface IFilter { [key: string]: any }
|
|
79
|
-
|
|
80
|
-
interface IFilterData {
|
|
81
|
-
filter?: IFilter;
|
|
82
|
-
pagination?: IPagination;
|
|
83
|
-
select?: string[];
|
|
84
|
-
sort?: ISort;
|
|
85
|
-
withDeleted?: boolean;
|
|
86
|
-
extraKey?: string[];
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
#### IDeleteData
|
|
91
|
-
|
|
92
|
-
Delete request payload matching backend DeleteDto.
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
type DeleteType = 'delete' | 'restore' | 'permanent';
|
|
10
|
+
## Installation
|
|
96
11
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
type: DeleteType;
|
|
100
|
-
}
|
|
12
|
+
```bash
|
|
13
|
+
npm install @flusys/ng-shared @flusys/ng-core
|
|
101
14
|
```
|
|
102
15
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Simple dropdown item for select components.
|
|
16
|
+
---
|
|
106
17
|
|
|
107
|
-
|
|
108
|
-
interface IDropDown {
|
|
109
|
-
label: string;
|
|
110
|
-
value: string;
|
|
111
|
-
}
|
|
112
|
-
```
|
|
18
|
+
## 1. ApiResourceService — Signal-Driven CRUD
|
|
113
19
|
|
|
114
|
-
|
|
20
|
+
Extend this class to get a reactive list resource backed by `resource()`, with pagination, search, and all standard POST-only RPC endpoints.
|
|
115
21
|
|
|
116
22
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
search: string;
|
|
121
|
-
[key: string]: unknown;
|
|
122
|
-
}
|
|
23
|
+
import { Injectable, inject } from "@angular/core";
|
|
24
|
+
import { HttpClient } from "@angular/common/http";
|
|
25
|
+
import { ApiResourceService } from "@flusys/ng-shared";
|
|
123
26
|
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### File Select Interfaces
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
interface IFileBasicInfo {
|
|
27
|
+
export interface IProduct {
|
|
131
28
|
id: string;
|
|
132
29
|
name: string;
|
|
133
|
-
|
|
134
|
-
size: string;
|
|
135
|
-
url: string | null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface IFileUploadOptions {
|
|
139
|
-
storageConfigId?: string;
|
|
140
|
-
folderPath?: string;
|
|
141
|
-
maxWidth?: number;
|
|
142
|
-
maxHeight?: number;
|
|
143
|
-
quality?: number;
|
|
144
|
-
compress?: boolean;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
interface IUploadedFile {
|
|
148
|
-
id?: string; // File manager ID (UUID) - available when registered
|
|
149
|
-
name: string;
|
|
150
|
-
key: string;
|
|
151
|
-
size: number;
|
|
152
|
-
contentType: string;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
interface IFileSelectFilter {
|
|
156
|
-
page: number;
|
|
157
|
-
pageSize: number;
|
|
158
|
-
search: string;
|
|
159
|
-
contentTypes?: string[];
|
|
160
|
-
folderId?: string;
|
|
161
|
-
[key: string]: unknown;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
type LoadFilesFn = (filter: IFileSelectFilter) => Observable<IListResponse<IFileBasicInfo>>;
|
|
165
|
-
type UploadFileFn = (file: File, options?: IFileUploadOptions) => Observable<ISingleResponse<IUploadedFile>>;
|
|
166
|
-
type GetFileUrlsFn = (fileIds: string[]) => Observable<ISingleResponse<IFileBasicInfo[]>>;
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
**File Type Filters (Constants):**
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
import { FILE_TYPE_FILTERS, getAcceptString, isFileTypeAllowed, getFileIconClass, formatFileSize } from '@flusys/ng-shared';
|
|
173
|
-
|
|
174
|
-
FILE_TYPE_FILTERS.IMAGES // ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
|
|
175
|
-
FILE_TYPE_FILTERS.DOCUMENTS // ['application/pdf', 'application/msword', ...]
|
|
176
|
-
FILE_TYPE_FILTERS.VIDEOS // ['video/mp4', 'video/webm', ...]
|
|
177
|
-
FILE_TYPE_FILTERS.AUDIO // ['audio/mpeg', 'audio/wav', ...]
|
|
178
|
-
FILE_TYPE_FILTERS.ALL // [] (allows all)
|
|
179
|
-
|
|
180
|
-
getAcceptString(['image/*']) // 'image/*'
|
|
181
|
-
isFileTypeAllowed(file, ['image/*']) // true/false
|
|
182
|
-
getFileIconClass('image/png') // 'pi pi-image'
|
|
183
|
-
formatFileSize(1024) // '1 KB'
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### Response Interfaces
|
|
187
|
-
|
|
188
|
-
Type-safe interfaces matching FLUSYS_NEST backend response DTOs.
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// Single item response (POST /resource/insert, /update, /get/:id)
|
|
192
|
-
interface ISingleResponse<T> {
|
|
193
|
-
success: boolean;
|
|
194
|
-
message: string;
|
|
195
|
-
data?: T;
|
|
196
|
-
_meta?: IRequestMeta;
|
|
30
|
+
price: number;
|
|
197
31
|
}
|
|
198
|
-
|
|
199
|
-
// List response with pagination (POST /resource/get-all)
|
|
200
|
-
interface IListResponse<T> {
|
|
201
|
-
success: boolean;
|
|
202
|
-
message: string;
|
|
203
|
-
data?: T[];
|
|
204
|
-
meta: IPaginationMeta;
|
|
205
|
-
_meta?: IRequestMeta;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Bulk operation response (POST /resource/insert-many, /update-many)
|
|
209
|
-
interface IBulkResponse<T> {
|
|
210
|
-
success: boolean;
|
|
211
|
-
message: string;
|
|
212
|
-
data?: T[];
|
|
213
|
-
meta: IBulkMeta;
|
|
214
|
-
_meta?: IRequestMeta;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Message-only response (POST /resource/delete, /logout)
|
|
218
|
-
interface IMessageResponse {
|
|
219
|
-
success: boolean;
|
|
220
|
-
message: string;
|
|
221
|
-
_meta?: IRequestMeta;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Error response (validation errors, exceptions)
|
|
225
|
-
interface IErrorResponse {
|
|
226
|
-
success: false;
|
|
227
|
-
message: string;
|
|
228
|
-
code?: string;
|
|
229
|
-
errors?: IValidationError[];
|
|
230
|
-
_meta?: IRequestMeta;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Union type
|
|
234
|
-
type ApiResponse<T> = ISingleResponse<T> | IListResponse<T> | IBulkResponse<T> | IMessageResponse | IErrorResponse;
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Metadata types:**
|
|
238
|
-
|
|
239
|
-
| Interface | Fields |
|
|
240
|
-
| ------------------ | -------------------------------------------- |
|
|
241
|
-
| `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
|
|
242
|
-
| `IPaginationMeta` | `total, page, pageSize, count, hasMore?, totalPages?` |
|
|
243
|
-
| `IBulkMeta` | `count, failed?, total?` |
|
|
244
|
-
| `IValidationError` | `field, message, constraint?` |
|
|
245
|
-
|
|
246
|
-
### Auth & Storage Response Types
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
interface ILoginResponse {
|
|
250
|
-
success: boolean;
|
|
251
|
-
message: string;
|
|
252
|
-
data: { accessToken: string; refreshToken: string; user: ILoginUserData };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
interface IRefreshTokenResponse {
|
|
256
|
-
success: boolean;
|
|
257
|
-
message: string;
|
|
258
|
-
data: { accessToken: string; refreshToken?: string };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
interface IFileData {
|
|
262
|
-
id: string;
|
|
32
|
+
export interface IProductDto {
|
|
263
33
|
name: string;
|
|
264
|
-
|
|
265
|
-
contentType: string;
|
|
266
|
-
size: number;
|
|
267
|
-
key: string;
|
|
268
|
-
url?: string;
|
|
269
|
-
thumbnailUrl?: string;
|
|
270
|
-
createdAt: Date;
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Permission Interfaces
|
|
275
|
-
|
|
276
|
-
Discriminated union for building complex permission logic trees.
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// Single permission check
|
|
280
|
-
interface IActionNode {
|
|
281
|
-
type: 'action';
|
|
282
|
-
actionId: string;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Group with AND/OR logic
|
|
286
|
-
interface IGroupNode {
|
|
287
|
-
type: 'group';
|
|
288
|
-
operator: 'AND' | 'OR';
|
|
289
|
-
children: ILogicNode[];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Union type
|
|
293
|
-
type ILogicNode = IActionNode | IGroupNode;
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 2. Enums
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
enum ContactTypeEnum {
|
|
302
|
-
PHONE = 1,
|
|
303
|
-
EMAIL = 2,
|
|
34
|
+
price: number;
|
|
304
35
|
}
|
|
305
36
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## 3. Constants
|
|
316
|
-
|
|
317
|
-
### Permission Constants
|
|
318
|
-
|
|
319
|
-
Centralized permission codes for type-safe permission checks. Single source of truth to prevent typos.
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
import { PERMISSIONS, USER_PERMISSIONS, ROLE_PERMISSIONS } from '@flusys/ng-shared';
|
|
323
|
-
|
|
324
|
-
// Use constants instead of strings
|
|
325
|
-
*hasPermission="PERMISSIONS.USER.READ"
|
|
326
|
-
|
|
327
|
-
// Individual permission groups available:
|
|
328
|
-
USER_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
329
|
-
COMPANY_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
330
|
-
BRANCH_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
331
|
-
ACTION_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
332
|
-
ROLE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
333
|
-
ROLE_ACTION_PERMISSIONS // { READ, ASSIGN }
|
|
334
|
-
USER_ROLE_PERMISSIONS // { READ, ASSIGN }
|
|
335
|
-
USER_ACTION_PERMISSIONS // { READ, ASSIGN }
|
|
336
|
-
COMPANY_ACTION_PERMISSIONS // { READ, ASSIGN }
|
|
337
|
-
FILE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
338
|
-
FOLDER_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
339
|
-
STORAGE_CONFIG_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
340
|
-
EMAIL_CONFIG_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
341
|
-
EMAIL_TEMPLATE_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
342
|
-
FORM_PERMISSIONS // { CREATE, READ, UPDATE, DELETE }
|
|
343
|
-
|
|
344
|
-
// Aggregated object with all permissions
|
|
345
|
-
PERMISSIONS.USER.READ // 'user.read'
|
|
346
|
-
PERMISSIONS.ROLE.CREATE // 'role.create'
|
|
347
|
-
PERMISSIONS.FILE.DELETE // 'file.delete'
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
**Type:** `PermissionCode` - Union type of all valid permission code strings.
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## 4. Services
|
|
355
|
-
|
|
356
|
-
### ApiResourceService
|
|
357
|
-
|
|
358
|
-
Signal-based CRUD service using Angular 21 `resource()` API. All endpoints use POST (RPC-style).
|
|
359
|
-
|
|
360
|
-
**ServiceName Type:**
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
type ServiceName = 'auth' | 'administration' | 'iam' | 'storage' | 'formBuilder' | 'email';
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
**Define a service:**
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
import { Injectable } from '@angular/core';
|
|
370
|
-
import { HttpClient } from '@angular/common/http';
|
|
371
|
-
import { ApiResourceService } from '@flusys/ng-shared';
|
|
372
|
-
|
|
373
|
-
@Injectable({ providedIn: 'root' })
|
|
374
|
-
export class UserService extends ApiResourceService<UserDto, IUser> {
|
|
375
|
-
constructor(http: HttpClient) {
|
|
376
|
-
// Option 1: Use global apiBaseUrl (default)
|
|
377
|
-
super('users', http);
|
|
378
|
-
// Base URL: APP_CONFIG.apiBaseUrl + '/users'
|
|
379
|
-
|
|
380
|
-
// Option 2: Use feature-specific service URL (recommended)
|
|
381
|
-
super('users', http, 'administration');
|
|
382
|
-
// Base URL: APP_CONFIG.services.administration.baseUrl + '/users'
|
|
37
|
+
@Injectable({ providedIn: "root" })
|
|
38
|
+
export class ProductService extends ApiResourceService<IProductDto, IProduct> {
|
|
39
|
+
constructor() {
|
|
40
|
+
super("products", "administration"); // moduleApiName, http, serviceName (key in APP_CONFIG.services)
|
|
383
41
|
}
|
|
384
42
|
}
|
|
385
43
|
```
|
|
386
44
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
| Parameter | Type | Required | Description |
|
|
390
|
-
|-----------|------|----------|-------------|
|
|
391
|
-
| `moduleApiName` | `string` | Yes | API path segment (e.g., 'users', 'file-manager') |
|
|
392
|
-
| `http` | `HttpClient` | Yes | Angular HttpClient instance |
|
|
393
|
-
| `serviceName` | `ServiceName` | No | Feature service name for URL resolution |
|
|
394
|
-
|
|
395
|
-
**Service URL Resolution:**
|
|
396
|
-
- If `serviceName` is provided, uses `getServiceUrl(config, serviceName)` to resolve the base URL
|
|
397
|
-
- Falls back to `APP_CONFIG.apiBaseUrl` if service not found or `serviceName` not provided
|
|
398
|
-
|
|
399
|
-
**Endpoint mapping:**
|
|
400
|
-
|
|
401
|
-
| Method | HTTP | Endpoint | Response |
|
|
402
|
-
| -------------- | ---- | ------------------------- | ------------------------ |
|
|
403
|
-
| `insert(dto)` | POST | `/{resource}/insert` | `ISingleResponse<T>` |
|
|
404
|
-
| `insertMany(dtos)` | POST | `/{resource}/insert-many` | `IBulkResponse<T>` |
|
|
405
|
-
| `findById(id, select?)` | POST | `/{resource}/get/:id` | `ISingleResponse<T>` |
|
|
406
|
-
| `getAll(search, filter)` | POST | `/{resource}/get-all?q=` | `IListResponse<T>` |
|
|
407
|
-
| `update(dto)` | POST | `/{resource}/update` | `ISingleResponse<T>` |
|
|
408
|
-
| `updateMany(dtos)` | POST | `/{resource}/update-many` | `IBulkResponse<T>` |
|
|
409
|
-
| `delete(deleteDto)` | POST | `/{resource}/delete` | `IMessageResponse` |
|
|
410
|
-
|
|
411
|
-
All methods above return `Observable`. Async (Promise) variants are also available: `insertAsync`, `insertManyAsync`, `findByIdAsync`, `updateAsync`, `updateManyAsync`, `deleteAsync`.
|
|
412
|
-
|
|
413
|
-
**Reactive signals:**
|
|
414
|
-
|
|
415
|
-
| Signal | Type | Description |
|
|
416
|
-
| ------------ | ------------------------- | ------------------------ |
|
|
417
|
-
| `isLoading` | `Signal<boolean>` | Whether data is loading |
|
|
418
|
-
| `data` | `Signal<T[]>` | Current list data |
|
|
419
|
-
| `total` | `Signal<number>` | Total item count |
|
|
420
|
-
| `pageInfo` | `Signal<IPaginationMeta>` | Pagination metadata |
|
|
421
|
-
| `hasMore` | `Signal<boolean>` | More pages available |
|
|
422
|
-
| `searchTerm` | `WritableSignal<string>` | Current search term |
|
|
423
|
-
| `filterData` | `WritableSignal<IFilterData>` | Filter/pagination state |
|
|
424
|
-
|
|
425
|
-
**List management:**
|
|
45
|
+
Using the reactive list in a component:
|
|
426
46
|
|
|
427
47
|
```typescript
|
|
428
|
-
|
|
429
|
-
|
|
48
|
+
@Component({ ... })
|
|
49
|
+
export class ProductListComponent {
|
|
50
|
+
private readonly svc = inject(ProductService);
|
|
430
51
|
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
userService.resetPagination();
|
|
435
|
-
userService.reload();
|
|
436
|
-
```
|
|
52
|
+
readonly items = this.svc.data; // Signal<IProduct[]>
|
|
53
|
+
readonly total = this.svc.total; // Signal<number>
|
|
54
|
+
readonly loading = this.svc.isLoading; // Signal<boolean>
|
|
437
55
|
|
|
438
|
-
|
|
56
|
+
ngOnInit() { this.svc.fetchList(); }
|
|
439
57
|
|
|
440
|
-
|
|
441
|
-
@Component({...})
|
|
442
|
-
export class UserListComponent {
|
|
443
|
-
private readonly userService = inject(UserService);
|
|
58
|
+
onSearch(q: string) { this.svc.fetchList(q); }
|
|
444
59
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
60
|
+
onPageChange(page: number, size: number) {
|
|
61
|
+
this.svc.setPagination({ currentPage: page, pageSize: size });
|
|
62
|
+
}
|
|
448
63
|
|
|
449
|
-
|
|
450
|
-
this.
|
|
64
|
+
async onCreate(dto: IProductDto) {
|
|
65
|
+
await this.svc.insert(dto);
|
|
66
|
+
this.svc.reload();
|
|
451
67
|
}
|
|
452
68
|
|
|
453
|
-
async
|
|
454
|
-
await this.
|
|
455
|
-
this.
|
|
69
|
+
async onDelete(id: string) {
|
|
70
|
+
await this.svc.delete({ id });
|
|
71
|
+
this.svc.reload();
|
|
456
72
|
}
|
|
457
73
|
}
|
|
458
74
|
```
|
|
459
75
|
|
|
460
|
-
|
|
76
|
+
Available CRUD methods: `insert`, `insertMany`, `findById`, `findByIds`, `getAll`, `getByFilter`, `update`, `updateMany`, `bulkUpsert`, `delete`.
|
|
461
77
|
|
|
462
|
-
|
|
78
|
+
---
|
|
463
79
|
|
|
464
|
-
|
|
80
|
+
## 2. FileUrlService — Presigned URL Fetching
|
|
465
81
|
|
|
466
|
-
|
|
82
|
+
Never construct file URLs manually. `FileUrlService` handles S3/Azure presigned URLs, caching, and deduplication.
|
|
467
83
|
|
|
468
84
|
```typescript
|
|
469
85
|
import { FileUrlService } from '@flusys/ng-shared';
|
|
470
86
|
|
|
471
|
-
@Component({...})
|
|
472
|
-
export class
|
|
87
|
+
@Component({ ... })
|
|
88
|
+
export class AvatarComponent {
|
|
473
89
|
private readonly fileUrlService = inject(FileUrlService);
|
|
90
|
+
readonly avatarUrl = signal<string | null>(null);
|
|
474
91
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.imageUrl.set(file?.url ?? null);
|
|
92
|
+
ngOnInit() {
|
|
93
|
+
this.fileUrlService.fetchSingleFileUrl('file-id-123').subscribe(file => {
|
|
94
|
+
this.avatarUrl.set(file?.url ?? null);
|
|
479
95
|
});
|
|
480
96
|
}
|
|
481
97
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
});
|
|
98
|
+
// Batch fetch — only requests IDs not already cached
|
|
99
|
+
loadMultiple(ids: string[]) {
|
|
100
|
+
this.fileUrlService.fetchFileUrls(ids).subscribe(files => { /* ... */ });
|
|
486
101
|
}
|
|
487
|
-
}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
**Methods:**
|
|
491
|
-
|
|
492
|
-
| Method | Returns | Description |
|
|
493
|
-
| ----------------------------- | ----------------------------------- | ---------------------------------- |
|
|
494
|
-
| `getFileUrl(fileId)` | `string \| null` | Get cached URL (synchronous) |
|
|
495
|
-
| `fileUrlSignal(fileId)` | `Signal<string \| null>` | Computed signal from cache |
|
|
496
|
-
| `fetchFileUrls(fileIds[])` | `Observable<FilesResponseDto[]>` | Fetch from backend, updates cache |
|
|
497
|
-
| `fetchSingleFileUrl(fileId)` | `Observable<FilesResponseDto \| null>` | Fetch single file |
|
|
498
|
-
| `clearCache()` | `void` | Clear all cached URLs |
|
|
499
|
-
| `removeFromCache(fileId)` | `void` | Remove specific entry from cache |
|
|
500
|
-
|
|
501
|
-
### PermissionValidatorService
|
|
502
|
-
|
|
503
|
-
Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM.
|
|
504
102
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
@Component({...})
|
|
509
|
-
export class MyComponent {
|
|
510
|
-
private readonly permissionValidator = inject(PermissionValidatorService);
|
|
511
|
-
|
|
512
|
-
ngOnInit() {
|
|
513
|
-
// Set permissions (typically done by IAM PermissionStateService)
|
|
514
|
-
this.permissionValidator.setPermissions(['user.view', 'user.create']);
|
|
515
|
-
|
|
516
|
-
// Check single permission
|
|
517
|
-
if (this.permissionValidator.hasPermission('user.view')) {
|
|
518
|
-
// User has permission
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Check loaded state
|
|
522
|
-
if (this.permissionValidator.isPermissionsLoaded()) {
|
|
523
|
-
// Permissions have been loaded
|
|
524
|
-
}
|
|
103
|
+
// Synchronous read from cache after a prior fetch
|
|
104
|
+
getCachedUrl(id: string): string | null {
|
|
105
|
+
return this.fileUrlService.getFileUrl(id);
|
|
525
106
|
}
|
|
526
107
|
}
|
|
527
108
|
```
|
|
528
109
|
|
|
529
|
-
**Methods:**
|
|
530
|
-
|
|
531
|
-
| Method | Returns | Description |
|
|
532
|
-
| ---------------------------- | --------- | ---------------------------------- |
|
|
533
|
-
| `setPermissions(codes[])` | `void` | Replace all permissions |
|
|
534
|
-
| `clearPermissions()` | `void` | Clear all permissions |
|
|
535
|
-
| `hasPermission(code)` | `boolean` | Check single permission |
|
|
536
|
-
| `isPermissionsLoaded()` | `boolean` | Whether permissions have been set |
|
|
537
|
-
|
|
538
|
-
**Signals:**
|
|
539
|
-
|
|
540
|
-
| Signal | Type | Description |
|
|
541
|
-
| ------------- | ------------------ | ------------------------------ |
|
|
542
|
-
| `permissions` | `Signal<string[]>` | Readonly current permissions |
|
|
543
|
-
|
|
544
|
-
### CookieService
|
|
545
|
-
|
|
546
|
-
SSR-aware cookie reading service.
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
import { CookieService } from '@flusys/ng-shared';
|
|
550
|
-
|
|
551
|
-
const cookies = inject(CookieService).get(); // Returns document.cookie (browser) or request header cookie (server)
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### PlatformService
|
|
555
|
-
|
|
556
|
-
SSR environment detection service.
|
|
557
|
-
|
|
558
|
-
```typescript
|
|
559
|
-
import { PlatformService } from '@flusys/ng-shared';
|
|
560
|
-
|
|
561
|
-
const platform = inject(PlatformService);
|
|
562
|
-
if (!platform.isServer) {
|
|
563
|
-
// Browser-only code (localStorage, window, etc.)
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
110
|
---
|
|
568
111
|
|
|
569
|
-
##
|
|
570
|
-
|
|
571
|
-
### IconComponent
|
|
572
|
-
|
|
573
|
-
Flexible icon renderer supporting PrimeNG icons, image files, and SVG.
|
|
574
|
-
|
|
575
|
-
- **Selector:** `lib-icon`
|
|
576
|
-
- **Inputs:** `icon` (required string), `iconType` (optional `IconTypeEnum`, default: `PRIMENG_ICON`)
|
|
577
|
-
|
|
578
|
-
```html
|
|
579
|
-
<!-- PrimeNG icon (default) -->
|
|
580
|
-
<lib-icon icon="pi pi-user" />
|
|
581
|
-
|
|
582
|
-
<!-- Image file -->
|
|
583
|
-
<lib-icon icon="/assets/logo.png" [iconType]="IconTypeEnum.IMAGE_FILE_LINK" />
|
|
584
|
-
|
|
585
|
-
<!-- SVG tag (shows fallback icon - full SVG rendering coming soon) -->
|
|
586
|
-
<lib-icon icon="<svg>...</svg>" [iconType]="IconTypeEnum.DIRECT_TAG_SVG" />
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
### LazySelectComponent
|
|
590
|
-
|
|
591
|
-
Single-select dropdown with lazy loading, search, and scroll pagination.
|
|
592
|
-
|
|
593
|
-
- **Selector:** `lib-lazy-select`
|
|
594
|
-
- **Extends:** `BaseFormControl<string | null>`
|
|
595
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
596
|
-
|
|
597
|
-
```typescript
|
|
598
|
-
@Component({
|
|
599
|
-
imports: [LazySelectComponent],
|
|
600
|
-
template: `
|
|
601
|
-
<lib-lazy-select
|
|
602
|
-
[(value)]="selectedId"
|
|
603
|
-
[selectDataList]="items()"
|
|
604
|
-
[optionLabel]="'label'"
|
|
605
|
-
[optionValue]="'value'"
|
|
606
|
-
[isEditMode]="true"
|
|
607
|
-
[isLoading]="loading()"
|
|
608
|
-
[total]="total()"
|
|
609
|
-
[pagination]="pagination()"
|
|
610
|
-
[placeHolder]="'Select item'"
|
|
611
|
-
(onSearch)="handleSearch($event)"
|
|
612
|
-
(onPagination)="handlePagination($event)"
|
|
613
|
-
/>
|
|
614
|
-
`,
|
|
615
|
-
})
|
|
616
|
-
export class MyComponent {
|
|
617
|
-
readonly selectedId = signal<string | null>(null);
|
|
618
|
-
readonly items = signal<IDropDown[]>([]);
|
|
619
|
-
readonly loading = signal(false);
|
|
620
|
-
readonly total = signal<number | undefined>(undefined);
|
|
621
|
-
readonly pagination = signal<IPagination>({ currentPage: 0, pageSize: 20 });
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
**Inputs:**
|
|
626
|
-
|
|
627
|
-
| Input | Type | Description |
|
|
628
|
-
| ---------------- | ----------------- | -------------------------------- |
|
|
629
|
-
| `selectDataList` | `Array<IDropDown>` | Dropdown options (required) |
|
|
630
|
-
| `optionLabel` | `string` | Label field name (required) |
|
|
631
|
-
| `optionValue` | `string` | Value field name (required) |
|
|
632
|
-
| `isEditMode` | `boolean` | Enable/disable editing (required)|
|
|
633
|
-
| `isLoading` | `boolean` | Loading state (required) |
|
|
634
|
-
| `total` | `number \| undefined` | Total items for pagination |
|
|
635
|
-
| `pagination` | `IPagination` | Current pagination state |
|
|
636
|
-
| `placeHolder` | `string` | Placeholder text (default: `'Select Option'`) |
|
|
637
|
-
|
|
638
|
-
**Model:** `value` - Two-way bound selected value (`string | null`)
|
|
639
|
-
|
|
640
|
-
**Outputs:** `onSearch` (debounced 500ms), `onPagination` (scroll-triggered)
|
|
641
|
-
|
|
642
|
-
### LazyMultiSelectComponent
|
|
643
|
-
|
|
644
|
-
Multi-select dropdown with lazy loading, search, select-all, and scroll pagination.
|
|
645
|
-
|
|
646
|
-
- **Selector:** `lib-lazy-multi-select`
|
|
647
|
-
- **Extends:** `BaseFormControl<string[] | null>`
|
|
648
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
649
|
-
|
|
650
|
-
```typescript
|
|
651
|
-
@Component({
|
|
652
|
-
imports: [LazyMultiSelectComponent],
|
|
653
|
-
template: `
|
|
654
|
-
<lib-lazy-multi-select
|
|
655
|
-
[(value)]="selectedIds"
|
|
656
|
-
[selectDataList]="items()"
|
|
657
|
-
[isEditMode]="true"
|
|
658
|
-
[isLoading]="loading()"
|
|
659
|
-
[total]="total()"
|
|
660
|
-
[pagination]="pagination()"
|
|
661
|
-
[placeHolder]="'Select items'"
|
|
662
|
-
(onSearch)="handleSearch($event)"
|
|
663
|
-
(onPagination)="handlePagination($event)"
|
|
664
|
-
/>
|
|
665
|
-
`,
|
|
666
|
-
})
|
|
667
|
-
export class MyComponent {
|
|
668
|
-
readonly selectedIds = signal<string[] | null>(null);
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
**Inputs:** `selectDataList`, `isEditMode`, `isLoading`, `total`, `pagination`, `placeHolder` (same as `LazySelectComponent`). Does not have `optionLabel`/`optionValue` (uses `IDropDown.label`/`IDropDown.value` directly).
|
|
673
|
-
|
|
674
|
-
**Model:** `value` - Two-way bound selected values (`string[] | null`)
|
|
675
|
-
|
|
676
|
-
**Computed signals:** `selectedValueDisplay` (display text), `isSelectAll` (all items selected)
|
|
677
|
-
|
|
678
|
-
### UserSelectComponent
|
|
679
|
-
|
|
680
|
-
Single user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
|
|
681
|
-
|
|
682
|
-
- **Selector:** `lib-user-select`
|
|
683
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
684
|
-
|
|
685
|
-
```typescript
|
|
686
|
-
@Component({
|
|
687
|
-
imports: [UserSelectComponent],
|
|
688
|
-
template: `
|
|
689
|
-
<!-- Simple usage - uses USER_PROVIDER internally -->
|
|
690
|
-
<lib-user-select
|
|
691
|
-
[(value)]="selectedUserId"
|
|
692
|
-
[isEditMode]="true"
|
|
693
|
-
/>
|
|
694
|
-
|
|
695
|
-
<!-- With custom loadUsers function -->
|
|
696
|
-
<lib-user-select
|
|
697
|
-
[(value)]="selectedUserId"
|
|
698
|
-
[isEditMode]="true"
|
|
699
|
-
[loadUsers]="customLoadUsers"
|
|
700
|
-
/>
|
|
701
|
-
`,
|
|
702
|
-
})
|
|
703
|
-
export class MyComponent {
|
|
704
|
-
readonly selectedUserId = signal<string | null>(null);
|
|
705
|
-
}
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
**Inputs:**
|
|
709
|
-
|
|
710
|
-
| Input | Type | Default | Description |
|
|
711
|
-
| ----- | ---- | ------- | ----------- |
|
|
712
|
-
| `isEditMode` | `boolean` | required | Enable/disable editing |
|
|
713
|
-
| `placeHolder` | `string` | `'Select User'` | Placeholder text |
|
|
714
|
-
| `filterActive` | `boolean` | `true` | Filter active users only |
|
|
715
|
-
| `additionalFilters` | `Record<string, unknown>` | `{}` | Extra filter params |
|
|
716
|
-
| `pageSize` | `number` | `20` | Page size for pagination |
|
|
717
|
-
| `loadUsers` | `LoadUsersFn` | - | Custom user loading function |
|
|
718
|
-
|
|
719
|
-
**Model:** `value` - Two-way bound selected user ID (`string | null`)
|
|
720
|
-
|
|
721
|
-
**Outputs:** `userSelected` (IUserBasicInfo | null), `onError` (Error)
|
|
722
|
-
|
|
723
|
-
### UserMultiSelectComponent
|
|
724
|
-
|
|
725
|
-
Multiple user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
|
|
726
|
-
|
|
727
|
-
- **Selector:** `lib-user-multi-select`
|
|
728
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
@Component({
|
|
732
|
-
imports: [UserMultiSelectComponent],
|
|
733
|
-
template: `
|
|
734
|
-
<lib-user-multi-select
|
|
735
|
-
[(value)]="selectedUserIds"
|
|
736
|
-
[isEditMode]="true"
|
|
737
|
-
/>
|
|
738
|
-
`,
|
|
739
|
-
})
|
|
740
|
-
export class MyComponent {
|
|
741
|
-
readonly selectedUserIds = signal<string[] | null>(null);
|
|
742
|
-
}
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
**Inputs:** Same as `UserSelectComponent`.
|
|
746
|
-
|
|
747
|
-
**Model:** `value` - Two-way bound selected user IDs (`string[] | null`)
|
|
748
|
-
|
|
749
|
-
**Outputs:** `usersSelected` (IUserBasicInfo[]), `onError` (Error)
|
|
750
|
-
|
|
751
|
-
### FileUploaderComponent
|
|
752
|
-
|
|
753
|
-
Drag & drop file upload with type filtering. Pass your own `uploadFile` function.
|
|
754
|
-
|
|
755
|
-
- **Selector:** `lib-file-uploader`
|
|
112
|
+
## 3. PermissionValidatorService + HasPermissionDirective
|
|
756
113
|
|
|
757
114
|
```typescript
|
|
758
|
-
|
|
759
|
-
imports: [FileUploaderComponent],
|
|
760
|
-
template: `
|
|
761
|
-
<!-- Single image upload -->
|
|
762
|
-
<lib-file-uploader
|
|
763
|
-
[uploadFile]="uploadFile"
|
|
764
|
-
[acceptTypes]="['image/*']"
|
|
765
|
-
[multiple]="false"
|
|
766
|
-
(fileUploaded)="onFileUploaded($event)"
|
|
767
|
-
/>
|
|
768
|
-
|
|
769
|
-
<!-- Multiple document upload -->
|
|
770
|
-
<lib-file-uploader
|
|
771
|
-
[uploadFile]="uploadFile"
|
|
772
|
-
[acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
|
|
773
|
-
[multiple]="true"
|
|
774
|
-
[maxFiles]="5"
|
|
775
|
-
(filesUploaded)="onFilesUploaded($event)"
|
|
776
|
-
/>
|
|
777
|
-
`,
|
|
778
|
-
})
|
|
779
|
-
export class MyComponent {
|
|
780
|
-
readonly uploadService = inject(UploadService);
|
|
115
|
+
const pv = inject(PermissionValidatorService);
|
|
781
116
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
117
|
+
pv.hasPermission("user.create"); // true
|
|
118
|
+
pv.hasPermission("report.export"); // true — wildcard match
|
|
119
|
+
pv.isPermissionsLoaded(); // boolean
|
|
785
120
|
```
|
|
786
121
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
| ----- | ---- | ------- | ----------- |
|
|
791
|
-
| `uploadFile` | `UploadFileFn` | required | Upload function |
|
|
792
|
-
| `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
|
|
793
|
-
| `multiple` | `boolean` | `false` | Allow multiple files |
|
|
794
|
-
| `maxFiles` | `number` | `10` | Max files for multiple |
|
|
795
|
-
| `maxSizeMb` | `number` | `10` | Max file size in MB |
|
|
796
|
-
| `uploadOptions` | `IFileUploadOptions` | `{}` | Upload options |
|
|
797
|
-
| `disabled` | `boolean` | `false` | Disable uploader |
|
|
798
|
-
| `showPreview` | `boolean` | `true` | Show selected files preview |
|
|
799
|
-
| `autoUpload` | `boolean` | `true` | Upload immediately on selection |
|
|
800
|
-
|
|
801
|
-
**Outputs:** `fileUploaded` (IUploadedFile), `filesUploaded` (IUploadedFile[]), `onError` (Error), `fileSelected` (File[])
|
|
802
|
-
|
|
803
|
-
### FileSelectorDialogComponent
|
|
804
|
-
|
|
805
|
-
Dialog to browse and select existing files with filtering.
|
|
806
|
-
|
|
807
|
-
- **Selector:** `lib-file-selector-dialog`
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
@Component({
|
|
811
|
-
imports: [FileSelectorDialogComponent],
|
|
812
|
-
template: `
|
|
813
|
-
<lib-file-selector-dialog
|
|
814
|
-
[(visible)]="showFileSelector"
|
|
815
|
-
[loadFiles]="loadFiles"
|
|
816
|
-
[acceptTypes]="['image/*']"
|
|
817
|
-
[multiple]="false"
|
|
818
|
-
(fileSelected)="onFileSelected($event)"
|
|
819
|
-
/>
|
|
820
|
-
`,
|
|
821
|
-
})
|
|
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
|
-
}
|
|
122
|
+
```html
|
|
123
|
+
<!-- Removes element from DOM when permission fails -->
|
|
124
|
+
<button *hasPermission="'user.create'">Create User</button>
|
|
831
125
|
```
|
|
832
126
|
|
|
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
127
|
---
|
|
849
128
|
|
|
850
|
-
##
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
}
|
|
129
|
+
## 4. Permission Guards
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { permissionGuard, anyPermissionGuard, allPermissionsGuard } from "@flusys/ng-shared";
|
|
133
|
+
|
|
134
|
+
export const routes: Routes = [
|
|
135
|
+
{
|
|
136
|
+
path: "users",
|
|
137
|
+
canActivate: [permissionGuard("user.view")], // single
|
|
138
|
+
loadComponent: () => import("./user-list.component"),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
path: "reports",
|
|
142
|
+
canActivate: [anyPermissionGuard(["report.view", "report.export"])], // OR
|
|
143
|
+
loadComponent: () => import("./reports.component"),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
path: "admin",
|
|
147
|
+
canActivate: [allPermissionsGuard(["admin.view", "admin.manage"])], // AND
|
|
148
|
+
loadComponent: () => import("./admin.component"),
|
|
149
|
+
},
|
|
150
|
+
];
|
|
882
151
|
```
|
|
883
152
|
|
|
884
|
-
|
|
153
|
+
All three guards redirect to `/` by default. Pass a second argument to change it: `permissionGuard('user.view', '/access-denied')`.
|
|
885
154
|
|
|
886
|
-
|
|
155
|
+
---
|
|
887
156
|
|
|
888
|
-
|
|
889
|
-
- **Input:** `isEditMode` (required boolean)
|
|
157
|
+
## 5. TranslatePipe
|
|
890
158
|
|
|
891
159
|
```html
|
|
892
|
-
|
|
893
|
-
<p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
|
|
160
|
+
{{ 'shared.save' | translate }} {{ 'pagination.showing' | translate: { from: 1, to: 10 } }}
|
|
894
161
|
```
|
|
895
162
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
Automatically replaces broken or empty image `src` with a default fallback image.
|
|
899
|
-
|
|
900
|
-
- **Selector:** `img` (applies to all `<img>` elements)
|
|
901
|
-
- **Input:** `src` (standard img src)
|
|
902
|
-
- **Default image:** `lib/assets/images/default/default-image.jpg`
|
|
903
|
-
|
|
904
|
-
```html
|
|
905
|
-
<img [src]="imageUrl()" alt="Product" />
|
|
906
|
-
<!-- Falls back to default image on error or empty src -->
|
|
907
|
-
```
|
|
908
|
-
|
|
909
|
-
### PreventDefaultDirective
|
|
910
|
-
|
|
911
|
-
Prevents default browser behavior on specified events and emits the event.
|
|
912
|
-
|
|
913
|
-
- **Selector:** `[appPreventDefault]`
|
|
914
|
-
- **Inputs:** `eventType` (`'click' | 'keydown' | 'keyup'`, default: `'click'`), `preventKey` (optional key filter)
|
|
915
|
-
- **Output:** `action` - Emits the prevented event
|
|
916
|
-
|
|
917
|
-
```html
|
|
918
|
-
<a href="#" appPreventDefault (action)="handleClick($event)">Click me</a>
|
|
919
|
-
<input appPreventDefault eventType="keydown" preventKey="Enter" (action)="onEnter($event)" />
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
---
|
|
923
|
-
|
|
924
|
-
## 7. Guards
|
|
925
|
-
|
|
926
|
-
Route-level guards for permission-based access control. All guards deny access when permissions are not loaded (fail-closed).
|
|
927
|
-
|
|
928
|
-
### permissionGuard
|
|
929
|
-
|
|
930
|
-
Single permission or complex logic check.
|
|
931
|
-
|
|
932
|
-
```typescript
|
|
933
|
-
import { permissionGuard } from '@flusys/ng-shared';
|
|
934
|
-
|
|
935
|
-
const routes: Routes = [
|
|
936
|
-
// Simple permission
|
|
937
|
-
{ path: 'users', canActivate: [permissionGuard('user.view')] },
|
|
938
|
-
|
|
939
|
-
// Complex logic (ILogicNode)
|
|
940
|
-
{ path: 'admin', canActivate: [permissionGuard({
|
|
941
|
-
type: 'group',
|
|
942
|
-
operator: 'AND',
|
|
943
|
-
children: [
|
|
944
|
-
{ type: 'action', actionId: 'admin.view' },
|
|
945
|
-
{ type: 'action', actionId: 'admin.manage' },
|
|
946
|
-
],
|
|
947
|
-
})] },
|
|
948
|
-
|
|
949
|
-
// Custom redirect on deny
|
|
950
|
-
{ path: 'settings', canActivate: [permissionGuard('settings.view', '/access-denied')] },
|
|
951
|
-
];
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
### anyPermissionGuard
|
|
955
|
-
|
|
956
|
-
OR logic - allows access if user has ANY of the specified permissions.
|
|
957
|
-
|
|
958
|
-
```typescript
|
|
959
|
-
{ path: 'reports', canActivate: [anyPermissionGuard(['report.view', 'report.export'])] }
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
### allPermissionsGuard
|
|
963
|
-
|
|
964
|
-
AND logic - allows access only if user has ALL specified permissions.
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
{ path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
|
|
968
|
-
```
|
|
969
|
-
|
|
970
|
-
---
|
|
971
|
-
|
|
972
|
-
## 8. Utilities
|
|
973
|
-
|
|
974
|
-
### Permission Evaluator
|
|
975
|
-
|
|
976
|
-
Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards.
|
|
977
|
-
|
|
978
|
-
```typescript
|
|
979
|
-
import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions } from '@flusys/ng-shared';
|
|
980
|
-
|
|
981
|
-
const userPermissions = ['user.view', 'user.create'];
|
|
982
|
-
|
|
983
|
-
// Evaluate string or ILogicNode
|
|
984
|
-
evaluatePermission('user.view', userPermissions); // true
|
|
985
|
-
evaluatePermission(null, userPermissions); // false
|
|
986
|
-
|
|
987
|
-
// Evaluate ILogicNode tree recursively
|
|
988
|
-
evaluateLogicNode(logicNode, userPermissions);
|
|
989
|
-
|
|
990
|
-
// Simple OR/AND checks
|
|
991
|
-
hasAnyPermission(['user.view', 'user.delete'], userPermissions); // true (has user.view)
|
|
992
|
-
hasAllPermissions(['user.view', 'user.delete'], userPermissions); // false (missing user.delete)
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
### Scroll Pagination
|
|
996
|
-
|
|
997
|
-
Utility for lazy-loading dropdowns with scroll detection.
|
|
998
|
-
|
|
999
|
-
```typescript
|
|
1000
|
-
import { checkScrollPagination, ScrollPaginationConfig } from '@flusys/ng-shared';
|
|
1001
|
-
|
|
1002
|
-
// In a component
|
|
1003
|
-
onScroll(event: Event): void {
|
|
1004
|
-
const nextPagination = checkScrollPagination(event, {
|
|
1005
|
-
pagination: this.pagination(),
|
|
1006
|
-
total: this.total(),
|
|
1007
|
-
isLoading: this.isLoading(),
|
|
1008
|
-
threshold: 50, // pixels from bottom (default: 50)
|
|
1009
|
-
});
|
|
1010
|
-
if (nextPagination) {
|
|
1011
|
-
this.onPagination.emit(nextPagination);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
```
|
|
1015
|
-
|
|
1016
|
-
**Interface:** `ScrollPaginationConfig` - `{ threshold?, pagination, total, isLoading }`
|
|
1017
|
-
|
|
1018
|
-
**Returns:** `IPagination | null` - Next page pagination or null if not needed.
|
|
1019
|
-
|
|
1020
|
-
---
|
|
1021
|
-
|
|
1022
|
-
## 9. Classes
|
|
1023
|
-
|
|
1024
|
-
### BaseFormControl
|
|
1025
|
-
|
|
1026
|
-
Abstract base class for custom form controls. Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
|
|
1027
|
-
|
|
1028
|
-
```typescript
|
|
1029
|
-
import { BaseFormControl, provideValueAccessor } from '@flusys/ng-shared';
|
|
1030
|
-
|
|
1031
|
-
@Component({
|
|
1032
|
-
selector: 'my-select',
|
|
1033
|
-
providers: [provideValueAccessor(MySelectComponent)],
|
|
1034
|
-
})
|
|
1035
|
-
export class MySelectComponent extends BaseFormControl<string | null> {
|
|
1036
|
-
override readonly value = model<string | null>(null);
|
|
1037
|
-
|
|
1038
|
-
constructor() {
|
|
1039
|
-
super();
|
|
1040
|
-
this.initializeFormControl(); // Required for ControlValueAccessor sync
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Usage in all form patterns:
|
|
1045
|
-
// Template-driven: <my-select [(value)]="selectedId" />
|
|
1046
|
-
// Reactive forms: <my-select [formControl]="myControl" />
|
|
1047
|
-
// Signal forms: <my-select [formField]="formTree.myField" />
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
**Abstract property:** `value: ModelSignal<T>` - Must override with `model<T>()`
|
|
1051
|
-
|
|
1052
|
-
**Models:** `disabled` (boolean), `touched` (boolean)
|
|
1053
|
-
|
|
1054
|
-
**Methods:** `initializeFormControl()` (call in constructor), `markAsTouched()` (call on blur)
|
|
1055
|
-
|
|
1056
|
-
**Helper:** `provideValueAccessor(ComponentClass)` - Factory for `NG_VALUE_ACCESSOR` provider
|
|
1057
|
-
|
|
1058
|
-
### BaseFormPage
|
|
1059
|
-
|
|
1060
|
-
Abstract directive for form page components (create/edit).
|
|
1061
|
-
|
|
1062
|
-
```typescript
|
|
1063
|
-
import { BaseFormPage } from '@flusys/ng-shared';
|
|
1064
|
-
|
|
1065
|
-
@Component({ ... })
|
|
1066
|
-
export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
|
|
1067
|
-
private readonly productService = inject(ProductApiService);
|
|
1068
|
-
private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
|
|
1069
|
-
readonly formModel = this._formModel.asReadonly();
|
|
1070
|
-
|
|
1071
|
-
getFormModel(): Signal<IProductFormModel> { return this.formModel; }
|
|
1072
|
-
getResourceRoute(): string { return '/products'; }
|
|
1073
|
-
getResourceName(): string { return 'Product'; }
|
|
1074
|
-
isFormValid(): boolean { return this.formModel().name.trim().length > 0; }
|
|
1075
|
-
|
|
1076
|
-
loadItem(id: string): void {
|
|
1077
|
-
this.productService.findById(id).subscribe(res => {
|
|
1078
|
-
if (res.success && res.data) {
|
|
1079
|
-
this.existingItem.set(res.data);
|
|
1080
|
-
this._formModel.set({ name: res.data.name, price: res.data.price });
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
createItem(model: IProductFormModel): Observable<unknown> {
|
|
1086
|
-
return this.productService.insert(model);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
updateItem(model: IProductFormModel): Observable<unknown> {
|
|
1090
|
-
return this.productService.update({ id: this.existingItem()!.id, ...model });
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
**Signals:** `isLoading`, `existingItem`, `isEditMode` (computed)
|
|
1096
|
-
|
|
1097
|
-
**Methods:** `onSubmit()`, `onCancel()`, `showSuccess()`, `showError()`, `showValidationError()`
|
|
1098
|
-
|
|
1099
|
-
### BaseListPage
|
|
1100
|
-
|
|
1101
|
-
Abstract directive for list page components with pagination and CRUD operations.
|
|
1102
|
-
|
|
1103
|
-
```typescript
|
|
1104
|
-
import { BaseListPage } from '@flusys/ng-shared';
|
|
1105
|
-
|
|
1106
|
-
@Component({ ... })
|
|
1107
|
-
export class UserListComponent extends BaseListPage<IUser> {
|
|
1108
|
-
private readonly userService = inject(UserApiService);
|
|
1109
|
-
|
|
1110
|
-
getResourceRoute(): string { return '/users'; }
|
|
1111
|
-
getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
|
|
1112
|
-
|
|
1113
|
-
async loadData(): Promise<void> {
|
|
1114
|
-
this.isLoading.set(true);
|
|
1115
|
-
const res = await this.userService.findByIdAsync(...);
|
|
1116
|
-
this.items.set(res.data ?? []);
|
|
1117
|
-
this.total.set(res.meta?.total ?? 0);
|
|
1118
|
-
this.isLoading.set(false);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
```
|
|
1122
|
-
|
|
1123
|
-
**Signals:** `items`, `isLoading`, `total`, `pageSize`, `first`, `currentPage` (computed), `showCompanyInfo` (computed)
|
|
1124
|
-
|
|
1125
|
-
**Methods:** `onCreate()`, `onEdit(id)`, `onPageChange(event)`, `onDelete()`, `onDeleteAsync()`, `showSuccess()`, `showError()`, `showInfo()`, `showWarn()`
|
|
1126
|
-
|
|
1127
|
-
---
|
|
1128
|
-
|
|
1129
|
-
## 10. Modules
|
|
1130
|
-
|
|
1131
|
-
### AngularModule
|
|
1132
|
-
|
|
1133
|
-
Re-exports common Angular modules for convenience.
|
|
1134
|
-
|
|
1135
|
-
```typescript
|
|
1136
|
-
import { AngularModule } from '@flusys/ng-shared';
|
|
1137
|
-
// Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet, + directives
|
|
1138
|
-
// Providers: DatePipe
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
### PrimeModule
|
|
1142
|
-
|
|
1143
|
-
Re-exports PrimeNG component modules for convenience.
|
|
1144
|
-
|
|
1145
|
-
```typescript
|
|
1146
|
-
import { PrimeModule } from '@flusys/ng-shared';
|
|
1147
|
-
// Includes: Button, Table, Card, Dialog, InputText, Select, MultiSelect,
|
|
1148
|
-
// DatePicker, Checkbox, FileUpload, Image, Tag, Tabs, TreeTable, and more (27 modules)
|
|
1149
|
-
```
|
|
1150
|
-
|
|
1151
|
-
---
|
|
1152
|
-
|
|
1153
|
-
## 11. Provider Interfaces (Package Independence)
|
|
1154
|
-
|
|
1155
|
-
ng-shared defines **provider interfaces** to enable feature packages (ng-iam, ng-storage) to access auth functionality without direct dependencies.
|
|
1156
|
-
|
|
1157
|
-
### Architecture
|
|
1158
|
-
|
|
1159
|
-
```
|
|
1160
|
-
ng-shared (defines interfaces + tokens)
|
|
1161
|
-
|
|
|
1162
|
-
ng-auth (implements interfaces with adapters)
|
|
1163
|
-
|
|
|
1164
|
-
ng-iam/ng-storage (consume interfaces via DI)
|
|
1165
|
-
```
|
|
1166
|
-
|
|
1167
|
-
### Available Providers
|
|
1168
|
-
|
|
1169
|
-
#### IUserProvider / `USER_PROVIDER`
|
|
1170
|
-
|
|
1171
|
-
User list access for IAM user selection.
|
|
1172
|
-
|
|
1173
|
-
```typescript
|
|
1174
|
-
interface IUserBasicInfo { id: string; name: string; email: string }
|
|
1175
|
-
|
|
1176
|
-
interface IUserProvider {
|
|
1177
|
-
getUsers(filter?: {
|
|
1178
|
-
page?: number; pageSize?: number; search?: string;
|
|
1179
|
-
companyId?: string; branchId?: string;
|
|
1180
|
-
}): Observable<IListResponse<IUserBasicInfo>>;
|
|
1181
|
-
}
|
|
1182
|
-
```
|
|
1183
|
-
|
|
1184
|
-
#### ICompanyApiProvider / `COMPANY_API_PROVIDER`
|
|
1185
|
-
|
|
1186
|
-
Company list access for IAM company selection.
|
|
1187
|
-
|
|
1188
|
-
```typescript
|
|
1189
|
-
interface ICompanyBasicInfo { id: string; name: string; slug?: string }
|
|
1190
|
-
|
|
1191
|
-
interface ICompanyApiProvider {
|
|
1192
|
-
getCompanies(filter?: {
|
|
1193
|
-
page?: number; pageSize?: number; search?: string;
|
|
1194
|
-
}): Observable<IListResponse<ICompanyBasicInfo>>;
|
|
1195
|
-
}
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
|
-
#### IUserPermissionProvider / `USER_PERMISSION_PROVIDER`
|
|
1199
|
-
|
|
1200
|
-
User permission queries for IAM.
|
|
1201
|
-
|
|
1202
|
-
```typescript
|
|
1203
|
-
interface IUserPermissionProvider {
|
|
1204
|
-
getUserBranchPermissions(userId: string): Observable<ISingleResponse<any>>;
|
|
1205
|
-
}
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
#### IProfileUploadProvider / `PROFILE_UPLOAD_PROVIDER`
|
|
1209
|
-
|
|
1210
|
-
Profile picture upload for ng-auth profile page. Implemented by ng-storage.
|
|
1211
|
-
|
|
1212
|
-
```typescript
|
|
1213
|
-
interface IProfileUploadResult {
|
|
1214
|
-
id: string; // File manager ID (UUID)
|
|
1215
|
-
name: string; // Original file name
|
|
1216
|
-
key: string; // Storage key/path
|
|
1217
|
-
size: number; // File size in bytes
|
|
1218
|
-
contentType: string; // MIME type
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
interface IProfileUploadOptions {
|
|
1222
|
-
folderPath?: string;
|
|
1223
|
-
compress?: boolean;
|
|
1224
|
-
maxWidth?: number;
|
|
1225
|
-
maxHeight?: number;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
interface IProfileUploadProvider {
|
|
1229
|
-
uploadProfilePicture(
|
|
1230
|
-
file: File,
|
|
1231
|
-
options?: IProfileUploadOptions
|
|
1232
|
-
): Observable<ISingleResponse<IProfileUploadResult>>;
|
|
1233
|
-
}
|
|
1234
|
-
```
|
|
1235
|
-
|
|
1236
|
-
#### IProfilePermissionProvider / `PROFILE_PERMISSION_PROVIDER`
|
|
1237
|
-
|
|
1238
|
-
User permission queries for ng-auth profile page. Implemented by ng-iam.
|
|
1239
|
-
|
|
1240
|
-
```typescript
|
|
1241
|
-
interface IProfileRoleInfo {
|
|
1242
|
-
id: string;
|
|
1243
|
-
name: string;
|
|
1244
|
-
description?: string | null;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
interface IProfileActionInfo {
|
|
1248
|
-
id: string;
|
|
1249
|
-
code: string;
|
|
1250
|
-
name: string;
|
|
1251
|
-
description?: string | null;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
interface IProfilePermissionProvider {
|
|
1255
|
-
getUserRoles(userId: string, branchId?: string): Observable<ISingleResponse<IProfileRoleInfo[]>>;
|
|
1256
|
-
getUserActions(userId: string, branchId?: string): Observable<ISingleResponse<IProfileActionInfo[]>>;
|
|
1257
|
-
}
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
#### IAuthStateProvider / `AUTH_STATE_PROVIDER`
|
|
1261
|
-
|
|
1262
|
-
Auth state access for feature packages (form-builder, etc.) that need to check authentication without depending on ng-auth directly.
|
|
1263
|
-
|
|
1264
|
-
```typescript
|
|
1265
|
-
interface IAuthStateProvider {
|
|
1266
|
-
/** Signal indicating if user is currently authenticated */
|
|
1267
|
-
isAuthenticated: Signal<boolean>;
|
|
1268
|
-
|
|
1269
|
-
/** Initialize auth state (restore session from cookies/storage) */
|
|
1270
|
-
initialize(): Observable<void>;
|
|
1271
|
-
}
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
**Usage:**
|
|
1275
|
-
|
|
1276
|
-
```typescript
|
|
1277
|
-
import { AUTH_STATE_PROVIDER } from '@flusys/ng-shared';
|
|
1278
|
-
|
|
1279
|
-
@Component({...})
|
|
1280
|
-
export class PublicFormComponent {
|
|
1281
|
-
private readonly authState = inject(AUTH_STATE_PROVIDER);
|
|
1282
|
-
|
|
1283
|
-
ngOnInit() {
|
|
1284
|
-
this.authState.initialize().subscribe(() => {
|
|
1285
|
-
if (this.authState.isAuthenticated()) {
|
|
1286
|
-
// User is logged in
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
```
|
|
1292
|
-
|
|
1293
|
-
#### IUserListProvider / `USER_LIST_PROVIDER`
|
|
1294
|
-
|
|
1295
|
-
Extends user list pages with extra columns, actions, and data enrichment. Optional provider.
|
|
1296
|
-
|
|
1297
|
-
```typescript
|
|
1298
|
-
interface IUserListItem {
|
|
1299
|
-
id: string;
|
|
1300
|
-
name: string;
|
|
1301
|
-
email: string;
|
|
1302
|
-
phone?: string;
|
|
1303
|
-
isActive?: boolean;
|
|
1304
|
-
[key: string]: unknown;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
interface IUserListAction<T = IUserListItem> {
|
|
1308
|
-
id: string;
|
|
1309
|
-
label: string;
|
|
1310
|
-
icon?: string;
|
|
1311
|
-
severity?: 'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger';
|
|
1312
|
-
permission?: string;
|
|
1313
|
-
tooltip?: string;
|
|
1314
|
-
disabled?: boolean | ((user: T) => boolean);
|
|
1315
|
-
visible?: boolean | ((user: T) => boolean);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
interface IUserListColumn {
|
|
1319
|
-
field: string;
|
|
1320
|
-
header: string;
|
|
1321
|
-
width?: string;
|
|
1322
|
-
sortable?: boolean;
|
|
1323
|
-
templateType?: 'text' | 'badge' | 'date' | 'boolean' | 'custom';
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
interface IUserListProvider<T extends IUserListItem = IUserListItem> {
|
|
1327
|
-
getExtraColumns?(): IUserListColumn[];
|
|
1328
|
-
getExtraRowActions?(): IUserListAction<T>[];
|
|
1329
|
-
getExtraToolbarActions?(): IUserListAction<T>[];
|
|
1330
|
-
onRowAction?(actionId: string, user: T): void;
|
|
1331
|
-
onToolbarAction?(actionId: string, selectedUsers: T[]): void;
|
|
1332
|
-
enrichListData?(users: T[]): Observable<T[]>;
|
|
1333
|
-
}
|
|
1334
|
-
```
|
|
1335
|
-
|
|
1336
|
-
**Usage:**
|
|
1337
|
-
|
|
1338
|
-
```typescript
|
|
1339
|
-
// In app.config.ts
|
|
1340
|
-
providers: [
|
|
1341
|
-
{ provide: USER_LIST_PROVIDER, useClass: MyUserListProvider },
|
|
1342
|
-
]
|
|
1343
|
-
|
|
1344
|
-
// Implementation
|
|
1345
|
-
@Injectable({ providedIn: 'root' })
|
|
1346
|
-
export class MyUserListProvider implements IUserListProvider {
|
|
1347
|
-
getExtraRowActions() {
|
|
1348
|
-
return [
|
|
1349
|
-
{ id: 'assign-role', label: 'Assign Role', icon: 'pi pi-users' },
|
|
1350
|
-
];
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
```
|
|
1354
|
-
|
|
1355
|
-
### Usage in Consuming Packages
|
|
1356
|
-
|
|
1357
|
-
```typescript
|
|
1358
|
-
import { USER_PROVIDER } from '@flusys/ng-shared';
|
|
1359
|
-
|
|
1360
|
-
export class UserSelectorComponent {
|
|
1361
|
-
private readonly userProvider = inject(USER_PROVIDER);
|
|
1362
|
-
|
|
1363
|
-
loadUsers() {
|
|
1364
|
-
this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe(response => {
|
|
1365
|
-
this.users.set(response.data ?? []);
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
```
|
|
1370
|
-
|
|
1371
|
-
### Wiring in App
|
|
1372
|
-
|
|
1373
|
-
```typescript
|
|
1374
|
-
// app.config.ts
|
|
1375
|
-
import { provideAuthProviders } from '@flusys/ng-auth';
|
|
1376
|
-
|
|
1377
|
-
export const appConfig: ApplicationConfig = {
|
|
1378
|
-
providers: [
|
|
1379
|
-
...provideAuthProviders(), // Registers all auth adapters for provider tokens
|
|
1380
|
-
],
|
|
1381
|
-
};
|
|
1382
|
-
```
|
|
1383
|
-
|
|
1384
|
-
### See Also
|
|
1385
|
-
|
|
1386
|
-
- [AUTH-GUIDE.md](AUTH-GUIDE.md) - Adapter implementations
|
|
1387
|
-
- [IAM-GUIDE.md](IAM-GUIDE.md) - IAM usage examples
|
|
1388
|
-
- [../CLAUDE.md](../CLAUDE.md) - Complete pattern documentation
|
|
1389
|
-
|
|
1390
|
-
---
|
|
1391
|
-
|
|
1392
|
-
## Best Practices
|
|
1393
|
-
|
|
1394
|
-
### API Services
|
|
1395
|
-
|
|
1396
|
-
- **Extend `ApiResourceService`** for new services (signal-based)
|
|
1397
|
-
- Use reactive signals (`data`, `isLoading`, `total`) in templates
|
|
1398
|
-
- Use `fetchList()` to trigger queries, `reload()` to refresh
|
|
1399
|
-
- Use async methods (`insertAsync`, `updateAsync`) for one-off operations
|
|
1400
|
-
|
|
1401
|
-
### File URLs
|
|
1402
|
-
|
|
1403
|
-
- **Always use `FileUrlService`** - never construct URLs manually
|
|
1404
|
-
- Use `fetchSingleFileUrl()` for one-off fetches, `fetchFileUrls()` for batches
|
|
1405
|
-
- Use `fileUrlSignal()` for reactive template bindings
|
|
1406
|
-
|
|
1407
|
-
### Components
|
|
1408
|
-
|
|
1409
|
-
- Use `AngularModule` and `PrimeModule` for common imports
|
|
1410
|
-
- Use signal inputs via `input()` and `input.required()`
|
|
1411
|
-
- Components use `lib-` prefix for selectors
|
|
1412
|
-
|
|
1413
|
-
### Directives
|
|
1414
|
-
|
|
1415
|
-
- Directives use `app` prefix for selectors (`appPreventDefault`, `appEditModeElementChanger`)
|
|
1416
|
-
- Exception: `HasPermissionDirective` uses `[hasPermission]`
|
|
1417
|
-
- Exception: `IsEmptyImageDirective` uses `img` selector (auto-applies to all images)
|
|
1418
|
-
|
|
1419
|
-
### Permissions
|
|
1420
|
-
|
|
1421
|
-
- Use `HasPermissionDirective` for template-level permission checks
|
|
1422
|
-
- Use permission guards for route-level access control
|
|
1423
|
-
- Use `PermissionValidatorService` for programmatic checks in services
|
|
1424
|
-
- Permissions follow fail-closed model: no access by default
|
|
1425
|
-
|
|
1426
|
-
---
|
|
1427
|
-
|
|
1428
|
-
## Common Issues
|
|
1429
|
-
|
|
1430
|
-
### ApiResourceService Not Updating UI
|
|
1431
|
-
|
|
1432
|
-
**Problem:** UI doesn't update after operations.
|
|
1433
|
-
|
|
1434
|
-
**Solution:** Use signal syntax in templates:
|
|
1435
|
-
|
|
1436
|
-
```html
|
|
1437
|
-
<!-- Correct -->
|
|
1438
|
-
<div>{{ users() }}</div>
|
|
1439
|
-
|
|
1440
|
-
<!-- Wrong -->
|
|
1441
|
-
<div>{{ users }}</div>
|
|
1442
|
-
```
|
|
1443
|
-
|
|
1444
|
-
### FileUrlService Returns Error
|
|
1445
|
-
|
|
1446
|
-
**Problem:** File URL fetching fails.
|
|
1447
|
-
|
|
1448
|
-
**Solution:**
|
|
1449
|
-
|
|
1450
|
-
1. Ensure storage service is enabled in environment config
|
|
1451
|
-
2. Verify file ID exists in database
|
|
1452
|
-
3. Check storage provider configuration (local/S3/Azure)
|
|
1453
|
-
|
|
1454
|
-
### Circular Dependency with ng-layout
|
|
1455
|
-
|
|
1456
|
-
**Problem:** Build fails with circular dependency.
|
|
1457
|
-
|
|
1458
|
-
**Solution:** ng-shared must NEVER import from ng-layout. Move shared components to ng-shared, layout-specific components to ng-layout.
|
|
1459
|
-
|
|
1460
|
-
---
|
|
1461
|
-
|
|
1462
|
-
## API Reference
|
|
1463
|
-
|
|
1464
|
-
### Services
|
|
1465
|
-
|
|
1466
|
-
| Service | Description |
|
|
1467
|
-
| ------------------------------ | ------------------------------------- |
|
|
1468
|
-
| `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API (accepts optional `serviceName` for feature-specific URLs) |
|
|
1469
|
-
| `FileUrlService` | Cloud storage URL fetching |
|
|
1470
|
-
| `PermissionValidatorService` | Permission state management |
|
|
1471
|
-
| `CookieService` | SSR-aware cookie reading |
|
|
1472
|
-
| `PlatformService` | SSR environment detection |
|
|
1473
|
-
|
|
1474
|
-
### Classes
|
|
1475
|
-
|
|
1476
|
-
| Class | Description |
|
|
1477
|
-
| ------------------------ | -------------------------------------------- |
|
|
1478
|
-
| `ApiResourceService` | Signal-based CRUD base class (alias: `ApiService`) |
|
|
1479
|
-
| `BaseFormControl` | Abstract base for custom form controls |
|
|
1480
|
-
| `BaseFormPage` | Abstract directive for create/edit pages |
|
|
1481
|
-
| `BaseListPage` | Abstract directive for list pages |
|
|
1482
|
-
|
|
1483
|
-
### Constants
|
|
1484
|
-
|
|
1485
|
-
| Constant | Description |
|
|
1486
|
-
| -------------- | --------------------------------------------- |
|
|
1487
|
-
| `PERMISSIONS` | Aggregated permission codes by module |
|
|
1488
|
-
| `USER_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for users |
|
|
1489
|
-
| `ROLE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for roles |
|
|
1490
|
-
| `FILE_PERMISSIONS` | `{ CREATE, READ, UPDATE, DELETE }` for files |
|
|
1491
|
-
| `FILE_TYPE_FILTERS` | Predefined MIME type arrays (IMAGES, DOCUMENTS, etc.) |
|
|
1492
|
-
|
|
1493
|
-
### Components
|
|
1494
|
-
|
|
1495
|
-
| Component | Selector | Description |
|
|
1496
|
-
| -------------------------- | ---------------------- | ------------------------- |
|
|
1497
|
-
| `IconComponent` | `lib-icon` | Flexible icon renderer |
|
|
1498
|
-
| `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
|
|
1499
|
-
| `LazyMultiSelectComponent` | `lib-lazy-multi-select`| Lazy-loading multi-select |
|
|
1500
|
-
| `UserSelectComponent` | `lib-user-select` | Single user selector |
|
|
1501
|
-
| `UserMultiSelectComponent` | `lib-user-multi-select`| Multiple user selector |
|
|
1502
|
-
| `FileUploaderComponent` | `lib-file-uploader` | Drag & drop file upload |
|
|
1503
|
-
| `FileSelectorDialogComponent` | `lib-file-selector-dialog` | File browser dialog |
|
|
1504
|
-
|
|
1505
|
-
### Directives
|
|
1506
|
-
|
|
1507
|
-
| Directive | Selector | Description |
|
|
1508
|
-
| --------------------------------- | --------------------------- | ----------------------------------- |
|
|
1509
|
-
| `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
|
|
1510
|
-
| `EditModeElementChangerDirective` | `[appEditModeElementChanger]` | Toggle edit mode on form controls |
|
|
1511
|
-
| `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
|
|
1512
|
-
| `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
|
|
1513
|
-
|
|
1514
|
-
### Guards
|
|
1515
|
-
|
|
1516
|
-
| Guard | Description |
|
|
1517
|
-
| ---------------------- | -------------------------------------- |
|
|
1518
|
-
| `permissionGuard` | Single permission or ILogicNode check |
|
|
1519
|
-
| `anyPermissionGuard` | OR logic (any of listed permissions) |
|
|
1520
|
-
| `allPermissionsGuard` | AND logic (all of listed permissions) |
|
|
1521
|
-
|
|
1522
|
-
### Interfaces
|
|
1523
|
-
|
|
1524
|
-
| Interface | Description |
|
|
1525
|
-
| -------------------- | ------------------------------------ |
|
|
1526
|
-
| `IBaseEntity` | Base entity with ID and timestamps |
|
|
1527
|
-
| `ILoggedUserInfo` | Current user info with company ctx |
|
|
1528
|
-
| `IFilterData` | Filter, pagination, sort payload |
|
|
1529
|
-
| `IDeleteData` | Delete request payload |
|
|
1530
|
-
| `IDropDown` | Simple label/value pair |
|
|
1531
|
-
| `ISingleResponse<T>` | Single item response |
|
|
1532
|
-
| `IListResponse<T>` | List with pagination |
|
|
1533
|
-
| `IBulkResponse<T>` | Bulk operation response |
|
|
1534
|
-
| `IMessageResponse` | Message-only response |
|
|
1535
|
-
| `IErrorResponse` | Error with validation details |
|
|
1536
|
-
| `ILogicNode` | Permission logic tree (AND/OR nodes) |
|
|
1537
|
-
| `IUserSelectFilter` | User select filter params |
|
|
1538
|
-
| `LoadUsersFn` | User loading function type |
|
|
1539
|
-
| `IFileBasicInfo` | Basic file info for selectors |
|
|
1540
|
-
| `IFileUploadOptions`| Upload options (compression, etc.) |
|
|
1541
|
-
| `IUploadedFile` | Uploaded file response |
|
|
1542
|
-
| `IFileSelectFilter` | File select filter params |
|
|
1543
|
-
| `LoadFilesFn` | File loading function type |
|
|
1544
|
-
| `UploadFileFn` | File upload function type |
|
|
1545
|
-
| `IAuthStateProvider`| Auth state provider interface |
|
|
1546
|
-
| `IUserListProvider` | User list extensions provider |
|
|
1547
|
-
| `IUserListItem` | Base user for list operations |
|
|
1548
|
-
| `IUserListAction` | User list action definition |
|
|
1549
|
-
| `IUserListColumn` | Extra column for user list |
|
|
1550
|
-
| `ServiceName` | `'auth' \| 'administration' \| 'iam' \| 'storage' \| 'formBuilder' \| 'email'` |
|
|
1551
|
-
|
|
1552
|
-
### Injection Tokens
|
|
1553
|
-
|
|
1554
|
-
| Token | Interface | Description |
|
|
1555
|
-
| -------------------------- | ------------------------- | ---------------------------- |
|
|
1556
|
-
| `USER_PROVIDER` | `IUserProvider` | User list for IAM |
|
|
1557
|
-
| `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | Company list for IAM |
|
|
1558
|
-
| `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | User permission queries |
|
|
1559
|
-
| `PROFILE_UPLOAD_PROVIDER` | `IProfileUploadProvider` | Profile picture upload (ng-storage) |
|
|
1560
|
-
| `PROFILE_PERMISSION_PROVIDER` | `IProfilePermissionProvider` | User permissions for profile (ng-iam) |
|
|
1561
|
-
| `AUTH_STATE_PROVIDER` | `IAuthStateProvider` | Auth state for feature packages |
|
|
1562
|
-
| `USER_LIST_PROVIDER` | `IUserListProvider` | User list extensions (optional) |
|
|
1563
|
-
|
|
1564
|
-
## See Also
|
|
1565
|
-
|
|
1566
|
-
- **[CORE-GUIDE.md](./CORE-GUIDE.md)** - Configuration, interceptors
|
|
1567
|
-
- **[LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md)** - Layout system
|
|
1568
|
-
- **[AUTH-GUIDE.md](./AUTH-GUIDE.md)** - Auth adapters for provider interfaces
|
|
1569
|
-
- **[IAM-GUIDE.md](./IAM-GUIDE.md)** - IAM permission usage
|
|
1570
|
-
|
|
1571
|
-
---
|
|
163
|
+
## License
|
|
1572
164
|
|
|
1573
|
-
|
|
1574
|
-
**Version:** 1.1.0
|
|
1575
|
-
**Angular Version:** 21
|
|
165
|
+
MIT © FLUSYS
|