@flusys/ng-shared 1.1.1-beta → 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 +88 -1216
- package/fesm2022/flusys-ng-shared.mjs +3553 -2119
- package/fesm2022/flusys-ng-shared.mjs.map +1 -1
- package/package.json +9 -8
- package/types/flusys-ng-shared.d.ts +1023 -1181
package/README.md
CHANGED
|
@@ -1,1293 +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
|
-
- **Dependencies:** ng-core
|
|
13
|
-
- **Dependents:** ng-layout, ng-auth, ng-iam, ng-storage, flusysng
|
|
14
|
-
- **Build Command:** `npm run build:ng-shared`
|
|
5
|
+
[](https://www.npmjs.com/package/@flusys/ng-shared)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
15
7
|
|
|
16
8
|
---
|
|
17
9
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
### Data Interfaces
|
|
21
|
-
|
|
22
|
-
#### IBaseEntity
|
|
23
|
-
|
|
24
|
-
Base entity interface matching backend Identity entity.
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
interface IBaseEntity {
|
|
28
|
-
id: string;
|
|
29
|
-
createdAt: Date;
|
|
30
|
-
updatedAt: Date;
|
|
31
|
-
deletedAt?: Date | null;
|
|
32
|
-
createdById?: string | null;
|
|
33
|
-
updatedById?: string | null;
|
|
34
|
-
deletedById?: string | null;
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Entity Mixins:**
|
|
39
|
-
|
|
40
|
-
| Interface | Fields | Purpose |
|
|
41
|
-
| --------------- | ---------------------------------- | ---------------------- |
|
|
42
|
-
| `ISoftDeletable`| `deletedAt?: Date \| null` | Soft delete support |
|
|
43
|
-
| `ITimestampable`| `createdAt, updatedAt` | Timestamp tracking |
|
|
44
|
-
| `IActivatable` | `isActive: boolean` | Active status toggle |
|
|
45
|
-
| `IOrderable` | `serial?: number \| null` | Ordering/sorting |
|
|
46
|
-
| `IMetadata` | `metadata?: Record<string, unknown>` | JSON metadata field |
|
|
47
|
-
|
|
48
|
-
#### ILoggedUserInfo
|
|
10
|
+
## Installation
|
|
49
11
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
interface ILoggedUserInfo {
|
|
54
|
-
id: string;
|
|
55
|
-
email: string;
|
|
56
|
-
name?: string;
|
|
57
|
-
phone?: string;
|
|
58
|
-
profilePictureId?: string;
|
|
59
|
-
companyId?: string;
|
|
60
|
-
branchId?: string;
|
|
61
|
-
companyLogoId?: string;
|
|
62
|
-
branchLogoId?: string;
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
#### IFilterData
|
|
67
|
-
|
|
68
|
-
Filter, pagination, and sort payload for list queries.
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
interface IPagination {
|
|
72
|
-
pageSize: number;
|
|
73
|
-
currentPage: number;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface ISort { [key: string]: 'ASC' | 'DESC' }
|
|
77
|
-
interface IFilter { [key: string]: any }
|
|
78
|
-
|
|
79
|
-
interface IFilterData {
|
|
80
|
-
filter?: IFilter;
|
|
81
|
-
pagination?: IPagination;
|
|
82
|
-
select?: string[];
|
|
83
|
-
sort?: ISort;
|
|
84
|
-
withDeleted?: boolean;
|
|
85
|
-
extraKey?: string[];
|
|
86
|
-
}
|
|
12
|
+
```bash
|
|
13
|
+
npm install @flusys/ng-shared @flusys/ng-core
|
|
87
14
|
```
|
|
88
15
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Delete request payload matching backend DeleteDto.
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
type DeleteType = 'delete' | 'restore' | 'permanent';
|
|
95
|
-
|
|
96
|
-
interface IDeleteData {
|
|
97
|
-
id: string | string[]; // Single or batch delete
|
|
98
|
-
type: DeleteType;
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
#### IDropDown
|
|
103
|
-
|
|
104
|
-
Simple dropdown item for select components.
|
|
16
|
+
---
|
|
105
17
|
|
|
106
|
-
|
|
107
|
-
interface IDropDown {
|
|
108
|
-
label: string;
|
|
109
|
-
value: string;
|
|
110
|
-
}
|
|
111
|
-
```
|
|
18
|
+
## 1. ApiResourceService — Signal-Driven CRUD
|
|
112
19
|
|
|
113
|
-
|
|
20
|
+
Extend this class to get a reactive list resource backed by `resource()`, with pagination, search, and all standard POST-only RPC endpoints.
|
|
114
21
|
|
|
115
22
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
search: string;
|
|
120
|
-
[key: string]: unknown;
|
|
121
|
-
}
|
|
23
|
+
import { Injectable, inject } from "@angular/core";
|
|
24
|
+
import { HttpClient } from "@angular/common/http";
|
|
25
|
+
import { ApiResourceService } from "@flusys/ng-shared";
|
|
122
26
|
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
#### File Select Interfaces
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface IFileBasicInfo {
|
|
27
|
+
export interface IProduct {
|
|
130
28
|
id: string;
|
|
131
29
|
name: string;
|
|
132
|
-
|
|
133
|
-
size: string;
|
|
134
|
-
url: string | null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
interface IFileUploadOptions {
|
|
138
|
-
storageConfigId?: string;
|
|
139
|
-
folderPath?: string;
|
|
140
|
-
maxWidth?: number;
|
|
141
|
-
maxHeight?: number;
|
|
142
|
-
quality?: number;
|
|
143
|
-
compress?: boolean;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
interface IUploadedFile {
|
|
147
|
-
name: string;
|
|
148
|
-
key: string;
|
|
149
|
-
size: number;
|
|
150
|
-
contentType: string;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
interface IFileSelectFilter {
|
|
154
|
-
page: number;
|
|
155
|
-
pageSize: number;
|
|
156
|
-
search: string;
|
|
157
|
-
contentTypes?: string[];
|
|
158
|
-
folderId?: string;
|
|
159
|
-
[key: string]: unknown;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
type LoadFilesFn = (filter: IFileSelectFilter) => Observable<IListResponse<IFileBasicInfo>>;
|
|
163
|
-
type UploadFileFn = (file: File, options?: IFileUploadOptions) => Observable<ISingleResponse<IUploadedFile>>;
|
|
164
|
-
type GetFileUrlsFn = (fileIds: string[]) => Observable<ISingleResponse<IFileBasicInfo[]>>;
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**File Type Filters (Constants):**
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { FILE_TYPE_FILTERS, getAcceptString, isFileTypeAllowed, getFileIconClass, formatFileSize } from '@flusys/ng-shared';
|
|
171
|
-
|
|
172
|
-
FILE_TYPE_FILTERS.IMAGES // ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
|
|
173
|
-
FILE_TYPE_FILTERS.DOCUMENTS // ['application/pdf', 'application/msword', ...]
|
|
174
|
-
FILE_TYPE_FILTERS.VIDEOS // ['video/mp4', 'video/webm', ...]
|
|
175
|
-
FILE_TYPE_FILTERS.AUDIO // ['audio/mpeg', 'audio/wav', ...]
|
|
176
|
-
FILE_TYPE_FILTERS.ALL // [] (allows all)
|
|
177
|
-
|
|
178
|
-
getAcceptString(['image/*']) // 'image/*'
|
|
179
|
-
isFileTypeAllowed(file, ['image/*']) // true/false
|
|
180
|
-
getFileIconClass('image/png') // 'pi pi-image'
|
|
181
|
-
formatFileSize('1024') // '1 KB'
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Response Interfaces
|
|
185
|
-
|
|
186
|
-
Type-safe interfaces matching FLUSYS_NEST backend response DTOs.
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
// Single item response (POST /resource/insert, /update, /get/:id)
|
|
190
|
-
interface ISingleResponse<T> {
|
|
191
|
-
success: boolean;
|
|
192
|
-
message: string;
|
|
193
|
-
data?: T;
|
|
194
|
-
_meta?: IRequestMeta;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// List response with pagination (POST /resource/get-all)
|
|
198
|
-
interface IListResponse<T> {
|
|
199
|
-
success: boolean;
|
|
200
|
-
message: string;
|
|
201
|
-
data?: T[];
|
|
202
|
-
meta: IPaginationMeta;
|
|
203
|
-
_meta?: IRequestMeta;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Bulk operation response (POST /resource/insert-many, /update-many)
|
|
207
|
-
interface IBulkResponse<T> {
|
|
208
|
-
success: boolean;
|
|
209
|
-
message: string;
|
|
210
|
-
data?: T[];
|
|
211
|
-
meta: IBulkMeta;
|
|
212
|
-
_meta?: IRequestMeta;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Message-only response (POST /resource/delete, /logout)
|
|
216
|
-
interface IMessageResponse {
|
|
217
|
-
success: boolean;
|
|
218
|
-
message: string;
|
|
219
|
-
_meta?: IRequestMeta;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Error response (validation errors, exceptions)
|
|
223
|
-
interface IErrorResponse {
|
|
224
|
-
success: false;
|
|
225
|
-
message: string;
|
|
226
|
-
code?: string;
|
|
227
|
-
errors?: IValidationError[];
|
|
228
|
-
_meta?: IRequestMeta;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Union type
|
|
232
|
-
type ApiResponse<T> = ISingleResponse<T> | IListResponse<T> | IBulkResponse<T> | IMessageResponse | IErrorResponse;
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Metadata types:**
|
|
236
|
-
|
|
237
|
-
| Interface | Fields |
|
|
238
|
-
| ------------------ | -------------------------------------------- |
|
|
239
|
-
| `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
|
|
240
|
-
| `IPaginationMeta` | `total, page, pageSize, count, hasMore?, totalPages?` |
|
|
241
|
-
| `IBulkMeta` | `count, failed?, total?` |
|
|
242
|
-
| `IValidationError` | `field, message, constraint?` |
|
|
243
|
-
|
|
244
|
-
### Auth & Storage Response Types
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
interface ILoginResponse {
|
|
248
|
-
success: boolean;
|
|
249
|
-
message: string;
|
|
250
|
-
data: { accessToken: string; refreshToken: string; user: ILoginUserData };
|
|
30
|
+
price: number;
|
|
251
31
|
}
|
|
252
|
-
|
|
253
|
-
interface IRefreshTokenResponse {
|
|
254
|
-
success: boolean;
|
|
255
|
-
message: string;
|
|
256
|
-
data: { accessToken: string; refreshToken?: string };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
interface IFileData {
|
|
260
|
-
id: string;
|
|
32
|
+
export interface IProductDto {
|
|
261
33
|
name: string;
|
|
262
|
-
|
|
263
|
-
contentType: string;
|
|
264
|
-
size: number;
|
|
265
|
-
key: string;
|
|
266
|
-
url?: string;
|
|
267
|
-
thumbnailUrl?: string;
|
|
268
|
-
createdAt: Date;
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Permission Interfaces
|
|
273
|
-
|
|
274
|
-
Discriminated union for building complex permission logic trees.
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// Single permission check
|
|
278
|
-
interface IActionNode {
|
|
279
|
-
type: 'action';
|
|
280
|
-
actionId: string;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Group with AND/OR logic
|
|
284
|
-
interface IGroupNode {
|
|
285
|
-
type: 'group';
|
|
286
|
-
operator: 'AND' | 'OR';
|
|
287
|
-
children: ILogicNode[];
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Union type
|
|
291
|
-
type ILogicNode = IActionNode | IGroupNode;
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## 2. Enums
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
enum ContactTypeEnum {
|
|
300
|
-
PHONE = 1,
|
|
301
|
-
EMAIL = 2,
|
|
34
|
+
price: number;
|
|
302
35
|
}
|
|
303
36
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## 3. Services
|
|
314
|
-
|
|
315
|
-
### ApiResourceService
|
|
316
|
-
|
|
317
|
-
Signal-based CRUD service using Angular 21 `resource()` API. All endpoints use POST (RPC-style).
|
|
318
|
-
|
|
319
|
-
**Define a service:**
|
|
320
|
-
|
|
321
|
-
```typescript
|
|
322
|
-
import { Injectable } from '@angular/core';
|
|
323
|
-
import { HttpClient } from '@angular/common/http';
|
|
324
|
-
import { ApiResourceService } from '@flusys/ng-shared';
|
|
325
|
-
|
|
326
|
-
@Injectable({ providedIn: 'root' })
|
|
327
|
-
export class UserService extends ApiResourceService<UserDto, IUser> {
|
|
328
|
-
constructor(http: HttpClient) {
|
|
329
|
-
super('users', http); // Base URL: APP_CONFIG.apiBaseUrl + '/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)
|
|
330
41
|
}
|
|
331
42
|
}
|
|
332
43
|
```
|
|
333
44
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
| Method | HTTP | Endpoint | Response |
|
|
337
|
-
| -------------- | ---- | ------------------------- | ------------------------ |
|
|
338
|
-
| `insert(dto)` | POST | `/{resource}/insert` | `ISingleResponse<T>` |
|
|
339
|
-
| `insertMany(dtos)` | POST | `/{resource}/insert-many` | `IBulkResponse<T>` |
|
|
340
|
-
| `findById(id, select?)` | POST | `/{resource}/get/:id` | `ISingleResponse<T>` |
|
|
341
|
-
| `getAll(search, filter)` | POST | `/{resource}/get-all?q=` | `IListResponse<T>` |
|
|
342
|
-
| `update(dto)` | POST | `/{resource}/update` | `ISingleResponse<T>` |
|
|
343
|
-
| `updateMany(dtos)` | POST | `/{resource}/update-many` | `IBulkResponse<T>` |
|
|
344
|
-
| `delete(deleteDto)` | POST | `/{resource}/delete` | `IMessageResponse` |
|
|
345
|
-
|
|
346
|
-
All methods above return `Observable`. Async (Promise) variants are also available: `insertAsync`, `insertManyAsync`, `findByIdAsync`, `updateAsync`, `updateManyAsync`, `deleteAsync`.
|
|
347
|
-
|
|
348
|
-
**Reactive signals:**
|
|
349
|
-
|
|
350
|
-
| Signal | Type | Description |
|
|
351
|
-
| ------------ | ------------------------- | ------------------------ |
|
|
352
|
-
| `isLoading` | `Signal<boolean>` | Whether data is loading |
|
|
353
|
-
| `data` | `Signal<T[]>` | Current list data |
|
|
354
|
-
| `total` | `Signal<number>` | Total item count |
|
|
355
|
-
| `pageInfo` | `Signal<IPaginationMeta>` | Pagination metadata |
|
|
356
|
-
| `hasMore` | `Signal<boolean>` | More pages available |
|
|
357
|
-
| `searchTerm` | `WritableSignal<string>` | Current search term |
|
|
358
|
-
| `filterData` | `WritableSignal<IFilterData>` | Filter/pagination state |
|
|
359
|
-
|
|
360
|
-
**List management:**
|
|
45
|
+
Using the reactive list in a component:
|
|
361
46
|
|
|
362
47
|
```typescript
|
|
363
|
-
|
|
364
|
-
|
|
48
|
+
@Component({ ... })
|
|
49
|
+
export class ProductListComponent {
|
|
50
|
+
private readonly svc = inject(ProductService);
|
|
365
51
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
userService.resetPagination();
|
|
370
|
-
userService.reload();
|
|
371
|
-
```
|
|
52
|
+
readonly items = this.svc.data; // Signal<IProduct[]>
|
|
53
|
+
readonly total = this.svc.total; // Signal<number>
|
|
54
|
+
readonly loading = this.svc.isLoading; // Signal<boolean>
|
|
372
55
|
|
|
373
|
-
|
|
56
|
+
ngOnInit() { this.svc.fetchList(); }
|
|
374
57
|
|
|
375
|
-
|
|
376
|
-
@Component({...})
|
|
377
|
-
export class UserListComponent {
|
|
378
|
-
private readonly userService = inject(UserService);
|
|
58
|
+
onSearch(q: string) { this.svc.fetchList(q); }
|
|
379
59
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
60
|
+
onPageChange(page: number, size: number) {
|
|
61
|
+
this.svc.setPagination({ currentPage: page, pageSize: size });
|
|
62
|
+
}
|
|
383
63
|
|
|
384
|
-
|
|
385
|
-
this.
|
|
64
|
+
async onCreate(dto: IProductDto) {
|
|
65
|
+
await this.svc.insert(dto);
|
|
66
|
+
this.svc.reload();
|
|
386
67
|
}
|
|
387
68
|
|
|
388
|
-
async
|
|
389
|
-
await this.
|
|
390
|
-
this.
|
|
69
|
+
async onDelete(id: string) {
|
|
70
|
+
await this.svc.delete({ id });
|
|
71
|
+
this.svc.reload();
|
|
391
72
|
}
|
|
392
73
|
}
|
|
393
74
|
```
|
|
394
75
|
|
|
395
|
-
|
|
76
|
+
Available CRUD methods: `insert`, `insertMany`, `findById`, `findByIds`, `getAll`, `getByFilter`, `update`, `updateMany`, `bulkUpsert`, `delete`.
|
|
396
77
|
|
|
397
|
-
|
|
78
|
+
---
|
|
398
79
|
|
|
399
|
-
|
|
80
|
+
## 2. FileUrlService — Presigned URL Fetching
|
|
400
81
|
|
|
401
|
-
|
|
82
|
+
Never construct file URLs manually. `FileUrlService` handles S3/Azure presigned URLs, caching, and deduplication.
|
|
402
83
|
|
|
403
84
|
```typescript
|
|
404
85
|
import { FileUrlService } from '@flusys/ng-shared';
|
|
405
86
|
|
|
406
|
-
@Component({...})
|
|
407
|
-
export class
|
|
87
|
+
@Component({ ... })
|
|
88
|
+
export class AvatarComponent {
|
|
408
89
|
private readonly fileUrlService = inject(FileUrlService);
|
|
90
|
+
readonly avatarUrl = signal<string | null>(null);
|
|
409
91
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
this.imageUrl.set(file?.url ?? null);
|
|
92
|
+
ngOnInit() {
|
|
93
|
+
this.fileUrlService.fetchSingleFileUrl('file-id-123').subscribe(file => {
|
|
94
|
+
this.avatarUrl.set(file?.url ?? null);
|
|
414
95
|
});
|
|
415
96
|
}
|
|
416
97
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
98
|
+
// Batch fetch — only requests IDs not already cached
|
|
99
|
+
loadMultiple(ids: string[]) {
|
|
100
|
+
this.fileUrlService.fetchFileUrls(ids).subscribe(files => { /* ... */ });
|
|
421
101
|
}
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
**Methods:**
|
|
426
|
-
|
|
427
|
-
| Method | Returns | Description |
|
|
428
|
-
| ----------------------------- | ----------------------------------- | ---------------------------------- |
|
|
429
|
-
| `getFileUrl(fileId)` | `string \| null` | Get cached URL (synchronous) |
|
|
430
|
-
| `fileUrlSignal(fileId)` | `Signal<string \| null>` | Computed signal from cache |
|
|
431
|
-
| `fetchFileUrls(fileIds[])` | `Observable<FilesResponseDto[]>` | Fetch from backend, updates cache |
|
|
432
|
-
| `fetchSingleFileUrl(fileId)` | `Observable<FilesResponseDto \| null>` | Fetch single file |
|
|
433
|
-
| `clearCache()` | `void` | Clear all cached URLs |
|
|
434
|
-
| `removeFromCache(fileId)` | `void` | Remove specific entry from cache |
|
|
435
|
-
|
|
436
|
-
### PermissionValidatorService
|
|
437
|
-
|
|
438
|
-
Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM.
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
import { PermissionValidatorService } from '@flusys/ng-shared';
|
|
442
|
-
|
|
443
|
-
@Component({...})
|
|
444
|
-
export class MyComponent {
|
|
445
|
-
private readonly permissionValidator = inject(PermissionValidatorService);
|
|
446
102
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.
|
|
450
|
-
|
|
451
|
-
// Check single permission
|
|
452
|
-
if (this.permissionValidator.hasPermission('user.view')) {
|
|
453
|
-
// User has permission
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Check loaded state
|
|
457
|
-
if (this.permissionValidator.isPermissionsLoaded()) {
|
|
458
|
-
// Permissions have been loaded
|
|
459
|
-
}
|
|
103
|
+
// Synchronous read from cache after a prior fetch
|
|
104
|
+
getCachedUrl(id: string): string | null {
|
|
105
|
+
return this.fileUrlService.getFileUrl(id);
|
|
460
106
|
}
|
|
461
107
|
}
|
|
462
108
|
```
|
|
463
109
|
|
|
464
|
-
**Methods:**
|
|
465
|
-
|
|
466
|
-
| Method | Returns | Description |
|
|
467
|
-
| ---------------------------- | --------- | ---------------------------------- |
|
|
468
|
-
| `setPermissions(codes[])` | `void` | Replace all permissions |
|
|
469
|
-
| `clearPermissions()` | `void` | Clear all permissions |
|
|
470
|
-
| `hasPermission(code)` | `boolean` | Check single permission |
|
|
471
|
-
| `isPermissionsLoaded()` | `boolean` | Whether permissions have been set |
|
|
472
|
-
|
|
473
|
-
**Signals:**
|
|
474
|
-
|
|
475
|
-
| Signal | Type | Description |
|
|
476
|
-
| ------------- | ------------------ | ------------------------------ |
|
|
477
|
-
| `permissions` | `Signal<string[]>` | Readonly current permissions |
|
|
478
|
-
|
|
479
|
-
### CookieService
|
|
480
|
-
|
|
481
|
-
SSR-aware cookie reading service.
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
import { CookieService } from '@flusys/ng-shared';
|
|
485
|
-
|
|
486
|
-
const cookies = inject(CookieService).get(); // Returns document.cookie (browser) or request header cookie (server)
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### PlatformService
|
|
490
|
-
|
|
491
|
-
SSR environment detection service.
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
import { PlatformService } from '@flusys/ng-shared';
|
|
495
|
-
|
|
496
|
-
const platform = inject(PlatformService);
|
|
497
|
-
if (!platform.isServer) {
|
|
498
|
-
// Browser-only code (localStorage, window, etc.)
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
110
|
---
|
|
503
111
|
|
|
504
|
-
##
|
|
505
|
-
|
|
506
|
-
### IconComponent
|
|
507
|
-
|
|
508
|
-
Flexible icon renderer supporting PrimeNG icons, image files, and SVG.
|
|
509
|
-
|
|
510
|
-
- **Selector:** `lib-icon`
|
|
511
|
-
- **Inputs:** `icon` (required string), `iconType` (optional `IconTypeEnum`, default: `PRIMENG_ICON`)
|
|
512
|
-
|
|
513
|
-
```html
|
|
514
|
-
<!-- PrimeNG icon (default) -->
|
|
515
|
-
<lib-icon icon="pi pi-user" />
|
|
516
|
-
|
|
517
|
-
<!-- Image file -->
|
|
518
|
-
<lib-icon icon="/assets/logo.png" [iconType]="IconTypeEnum.IMAGE_FILE_LINK" />
|
|
519
|
-
|
|
520
|
-
<!-- SVG tag -->
|
|
521
|
-
<lib-icon icon="<svg>...</svg>" [iconType]="IconTypeEnum.DIRECT_TAG_SVG" />
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### LazySelectComponent
|
|
525
|
-
|
|
526
|
-
Single-select dropdown with lazy loading, search, and scroll pagination.
|
|
527
|
-
|
|
528
|
-
- **Selector:** `lib-lazy-select`
|
|
529
|
-
- **Extends:** `BaseFormControl<string | null>`
|
|
530
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
112
|
+
## 3. PermissionValidatorService + HasPermissionDirective
|
|
531
113
|
|
|
532
114
|
```typescript
|
|
533
|
-
|
|
534
|
-
imports: [LazySelectComponent],
|
|
535
|
-
template: `
|
|
536
|
-
<lib-lazy-select
|
|
537
|
-
[(value)]="selectedId"
|
|
538
|
-
[selectDataList]="items()"
|
|
539
|
-
[optionLabel]="'label'"
|
|
540
|
-
[optionValue]="'value'"
|
|
541
|
-
[isEditMode]="true"
|
|
542
|
-
[isLoading]="loading()"
|
|
543
|
-
[total]="total()"
|
|
544
|
-
[pagination]="pagination()"
|
|
545
|
-
[placeHolder]="'Select item'"
|
|
546
|
-
(onSearch)="handleSearch($event)"
|
|
547
|
-
(onPagination)="handlePagination($event)"
|
|
548
|
-
/>
|
|
549
|
-
`,
|
|
550
|
-
})
|
|
551
|
-
export class MyComponent {
|
|
552
|
-
readonly selectedId = signal<string | null>(null);
|
|
553
|
-
readonly items = signal<IDropDown[]>([]);
|
|
554
|
-
readonly loading = signal(false);
|
|
555
|
-
readonly total = signal<number | undefined>(undefined);
|
|
556
|
-
readonly pagination = signal<IPagination>({ currentPage: 0, pageSize: 20 });
|
|
557
|
-
}
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
**Inputs:**
|
|
561
|
-
|
|
562
|
-
| Input | Type | Description |
|
|
563
|
-
| ---------------- | ----------------- | -------------------------------- |
|
|
564
|
-
| `selectDataList` | `Array<IDropDown>` | Dropdown options (required) |
|
|
565
|
-
| `optionLabel` | `string` | Label field name (required) |
|
|
566
|
-
| `optionValue` | `string` | Value field name (required) |
|
|
567
|
-
| `isEditMode` | `boolean` | Enable/disable editing (required)|
|
|
568
|
-
| `isLoading` | `boolean` | Loading state (required) |
|
|
569
|
-
| `total` | `number \| undefined` | Total items for pagination |
|
|
570
|
-
| `pagination` | `IPagination` | Current pagination state |
|
|
571
|
-
| `placeHolder` | `string` | Placeholder text (default: `'Select Option'`) |
|
|
572
|
-
|
|
573
|
-
**Model:** `value` - Two-way bound selected value (`string | null`)
|
|
574
|
-
|
|
575
|
-
**Outputs:** `onSearch` (debounced 500ms), `onPagination` (scroll-triggered)
|
|
576
|
-
|
|
577
|
-
### LazyMultiSelectComponent
|
|
578
|
-
|
|
579
|
-
Multi-select dropdown with lazy loading, search, select-all, and scroll pagination.
|
|
580
|
-
|
|
581
|
-
- **Selector:** `lib-lazy-multi-select`
|
|
582
|
-
- **Extends:** `BaseFormControl<string[] | null>`
|
|
583
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
115
|
+
const pv = inject(PermissionValidatorService);
|
|
584
116
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
template: `
|
|
589
|
-
<lib-lazy-multi-select
|
|
590
|
-
[(value)]="selectedIds"
|
|
591
|
-
[selectDataList]="items()"
|
|
592
|
-
[isEditMode]="true"
|
|
593
|
-
[isLoading]="loading()"
|
|
594
|
-
[total]="total()"
|
|
595
|
-
[pagination]="pagination()"
|
|
596
|
-
[placeHolder]="'Select items'"
|
|
597
|
-
(onSearch)="handleSearch($event)"
|
|
598
|
-
(onPagination)="handlePagination($event)"
|
|
599
|
-
/>
|
|
600
|
-
`,
|
|
601
|
-
})
|
|
602
|
-
export class MyComponent {
|
|
603
|
-
readonly selectedIds = signal<string[] | null>(null);
|
|
604
|
-
}
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
**Inputs:** `selectDataList`, `isEditMode`, `isLoading`, `total`, `pagination`, `placeHolder` (same as `LazySelectComponent`). Does not have `optionLabel`/`optionValue` (uses `IDropDown.label`/`IDropDown.value` directly).
|
|
608
|
-
|
|
609
|
-
**Model:** `value` - Two-way bound selected values (`string[] | null`)
|
|
610
|
-
|
|
611
|
-
**Computed signals:** `selectedValueDisplay` (display text), `isSelectAll` (all items selected)
|
|
612
|
-
|
|
613
|
-
### UserSelectComponent
|
|
614
|
-
|
|
615
|
-
Single user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
|
|
616
|
-
|
|
617
|
-
- **Selector:** `lib-user-select`
|
|
618
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
@Component({
|
|
622
|
-
imports: [UserSelectComponent],
|
|
623
|
-
template: `
|
|
624
|
-
<!-- Simple usage - uses USER_PROVIDER internally -->
|
|
625
|
-
<lib-user-select
|
|
626
|
-
[(value)]="selectedUserId"
|
|
627
|
-
[isEditMode]="true"
|
|
628
|
-
/>
|
|
629
|
-
|
|
630
|
-
<!-- With custom loadUsers function -->
|
|
631
|
-
<lib-user-select
|
|
632
|
-
[(value)]="selectedUserId"
|
|
633
|
-
[isEditMode]="true"
|
|
634
|
-
[loadUsers]="customLoadUsers"
|
|
635
|
-
/>
|
|
636
|
-
`,
|
|
637
|
-
})
|
|
638
|
-
export class MyComponent {
|
|
639
|
-
readonly selectedUserId = signal<string | null>(null);
|
|
640
|
-
}
|
|
117
|
+
pv.hasPermission("user.create"); // true
|
|
118
|
+
pv.hasPermission("report.export"); // true — wildcard match
|
|
119
|
+
pv.isPermissionsLoaded(); // boolean
|
|
641
120
|
```
|
|
642
121
|
|
|
643
|
-
**Inputs:**
|
|
644
|
-
|
|
645
|
-
| Input | Type | Default | Description |
|
|
646
|
-
| ----- | ---- | ------- | ----------- |
|
|
647
|
-
| `isEditMode` | `boolean` | required | Enable/disable editing |
|
|
648
|
-
| `placeHolder` | `string` | `'Select User'` | Placeholder text |
|
|
649
|
-
| `filterActive` | `boolean` | `true` | Filter active users only |
|
|
650
|
-
| `additionalFilters` | `Record<string, unknown>` | `{}` | Extra filter params |
|
|
651
|
-
| `pageSize` | `number` | `20` | Page size for pagination |
|
|
652
|
-
| `loadUsers` | `LoadUsersFn` | - | Custom user loading function |
|
|
653
|
-
|
|
654
|
-
**Model:** `value` - Two-way bound selected user ID (`string | null`)
|
|
655
|
-
|
|
656
|
-
**Outputs:** `userSelected` (IUserBasicInfo | null), `onError` (Error)
|
|
657
|
-
|
|
658
|
-
### UserMultiSelectComponent
|
|
659
|
-
|
|
660
|
-
Multiple user selection with lazy loading. Uses `USER_PROVIDER` internally or accepts custom `loadUsers` function.
|
|
661
|
-
|
|
662
|
-
- **Selector:** `lib-user-multi-select`
|
|
663
|
-
- **Supports:** Template-driven, reactive forms, signal forms
|
|
664
|
-
|
|
665
|
-
```typescript
|
|
666
|
-
@Component({
|
|
667
|
-
imports: [UserMultiSelectComponent],
|
|
668
|
-
template: `
|
|
669
|
-
<lib-user-multi-select
|
|
670
|
-
[(value)]="selectedUserIds"
|
|
671
|
-
[isEditMode]="true"
|
|
672
|
-
/>
|
|
673
|
-
`,
|
|
674
|
-
})
|
|
675
|
-
export class MyComponent {
|
|
676
|
-
readonly selectedUserIds = signal<string[] | null>(null);
|
|
677
|
-
}
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
**Inputs:** Same as `UserSelectComponent`.
|
|
681
|
-
|
|
682
|
-
**Model:** `value` - Two-way bound selected user IDs (`string[] | null`)
|
|
683
|
-
|
|
684
|
-
**Outputs:** `usersSelected` (IUserBasicInfo[]), `onError` (Error)
|
|
685
|
-
|
|
686
|
-
### FileUploaderComponent
|
|
687
|
-
|
|
688
|
-
Drag & drop file upload with type filtering. Pass your own `uploadFile` function.
|
|
689
|
-
|
|
690
|
-
- **Selector:** `lib-file-uploader`
|
|
691
|
-
|
|
692
|
-
```typescript
|
|
693
|
-
@Component({
|
|
694
|
-
imports: [FileUploaderComponent],
|
|
695
|
-
template: `
|
|
696
|
-
<!-- Single image upload -->
|
|
697
|
-
<lib-file-uploader
|
|
698
|
-
[uploadFile]="uploadFile"
|
|
699
|
-
[acceptTypes]="['image/*']"
|
|
700
|
-
[multiple]="false"
|
|
701
|
-
(fileUploaded)="onFileUploaded($event)"
|
|
702
|
-
/>
|
|
703
|
-
|
|
704
|
-
<!-- Multiple document upload -->
|
|
705
|
-
<lib-file-uploader
|
|
706
|
-
[uploadFile]="uploadFile"
|
|
707
|
-
[acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
|
|
708
|
-
[multiple]="true"
|
|
709
|
-
[maxFiles]="5"
|
|
710
|
-
(filesUploaded)="onFilesUploaded($event)"
|
|
711
|
-
/>
|
|
712
|
-
`,
|
|
713
|
-
})
|
|
714
|
-
export class MyComponent {
|
|
715
|
-
readonly uploadService = inject(UploadService);
|
|
716
|
-
|
|
717
|
-
readonly uploadFile: UploadFileFn = (file, options) =>
|
|
718
|
-
this.uploadService.uploadSingleFile(file, options);
|
|
719
|
-
}
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
**Inputs:**
|
|
723
|
-
|
|
724
|
-
| Input | Type | Default | Description |
|
|
725
|
-
| ----- | ---- | ------- | ----------- |
|
|
726
|
-
| `uploadFile` | `UploadFileFn` | required | Upload function |
|
|
727
|
-
| `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
|
|
728
|
-
| `multiple` | `boolean` | `false` | Allow multiple files |
|
|
729
|
-
| `maxFiles` | `number` | `10` | Max files for multiple |
|
|
730
|
-
| `maxSizeMb` | `number` | `10` | Max file size in MB |
|
|
731
|
-
| `uploadOptions` | `IFileUploadOptions` | `{}` | Upload options |
|
|
732
|
-
| `disabled` | `boolean` | `false` | Disable uploader |
|
|
733
|
-
| `showPreview` | `boolean` | `true` | Show selected files preview |
|
|
734
|
-
| `autoUpload` | `boolean` | `true` | Upload immediately on selection |
|
|
735
|
-
|
|
736
|
-
**Outputs:** `fileUploaded` (IUploadedFile), `filesUploaded` (IUploadedFile[]), `onError` (Error), `fileSelected` (File[])
|
|
737
|
-
|
|
738
|
-
### FileSelectorDialogComponent
|
|
739
|
-
|
|
740
|
-
Dialog to browse and select existing files with filtering.
|
|
741
|
-
|
|
742
|
-
- **Selector:** `lib-file-selector-dialog`
|
|
743
|
-
|
|
744
|
-
```typescript
|
|
745
|
-
@Component({
|
|
746
|
-
imports: [FileSelectorDialogComponent],
|
|
747
|
-
template: `
|
|
748
|
-
<lib-file-selector-dialog
|
|
749
|
-
[(visible)]="showFileSelector"
|
|
750
|
-
[loadFiles]="loadFiles"
|
|
751
|
-
[acceptTypes]="['image/*']"
|
|
752
|
-
[multiple]="false"
|
|
753
|
-
(fileSelected)="onFileSelected($event)"
|
|
754
|
-
/>
|
|
755
|
-
`,
|
|
756
|
-
})
|
|
757
|
-
export class MyComponent {
|
|
758
|
-
readonly showFileSelector = signal(false);
|
|
759
|
-
readonly fileService = inject(FileManagerApiService);
|
|
760
|
-
|
|
761
|
-
readonly loadFiles: LoadFilesFn = (filter) =>
|
|
762
|
-
this.fileService.getAll(filter.search, {
|
|
763
|
-
pagination: { currentPage: filter.page, pageSize: filter.pageSize },
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
**Inputs:**
|
|
769
|
-
|
|
770
|
-
| Input | Type | Default | Description |
|
|
771
|
-
| ----- | ---- | ------- | ----------- |
|
|
772
|
-
| `loadFiles` | `LoadFilesFn` | required | File loading function |
|
|
773
|
-
| `header` | `string` | `'Select File'` | Dialog header |
|
|
774
|
-
| `acceptTypes` | `string[]` | `[]` | Allowed MIME types |
|
|
775
|
-
| `multiple` | `boolean` | `false` | Allow multiple selection |
|
|
776
|
-
| `maxSelection` | `number` | `10` | Max files for multiple |
|
|
777
|
-
| `pageSize` | `number` | `20` | Page size for pagination |
|
|
778
|
-
|
|
779
|
-
**Model:** `visible` - Two-way bound dialog visibility (`boolean`)
|
|
780
|
-
|
|
781
|
-
**Outputs:** `fileSelected` (IFileBasicInfo), `filesSelected` (IFileBasicInfo[]), `closed` (void), `onError` (Error)
|
|
782
|
-
|
|
783
|
-
---
|
|
784
|
-
|
|
785
|
-
## 5. Directives
|
|
786
|
-
|
|
787
|
-
### HasPermissionDirective
|
|
788
|
-
|
|
789
|
-
Structural directive for permission-based rendering. Fail-closed: hides content when permissions not loaded.
|
|
790
|
-
|
|
791
|
-
- **Selector:** `[hasPermission]`
|
|
792
|
-
- **Input:** `hasPermission` - `string | ILogicNode | null`
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
import { HasPermissionDirective, ILogicNode } from '@flusys/ng-shared';
|
|
796
|
-
|
|
797
|
-
@Component({
|
|
798
|
-
imports: [HasPermissionDirective],
|
|
799
|
-
template: `
|
|
800
|
-
<!-- Simple permission check -->
|
|
801
|
-
<button *hasPermission="'user.create'">Create User</button>
|
|
802
|
-
|
|
803
|
-
<!-- Complex AND/OR logic -->
|
|
804
|
-
<div *hasPermission="editLogic">Edit Panel</div>
|
|
805
|
-
`,
|
|
806
|
-
})
|
|
807
|
-
export class MyComponent {
|
|
808
|
-
readonly editLogic: ILogicNode = {
|
|
809
|
-
type: 'group',
|
|
810
|
-
operator: 'AND',
|
|
811
|
-
children: [
|
|
812
|
-
{ type: 'action', actionId: 'user.view' },
|
|
813
|
-
{ type: 'action', actionId: 'user.update' },
|
|
814
|
-
],
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
### EditModeElementChangerDirective
|
|
820
|
-
|
|
821
|
-
Toggles readonly/disabled state for form controls based on edit mode. Supports `<input>`, `<p-select>`, and `<p-calendar>`.
|
|
822
|
-
|
|
823
|
-
- **Selector:** `[appEditModeElementChanger]`
|
|
824
|
-
- **Input:** `isEditMode` (required boolean)
|
|
825
|
-
|
|
826
|
-
```html
|
|
827
|
-
<input [appEditModeElementChanger] [isEditMode]="isEditing()" />
|
|
828
|
-
<p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
### IsEmptyImageDirective
|
|
832
|
-
|
|
833
|
-
Automatically replaces broken or empty image `src` with a default fallback image.
|
|
834
|
-
|
|
835
|
-
- **Selector:** `img` (applies to all `<img>` elements)
|
|
836
|
-
- **Input:** `src` (standard img src)
|
|
837
|
-
- **Default image:** `lib/assets/images/default/default-image.jpg`
|
|
838
|
-
|
|
839
|
-
```html
|
|
840
|
-
<img [src]="imageUrl()" alt="Product" />
|
|
841
|
-
<!-- Falls back to default image on error or empty src -->
|
|
842
|
-
```
|
|
843
|
-
|
|
844
|
-
### PreventDefaultDirective
|
|
845
|
-
|
|
846
|
-
Prevents default browser behavior on specified events and emits the event.
|
|
847
|
-
|
|
848
|
-
- **Selector:** `[appPreventDefault]`
|
|
849
|
-
- **Inputs:** `eventType` (`'click' | 'keydown' | 'keyup'`, default: `'click'`), `preventKey` (optional key filter)
|
|
850
|
-
- **Output:** `action` - Emits the prevented event
|
|
851
|
-
|
|
852
122
|
```html
|
|
853
|
-
|
|
854
|
-
<
|
|
123
|
+
<!-- Removes element from DOM when permission fails -->
|
|
124
|
+
<button *hasPermission="'user.create'">Create User</button>
|
|
855
125
|
```
|
|
856
126
|
|
|
857
127
|
---
|
|
858
128
|
|
|
859
|
-
##
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
{
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
{ type: 'action', actionId: 'admin.manage' },
|
|
881
|
-
],
|
|
882
|
-
})] },
|
|
883
|
-
|
|
884
|
-
// Custom redirect on deny
|
|
885
|
-
{ path: 'settings', canActivate: [permissionGuard('settings.view', '/access-denied')] },
|
|
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
|
+
},
|
|
886
150
|
];
|
|
887
151
|
```
|
|
888
152
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
OR logic - allows access if user has ANY of the specified permissions.
|
|
892
|
-
|
|
893
|
-
```typescript
|
|
894
|
-
{ path: 'reports', canActivate: [anyPermissionGuard(['report.view', 'report.export'])] }
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
### allPermissionsGuard
|
|
898
|
-
|
|
899
|
-
AND logic - allows access only if user has ALL specified permissions.
|
|
900
|
-
|
|
901
|
-
```typescript
|
|
902
|
-
{ path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
|
|
903
|
-
```
|
|
153
|
+
All three guards redirect to `/` by default. Pass a second argument to change it: `permissionGuard('user.view', '/access-denied')`.
|
|
904
154
|
|
|
905
155
|
---
|
|
906
156
|
|
|
907
|
-
##
|
|
908
|
-
|
|
909
|
-
### Permission Evaluator
|
|
910
|
-
|
|
911
|
-
Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards.
|
|
912
|
-
|
|
913
|
-
```typescript
|
|
914
|
-
import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions } from '@flusys/ng-shared';
|
|
915
|
-
|
|
916
|
-
const userPermissions = ['user.view', 'user.create'];
|
|
917
|
-
|
|
918
|
-
// Evaluate string or ILogicNode
|
|
919
|
-
evaluatePermission('user.view', userPermissions); // true
|
|
920
|
-
evaluatePermission(null, userPermissions); // false
|
|
921
|
-
|
|
922
|
-
// Evaluate ILogicNode tree recursively
|
|
923
|
-
evaluateLogicNode(logicNode, userPermissions);
|
|
924
|
-
|
|
925
|
-
// Simple OR/AND checks
|
|
926
|
-
hasAnyPermission(['user.view', 'user.delete'], userPermissions); // true (has user.view)
|
|
927
|
-
hasAllPermissions(['user.view', 'user.delete'], userPermissions); // false (missing user.delete)
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
---
|
|
931
|
-
|
|
932
|
-
## 8. Classes
|
|
933
|
-
|
|
934
|
-
### BaseFormControl
|
|
935
|
-
|
|
936
|
-
Abstract base class for custom form controls. Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
|
|
937
|
-
|
|
938
|
-
```typescript
|
|
939
|
-
import { BaseFormControl, provideValueAccessor } from '@flusys/ng-shared';
|
|
940
|
-
|
|
941
|
-
@Component({
|
|
942
|
-
selector: 'my-select',
|
|
943
|
-
providers: [provideValueAccessor(MySelectComponent)],
|
|
944
|
-
})
|
|
945
|
-
export class MySelectComponent extends BaseFormControl<string | null> {
|
|
946
|
-
override readonly value = model<string | null>(null);
|
|
947
|
-
|
|
948
|
-
constructor() {
|
|
949
|
-
super();
|
|
950
|
-
this.initializeFormControl(); // Required for ControlValueAccessor sync
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Usage in all form patterns:
|
|
955
|
-
// Template-driven: <my-select [(value)]="selectedId" />
|
|
956
|
-
// Reactive forms: <my-select [formControl]="myControl" />
|
|
957
|
-
// Signal forms: <my-select [formField]="formTree.myField" />
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
**Abstract property:** `value: ModelSignal<T>` - Must override with `model<T>()`
|
|
961
|
-
|
|
962
|
-
**Models:** `disabled` (boolean), `touched` (boolean)
|
|
963
|
-
|
|
964
|
-
**Methods:** `initializeFormControl()` (call in constructor), `markAsTouched()` (call on blur)
|
|
965
|
-
|
|
966
|
-
**Helper:** `provideValueAccessor(ComponentClass)` - Factory for `NG_VALUE_ACCESSOR` provider
|
|
967
|
-
|
|
968
|
-
---
|
|
969
|
-
|
|
970
|
-
## 9. Modules
|
|
971
|
-
|
|
972
|
-
### AngularModule
|
|
973
|
-
|
|
974
|
-
Re-exports common Angular modules for convenience.
|
|
975
|
-
|
|
976
|
-
```typescript
|
|
977
|
-
import { AngularModule } from '@flusys/ng-shared';
|
|
978
|
-
// Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet, + directives
|
|
979
|
-
// Providers: DatePipe
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
### PrimeModule
|
|
983
|
-
|
|
984
|
-
Re-exports PrimeNG component modules for convenience.
|
|
985
|
-
|
|
986
|
-
```typescript
|
|
987
|
-
import { PrimeModule } from '@flusys/ng-shared';
|
|
988
|
-
// Includes: Button, Table, Card, Dialog, InputText, Select, MultiSelect,
|
|
989
|
-
// DatePicker, Checkbox, FileUpload, Image, Tag, Tabs, TreeTable, and more (27 modules)
|
|
990
|
-
```
|
|
991
|
-
|
|
992
|
-
---
|
|
993
|
-
|
|
994
|
-
## 10. Provider Interfaces (Package Independence)
|
|
995
|
-
|
|
996
|
-
ng-shared defines **provider interfaces** to enable feature packages (ng-iam, ng-storage) to access auth functionality without direct dependencies.
|
|
997
|
-
|
|
998
|
-
### Architecture
|
|
999
|
-
|
|
1000
|
-
```
|
|
1001
|
-
ng-shared (defines interfaces + tokens)
|
|
1002
|
-
|
|
|
1003
|
-
ng-auth (implements interfaces with adapters)
|
|
1004
|
-
|
|
|
1005
|
-
ng-iam/ng-storage (consume interfaces via DI)
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
### Available Providers
|
|
1009
|
-
|
|
1010
|
-
#### IUserProvider / `USER_PROVIDER`
|
|
1011
|
-
|
|
1012
|
-
User list access for IAM user selection.
|
|
1013
|
-
|
|
1014
|
-
```typescript
|
|
1015
|
-
interface IUserBasicInfo { id: string; name: string; email: string }
|
|
1016
|
-
|
|
1017
|
-
interface IUserProvider {
|
|
1018
|
-
getUsers(filter?: {
|
|
1019
|
-
page?: number; pageSize?: number; search?: string;
|
|
1020
|
-
companyId?: string; branchId?: string;
|
|
1021
|
-
}): Observable<IListResponse<IUserBasicInfo>>;
|
|
1022
|
-
}
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
#### ICompanyApiProvider / `COMPANY_API_PROVIDER`
|
|
1026
|
-
|
|
1027
|
-
Company list access for IAM company selection.
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
interface ICompanyBasicInfo { id: string; name: string; slug?: string }
|
|
1031
|
-
|
|
1032
|
-
interface ICompanyApiProvider {
|
|
1033
|
-
getCompanies(filter?: {
|
|
1034
|
-
page?: number; pageSize?: number; search?: string;
|
|
1035
|
-
}): Observable<IListResponse<ICompanyBasicInfo>>;
|
|
1036
|
-
}
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
#### IUserPermissionProvider / `USER_PERMISSION_PROVIDER`
|
|
1040
|
-
|
|
1041
|
-
User permission queries for IAM.
|
|
1042
|
-
|
|
1043
|
-
```typescript
|
|
1044
|
-
interface IUserPermissionProvider {
|
|
1045
|
-
getUserBranchPermissions(userId: string): Observable<ISingleResponse<any>>;
|
|
1046
|
-
}
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
#### IProfileUploadProvider / `PROFILE_UPLOAD_PROVIDER`
|
|
1050
|
-
|
|
1051
|
-
Profile picture upload for ng-auth profile page. Implemented by ng-storage.
|
|
1052
|
-
|
|
1053
|
-
```typescript
|
|
1054
|
-
interface IProfileUploadResult {
|
|
1055
|
-
id: string; // File manager ID (UUID)
|
|
1056
|
-
name: string; // Original file name
|
|
1057
|
-
key: string; // Storage key/path
|
|
1058
|
-
size: number; // File size in bytes
|
|
1059
|
-
contentType: string; // MIME type
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
interface IProfileUploadOptions {
|
|
1063
|
-
folderPath?: string;
|
|
1064
|
-
compress?: boolean;
|
|
1065
|
-
maxWidth?: number;
|
|
1066
|
-
maxHeight?: number;
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
interface IProfileUploadProvider {
|
|
1070
|
-
uploadProfilePicture(
|
|
1071
|
-
file: File,
|
|
1072
|
-
options?: IProfileUploadOptions
|
|
1073
|
-
): Observable<ISingleResponse<IProfileUploadResult>>;
|
|
1074
|
-
}
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
#### IProfilePermissionProvider / `PROFILE_PERMISSION_PROVIDER`
|
|
1078
|
-
|
|
1079
|
-
User permission queries for ng-auth profile page. Implemented by ng-iam.
|
|
1080
|
-
|
|
1081
|
-
```typescript
|
|
1082
|
-
interface IProfileRoleInfo {
|
|
1083
|
-
id: string;
|
|
1084
|
-
name: string;
|
|
1085
|
-
description?: string | null;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
interface IProfileActionInfo {
|
|
1089
|
-
id: string;
|
|
1090
|
-
code: string;
|
|
1091
|
-
name: string;
|
|
1092
|
-
description?: string | null;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
interface IProfilePermissionProvider {
|
|
1096
|
-
getUserRoles(userId: string, branchId?: string): Observable<ISingleResponse<IProfileRoleInfo[]>>;
|
|
1097
|
-
getUserActions(userId: string, branchId?: string): Observable<ISingleResponse<IProfileActionInfo[]>>;
|
|
1098
|
-
}
|
|
1099
|
-
```
|
|
1100
|
-
|
|
1101
|
-
### Usage in Consuming Packages
|
|
1102
|
-
|
|
1103
|
-
```typescript
|
|
1104
|
-
import { USER_PROVIDER } from '@flusys/ng-shared';
|
|
1105
|
-
|
|
1106
|
-
export class UserSelectorComponent {
|
|
1107
|
-
private readonly userProvider = inject(USER_PROVIDER);
|
|
1108
|
-
|
|
1109
|
-
loadUsers() {
|
|
1110
|
-
this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe(response => {
|
|
1111
|
-
this.users.set(response.data ?? []);
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
### Wiring in App
|
|
1118
|
-
|
|
1119
|
-
```typescript
|
|
1120
|
-
// app.config.ts
|
|
1121
|
-
import { provideAuthProviders } from '@flusys/ng-auth';
|
|
1122
|
-
|
|
1123
|
-
export const appConfig: ApplicationConfig = {
|
|
1124
|
-
providers: [
|
|
1125
|
-
...provideAuthProviders(), // Registers all auth adapters for provider tokens
|
|
1126
|
-
],
|
|
1127
|
-
};
|
|
1128
|
-
```
|
|
1129
|
-
|
|
1130
|
-
### See Also
|
|
1131
|
-
|
|
1132
|
-
- [AUTH-GUIDE.md](AUTH-GUIDE.md) - Adapter implementations
|
|
1133
|
-
- [IAM-GUIDE.md](IAM-GUIDE.md) - IAM usage examples
|
|
1134
|
-
- [../CLAUDE.md](../CLAUDE.md) - Complete pattern documentation
|
|
1135
|
-
|
|
1136
|
-
---
|
|
1137
|
-
|
|
1138
|
-
## Best Practices
|
|
1139
|
-
|
|
1140
|
-
### API Services
|
|
1141
|
-
|
|
1142
|
-
- **Extend `ApiResourceService`** for new services (signal-based)
|
|
1143
|
-
- Use reactive signals (`data`, `isLoading`, `total`) in templates
|
|
1144
|
-
- Use `fetchList()` to trigger queries, `reload()` to refresh
|
|
1145
|
-
- Use async methods (`insertAsync`, `updateAsync`) for one-off operations
|
|
1146
|
-
|
|
1147
|
-
### File URLs
|
|
1148
|
-
|
|
1149
|
-
- **Always use `FileUrlService`** - never construct URLs manually
|
|
1150
|
-
- Use `fetchSingleFileUrl()` for one-off fetches, `fetchFileUrls()` for batches
|
|
1151
|
-
- Use `fileUrlSignal()` for reactive template bindings
|
|
1152
|
-
|
|
1153
|
-
### Components
|
|
1154
|
-
|
|
1155
|
-
- Use `AngularModule` and `PrimeModule` for common imports
|
|
1156
|
-
- Use signal inputs via `input()` and `input.required()`
|
|
1157
|
-
- Components use `lib-` prefix for selectors
|
|
1158
|
-
|
|
1159
|
-
### Directives
|
|
1160
|
-
|
|
1161
|
-
- Directives use `app` prefix for selectors (`appPreventDefault`, `appEditModeElementChanger`)
|
|
1162
|
-
- Exception: `HasPermissionDirective` uses `[hasPermission]`
|
|
1163
|
-
- Exception: `IsEmptyImageDirective` uses `img` selector (auto-applies to all images)
|
|
1164
|
-
|
|
1165
|
-
### Permissions
|
|
1166
|
-
|
|
1167
|
-
- Use `HasPermissionDirective` for template-level permission checks
|
|
1168
|
-
- Use permission guards for route-level access control
|
|
1169
|
-
- Use `PermissionValidatorService` for programmatic checks in services
|
|
1170
|
-
- Permissions follow fail-closed model: no access by default
|
|
1171
|
-
|
|
1172
|
-
---
|
|
1173
|
-
|
|
1174
|
-
## Common Issues
|
|
1175
|
-
|
|
1176
|
-
### ApiResourceService Not Updating UI
|
|
1177
|
-
|
|
1178
|
-
**Problem:** UI doesn't update after operations.
|
|
1179
|
-
|
|
1180
|
-
**Solution:** Use signal syntax in templates:
|
|
157
|
+
## 5. TranslatePipe
|
|
1181
158
|
|
|
1182
159
|
```html
|
|
1183
|
-
|
|
1184
|
-
<div>{{ users() }}</div>
|
|
1185
|
-
|
|
1186
|
-
<!-- Wrong -->
|
|
1187
|
-
<div>{{ users }}</div>
|
|
160
|
+
{{ 'shared.save' | translate }} {{ 'pagination.showing' | translate: { from: 1, to: 10 } }}
|
|
1188
161
|
```
|
|
1189
162
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
**Problem:** File URL fetching fails.
|
|
1193
|
-
|
|
1194
|
-
**Solution:**
|
|
1195
|
-
|
|
1196
|
-
1. Ensure storage service is enabled in environment config
|
|
1197
|
-
2. Verify file ID exists in database
|
|
1198
|
-
3. Check storage provider configuration (local/S3/Azure)
|
|
1199
|
-
|
|
1200
|
-
### Circular Dependency with ng-layout
|
|
1201
|
-
|
|
1202
|
-
**Problem:** Build fails with circular dependency.
|
|
1203
|
-
|
|
1204
|
-
**Solution:** ng-shared must NEVER import from ng-layout. Move shared components to ng-shared, layout-specific components to ng-layout.
|
|
1205
|
-
|
|
1206
|
-
---
|
|
1207
|
-
|
|
1208
|
-
## API Reference
|
|
1209
|
-
|
|
1210
|
-
### Services
|
|
1211
|
-
|
|
1212
|
-
| Service | Description |
|
|
1213
|
-
| ------------------------------ | ------------------------------------- |
|
|
1214
|
-
| `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API |
|
|
1215
|
-
| `FileUrlService` | Cloud storage URL fetching |
|
|
1216
|
-
| `PermissionValidatorService` | Permission state management |
|
|
1217
|
-
| `CookieService` | SSR-aware cookie reading |
|
|
1218
|
-
| `PlatformService` | SSR environment detection |
|
|
1219
|
-
|
|
1220
|
-
### Components
|
|
1221
|
-
|
|
1222
|
-
| Component | Selector | Description |
|
|
1223
|
-
| -------------------------- | ---------------------- | ------------------------- |
|
|
1224
|
-
| `IconComponent` | `lib-icon` | Flexible icon renderer |
|
|
1225
|
-
| `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
|
|
1226
|
-
| `LazyMultiSelectComponent` | `lib-lazy-multi-select`| Lazy-loading multi-select |
|
|
1227
|
-
| `UserSelectComponent` | `lib-user-select` | Single user selector |
|
|
1228
|
-
| `UserMultiSelectComponent` | `lib-user-multi-select`| Multiple user selector |
|
|
1229
|
-
| `FileUploaderComponent` | `lib-file-uploader` | Drag & drop file upload |
|
|
1230
|
-
| `FileSelectorDialogComponent` | `lib-file-selector-dialog` | File browser dialog |
|
|
1231
|
-
|
|
1232
|
-
### Directives
|
|
1233
|
-
|
|
1234
|
-
| Directive | Selector | Description |
|
|
1235
|
-
| --------------------------------- | --------------------------- | ----------------------------------- |
|
|
1236
|
-
| `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
|
|
1237
|
-
| `EditModeElementChangerDirective` | `[appEditModeElementChanger]` | Toggle edit mode on form controls |
|
|
1238
|
-
| `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
|
|
1239
|
-
| `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
|
|
1240
|
-
|
|
1241
|
-
### Guards
|
|
1242
|
-
|
|
1243
|
-
| Guard | Description |
|
|
1244
|
-
| ---------------------- | -------------------------------------- |
|
|
1245
|
-
| `permissionGuard` | Single permission or ILogicNode check |
|
|
1246
|
-
| `anyPermissionGuard` | OR logic (any of listed permissions) |
|
|
1247
|
-
| `allPermissionsGuard` | AND logic (all of listed permissions) |
|
|
1248
|
-
|
|
1249
|
-
### Interfaces
|
|
1250
|
-
|
|
1251
|
-
| Interface | Description |
|
|
1252
|
-
| -------------------- | ------------------------------------ |
|
|
1253
|
-
| `IBaseEntity` | Base entity with ID and timestamps |
|
|
1254
|
-
| `ILoggedUserInfo` | Current user info with company ctx |
|
|
1255
|
-
| `IFilterData` | Filter, pagination, sort payload |
|
|
1256
|
-
| `IDeleteData` | Delete request payload |
|
|
1257
|
-
| `IDropDown` | Simple label/value pair |
|
|
1258
|
-
| `ISingleResponse<T>` | Single item response |
|
|
1259
|
-
| `IListResponse<T>` | List with pagination |
|
|
1260
|
-
| `IBulkResponse<T>` | Bulk operation response |
|
|
1261
|
-
| `IMessageResponse` | Message-only response |
|
|
1262
|
-
| `IErrorResponse` | Error with validation details |
|
|
1263
|
-
| `ILogicNode` | Permission logic tree (AND/OR nodes) |
|
|
1264
|
-
| `IUserSelectFilter` | User select filter params |
|
|
1265
|
-
| `LoadUsersFn` | User loading function type |
|
|
1266
|
-
| `IFileBasicInfo` | Basic file info for selectors |
|
|
1267
|
-
| `IFileUploadOptions`| Upload options (compression, etc.) |
|
|
1268
|
-
| `IUploadedFile` | Uploaded file response |
|
|
1269
|
-
| `IFileSelectFilter` | File select filter params |
|
|
1270
|
-
| `LoadFilesFn` | File loading function type |
|
|
1271
|
-
| `UploadFileFn` | File upload function type |
|
|
1272
|
-
|
|
1273
|
-
### Injection Tokens
|
|
1274
|
-
|
|
1275
|
-
| Token | Interface | Description |
|
|
1276
|
-
| -------------------------- | ------------------------- | ---------------------------- |
|
|
1277
|
-
| `USER_PROVIDER` | `IUserProvider` | User list for IAM |
|
|
1278
|
-
| `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | Company list for IAM |
|
|
1279
|
-
| `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | User permission queries |
|
|
1280
|
-
| `PROFILE_UPLOAD_PROVIDER` | `IProfileUploadProvider` | Profile picture upload (ng-storage) |
|
|
1281
|
-
| `PROFILE_PERMISSION_PROVIDER` | `IProfilePermissionProvider` | User permissions for profile (ng-iam) |
|
|
1282
|
-
|
|
1283
|
-
## See Also
|
|
1284
|
-
|
|
1285
|
-
- **[CORE-GUIDE.md](./CORE-GUIDE.md)** - Configuration, interceptors
|
|
1286
|
-
- **[LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md)** - Layout system
|
|
1287
|
-
- **[AUTH-GUIDE.md](./AUTH-GUIDE.md)** - Auth adapters for provider interfaces
|
|
1288
|
-
- **[IAM-GUIDE.md](./IAM-GUIDE.md)** - IAM permission usage
|
|
1289
|
-
|
|
1290
|
-
---
|
|
163
|
+
## License
|
|
1291
164
|
|
|
1292
|
-
|
|
1293
|
-
**Angular Version:** 21
|
|
165
|
+
MIT © FLUSYS
|