@flusys/ng-shared 4.1.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,75 +1,9 @@
1
1
  # @flusys/ng-shared
2
2
 
3
- > Shared utilities, response interfaces, provider interfaces, base classes, and reusable UI components for the FLUSYS Angular platform.
3
+ Shared utilities, provider tokens, base page classes, and reusable components for the FLUSYS Angular platform.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@flusys/ng-shared.svg)](https://www.npmjs.com/package/@flusys/ng-shared)
6
- [![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
9
-
10
- ---
11
-
12
- ## Table of Contents
13
-
14
- - [Overview](#overview)
15
- - [Features](#features)
16
- - [Compatibility](#compatibility)
17
- - [Installation](#installation)
18
- - [Quick Start](#quick-start)
19
- - [Response Interfaces](#response-interfaces)
20
- - [Provider Interface Pattern](#provider-interface-pattern)
21
- - [Injection Tokens](#injection-tokens)
22
- - [USER_PROVIDER](#user_provider)
23
- - [COMPANY_PROVIDER](#company_provider)
24
- - [FILE_PROVIDER](#file_provider)
25
- - [Base Classes](#base-classes)
26
- - [ApiResourceService](#apiresourceservice)
27
- - [BaseListPage](#baselistpage)
28
- - [BaseFormPage](#baseformpage)
29
- - [Services](#services)
30
- - [FileUrlService](#fileurlservice)
31
- - [PermissionValidatorService](#permissionvalidatorservice)
32
- - [Reusable Components](#reusable-components)
33
- - [Directives](#directives)
34
- - [Pipes](#pipes)
35
- - [Guards](#guards)
36
- - [Modules](#modules)
37
- - [Troubleshooting](#troubleshooting)
38
- - [License](#license)
39
-
40
- ---
41
-
42
- ## Overview
43
-
44
- `@flusys/ng-shared` is the second layer in the FLUSYS Angular dependency hierarchy. It depends on `@flusys/ng-core` and provides everything that feature packages (`ng-auth`, `ng-iam`, `ng-storage`, etc.) need to function **independently** of each other.
45
-
46
- The cornerstone of `ng-shared` is the **Provider Interface Pattern**: injection tokens that define contracts between feature packages. `ng-auth` provides implementations; `ng-iam`, `ng-storage`, and other packages inject the interfaces — never the concrete implementations.
47
-
48
- ---
49
-
50
- ## Features
51
-
52
- - ✅ Typed response interfaces (`ISingleResponse`, `IListResponse`, `IBulkResponse`, `IMessageResponse`)
53
- - ✅ Provider Interface Pattern — `USER_PROVIDER`, `COMPANY_PROVIDER`, `FILE_PROVIDER`
54
- - ✅ `ApiResourceService` — typed CRUD service base class
55
- - ✅ `BaseListPage` / `BaseFormPage` — page scaffolding with signal state
56
- - ✅ `FileUrlService` — safe presigned URL fetching (never construct URLs manually)
57
- - ✅ `PermissionValidatorService` — client-side permission evaluation
58
- - ✅ `LazySelectComponent` — virtualized dropdown with server-side search
59
- - ✅ `FileSelectorDialogComponent` — file picker dialog
60
- - ✅ `HasPermissionDirective` — structural directive for permission-gated content
61
- - ✅ `TranslatePipe` — i18n pipe with parameter interpolation
62
- - ✅ `AngularModule` / `PrimeModule` — pre-aggregated Angular and PrimeNG imports
63
-
64
- ---
65
-
66
- ## Compatibility
67
-
68
- | Package | Version |
69
- |---------|---------|
70
- | Angular | 21+ |
71
- | @flusys/ng-core | 4.x |
72
- | PrimeNG | 18+ |
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
73
7
 
74
8
  ---
75
9
 
@@ -81,449 +15,146 @@ npm install @flusys/ng-shared @flusys/ng-core
81
15
 
82
16
  ---
83
17
 
84
- ## Quick Start
85
-
86
- ```typescript
87
- // app.config.ts
88
- import { provideHttpClient, withInterceptors } from '@angular/common/http';
89
- import { APP_CONFIG } from '@flusys/ng-core';
90
- import { environment } from './environments/environment';
91
-
92
- export const appConfig: ApplicationConfig = {
93
- providers: [
94
- { provide: APP_CONFIG, useValue: environment },
95
- provideHttpClient(),
96
- // Feature providers register against ng-shared tokens (see below)
97
- ],
98
- };
99
- ```
100
-
101
- ---
102
-
103
- ## Response Interfaces
104
-
105
- All FLUSYS backend responses conform to one of four shapes:
106
-
107
- ### ISingleResponse\<T\>
108
-
109
- ```typescript
110
- interface ISingleResponse<T> {
111
- data: T;
112
- }
113
- ```
114
-
115
- ### IListResponse\<T\>
116
-
117
- ```typescript
118
- interface IListResponse<T> {
119
- data: T[];
120
- total: number;
121
- page: number;
122
- pageSize: number;
123
- }
124
- ```
125
-
126
- ### IBulkResponse\<T\>
127
-
128
- ```typescript
129
- interface IBulkResponse<T> {
130
- data: T[];
131
- successCount: number;
132
- failureCount: number;
133
- errors?: string[];
134
- }
135
- ```
136
-
137
- ### IMessageResponse
138
-
139
- ```typescript
140
- interface IMessageResponse {
141
- message: string;
142
- }
143
- ```
144
-
145
- ### IErrorResponse
146
-
147
- ```typescript
148
- interface IErrorResponse {
149
- statusCode: number;
150
- message: string;
151
- error?: string;
152
- }
153
- ```
154
-
155
- ### IListParams (Common Request Shape)
156
-
157
- ```typescript
158
- interface IListParams {
159
- page?: number;
160
- pageSize?: number;
161
- search?: string;
162
- sortBy?: string;
163
- sortOrder?: 'ASC' | 'DESC';
164
- filters?: Record<string, unknown>;
165
- }
166
- ```
167
-
168
- ---
169
-
170
- ## Provider Interface Pattern
171
-
172
- Feature packages never import each other directly. Instead, `ng-shared` defines **injection token interfaces** that decouple consumers from providers.
173
-
174
- ```
175
- ng-auth ──implements──► USER_PROVIDER token ◄──injects── ng-iam
176
- ──implements──► COMPANY_PROVIDER ◄──injects── ng-storage
177
- ng-storage ──implements──► FILE_PROVIDER ◄──injects── ng-shared components
178
- ```
179
-
180
- ### Injection Tokens
18
+ ## 1. ApiResourceService — Signal-Driven CRUD
181
19
 
182
- | Token | Interface | Provided By | Used By |
183
- |-------|-----------|-------------|---------|
184
- | `USER_PROVIDER` | `IUserProvider` | `ng-auth` | `ng-iam`, `ng-storage` |
185
- | `COMPANY_PROVIDER` | `ICompanyProvider` | `ng-auth` | `ng-storage`, `ng-notification` |
186
- | `FILE_PROVIDER` | `IFileProvider` | `ng-storage` | `ng-shared` components |
187
- | `LAYOUT_AUTH_STATE` | `ILayoutAuthState` | `ng-auth` | `ng-layout` |
188
- | `LAYOUT_AUTH_API` | `ILayoutAuthApi` | `ng-auth` | `ng-layout` |
189
- | `LAYOUT_NOTIFICATION_BELL` | `INotificationBellProvider` | `ng-notification` | `ng-layout` |
190
- | `LAYOUT_LANGUAGE_SELECTOR` | `ILanguageSelectorProvider` | `ng-localization` | `ng-layout` |
191
-
192
- ### USER_PROVIDER
20
+ Extend this class to get a reactive list resource backed by `resource()`, with pagination, search, and all standard POST-only RPC endpoints.
193
21
 
194
22
  ```typescript
195
- interface IUserProvider {
196
- user: Signal<ICurrentUser | null>;
197
- isAuthenticated: Signal<boolean>;
198
- }
23
+ import { Injectable, inject } from '@angular/core';
24
+ import { HttpClient } from '@angular/common/http';
25
+ import { ApiResourceService } from '@flusys/ng-shared';
199
26
 
200
- // Consuming (in ng-iam)
201
- import { USER_PROVIDER, IUserProvider } from '@flusys/ng-shared';
27
+ export interface IProduct { id: string; name: string; price: number; }
28
+ export interface IProductDto { name: string; price: number; }
202
29
 
203
30
  @Injectable({ providedIn: 'root' })
204
- export class IamService {
205
- private userProvider = inject<IUserProvider>(USER_PROVIDER);
206
-
207
- getCurrentUserId(): string | null {
208
- return this.userProvider.user()?.id ?? null;
31
+ export class ProductService extends ApiResourceService<IProductDto, IProduct> {
32
+ constructor() {
33
+ super('products', inject(HttpClient), 'administration'); // moduleApiName, http, serviceName (key in APP_CONFIG.services)
209
34
  }
210
35
  }
211
36
  ```
212
37
 
213
- ### COMPANY_PROVIDER
38
+ Using the reactive list in a component:
214
39
 
215
40
  ```typescript
216
- interface ICompanyProvider {
217
- company: Signal<ICompany | null>;
218
- branch: Signal<IBranch | null>;
219
- companyId: Signal<string | null>;
220
- }
221
- ```
222
-
223
- ### FILE_PROVIDER
224
-
225
- ```typescript
226
- interface IFileProvider {
227
- getFileUrl(fileId: string): Observable<string | null>;
228
- uploadFile(file: File, folder?: string): Observable<IUploadedFile>;
229
- }
230
- ```
231
-
232
- ---
233
-
234
- ## Base Classes
235
-
236
- ### ApiResourceService
237
-
238
- Generic typed CRUD service that maps to FLUSYS POST-only RPC endpoints. Extend this to create a fully-typed API service in seconds.
41
+ @Component({ ... })
42
+ export class ProductListComponent {
43
+ private readonly svc = inject(ProductService);
239
44
 
240
- ```typescript
241
- import { ApiResourceService } from '@flusys/ng-shared';
45
+ readonly items = this.svc.data; // Signal<IProduct[]>
46
+ readonly total = this.svc.total; // Signal<number>
47
+ readonly loading = this.svc.isLoading; // Signal<boolean>
242
48
 
243
- @Injectable({ providedIn: 'root' })
244
- export class ProductApiService extends ApiResourceService<Product, CreateProductDto, UpdateProductDto> {
245
- protected override resource = 'product';
246
- }
49
+ ngOnInit() { this.svc.fetchList(); }
247
50
 
248
- // Automatically provides:
249
- // - getAll(params): Observable<IListResponse<Product>>
250
- // - getById(id): Observable<ISingleResponse<Product>>
251
- // - insert(dto): Observable<ISingleResponse<Product>>
252
- // - update(dto): Observable<ISingleResponse<Product>>
253
- // - delete(id): Observable<IMessageResponse>
254
- // - bulkDelete(ids): Observable<IBulkResponse<Product>>
255
- ```
51
+ onSearch(q: string) { this.svc.fetchList(q); }
256
52
 
257
- ### BaseListPage
53
+ onPageChange(page: number, size: number) {
54
+ this.svc.setPagination({ currentPage: page, pageSize: size });
55
+ }
258
56
 
259
- Scaffold for list pages with built-in pagination, search, and loading state using signals.
57
+ async onCreate(dto: IProductDto) {
58
+ await this.svc.insert(dto);
59
+ this.svc.reload();
60
+ }
260
61
 
261
- ```typescript
262
- import { BaseListPage } from '@flusys/ng-shared';
263
-
264
- @Component({
265
- selector: 'app-product-list',
266
- templateUrl: './product-list.component.html',
267
- })
268
- export class ProductListComponent extends BaseListPage<Product> {
269
- protected apiService = inject(ProductApiService);
270
-
271
- // Inherited signals:
272
- // items Signal<Product[]>
273
- // total Signal<number>
274
- // page Signal<number>
275
- // pageSize Signal<number>
276
- // isLoading Signal<boolean>
277
- // searchQuery Signal<string>
62
+ async onDelete(id: string) {
63
+ await this.svc.delete({ id });
64
+ this.svc.reload();
65
+ }
278
66
  }
279
67
  ```
280
68
 
281
- ### BaseFormPage
282
-
283
- Scaffold for create/edit forms with validation state and submission handling.
284
-
285
- ```typescript
286
- import { BaseFormPage } from '@flusys/ng-shared';
287
-
288
- @Component({
289
- selector: 'app-product-form',
290
- templateUrl: './product-form.component.html',
291
- })
292
- export class ProductFormComponent extends BaseFormPage<Product> {
293
- protected apiService = inject(ProductApiService);
294
-
295
- // Inherited signals:
296
- // isSaving Signal<boolean>
297
- // isEditMode Signal<boolean>
298
- // formErrors Signal<Record<string, string>>
299
- }
300
- ```
69
+ Available CRUD methods: `insert`, `insertMany`, `findById`, `findByIds`, `getAll`, `getByFilter`, `update`, `updateMany`, `bulkUpsert`, `delete`.
301
70
 
302
71
  ---
303
72
 
304
- ## Services
305
-
306
- ### FileUrlService
73
+ ## 2. FileUrlService — Presigned URL Fetching
307
74
 
308
- **CRITICAL:** Always use `FileUrlService` to fetch file URLs. Never construct URLs manually. FLUSYS supports S3, Azure, and SFTP with presigned URLs that expire — only the backend knows the current URL.
75
+ Never construct file URLs manually. `FileUrlService` handles S3/Azure presigned URLs, caching, and deduplication.
309
76
 
310
77
  ```typescript
311
78
  import { FileUrlService } from '@flusys/ng-shared';
312
79
 
313
80
  @Component({ ... })
314
81
  export class AvatarComponent {
315
- private fileUrlService = inject(FileUrlService);
316
- avatarUrl = signal<string | null>(null);
82
+ private readonly fileUrlService = inject(FileUrlService);
83
+ readonly avatarUrl = signal<string | null>(null);
317
84
 
318
- async loadAvatar(fileId: string): Promise<void> {
319
- const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
320
- this.avatarUrl.set(file?.url ?? null);
85
+ ngOnInit() {
86
+ this.fileUrlService.fetchSingleFileUrl('file-id-123').subscribe(file => {
87
+ this.avatarUrl.set(file?.url ?? null);
88
+ });
321
89
  }
322
- }
323
- ```
324
-
325
- ```typescript
326
- // ❌ WRONG — never construct file URLs manually
327
- const url = `${apiBaseUrl}/storage/${fileId}`;
328
-
329
- // ✅ CORRECT — always use FileUrlService
330
- const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
331
- const url = file?.url ?? null;
332
- ```
333
-
334
- **Methods:**
335
-
336
- | Method | Description |
337
- |--------|-------------|
338
- | `fetchSingleFileUrl(fileId)` | Fetch presigned URL for a single file |
339
- | `fetchMultipleFileUrls(fileIds)` | Batch fetch presigned URLs |
340
- | `clearCache(fileId?)` | Invalidate cached URL(s) |
341
-
342
- ### PermissionValidatorService
343
-
344
- Client-side permission evaluation without direct ng-iam dependency.
345
-
346
- ```typescript
347
- import { PermissionValidatorService } from '@flusys/ng-shared';
348
90
 
349
- @Component({ ... })
350
- export class MyComponent {
351
- private permValidator = inject(PermissionValidatorService);
91
+ // Batch fetch — only requests IDs not already cached
92
+ loadMultiple(ids: string[]) {
93
+ this.fileUrlService.fetchFileUrls(ids).subscribe(files => { /* ... */ });
94
+ }
352
95
 
353
- canEdit = computed(() =>
354
- this.permValidator.hasPermission('product:update')
355
- );
96
+ // Synchronous read from cache after a prior fetch
97
+ getCachedUrl(id: string): string | null {
98
+ return this.fileUrlService.getFileUrl(id);
99
+ }
356
100
  }
357
101
  ```
358
102
 
359
103
  ---
360
104
 
361
- ## Reusable Components
362
-
363
- ### LazySelectComponent
364
-
365
- Virtualized dropdown with server-side search. Ideal for large datasets.
366
-
367
- ```html
368
- <flusys-lazy-select
369
- [apiService]="productApiService"
370
- [labelField]="'name'"
371
- [valueField]="'id'"
372
- [(ngModel)]="selectedProductId"
373
- placeholder="Search products..."
374
- />
375
- ```
376
-
377
- ### FileSelectorDialogComponent
378
-
379
- File picker dialog that integrates with `@flusys/ng-storage`.
380
-
381
- ```html
382
- <flusys-file-selector
383
- [accept]="'image/*'"
384
- [maxSize]="5242880"
385
- (fileSelected)="onFileSelected($event)"
386
- />
387
- ```
388
-
389
- ---
390
-
391
- ## Directives
392
-
393
- ### HasPermissionDirective
394
-
395
- Structural directive that removes elements from the DOM if the user lacks the required permission:
396
-
397
- ```html
398
- <!-- Single permission -->
399
- <button *hasPermission="'product:delete'">Delete</button>
400
-
401
- <!-- Multiple permissions (AND logic) -->
402
- <div *hasPermission="['product:update', 'product:read']">Edit Panel</div>
105
+ ## 3. PermissionValidatorService + HasPermissionDirective
403
106
 
404
- <!-- OR logic -->
405
- <div *hasPermission="'product:update'" [permissionOr]="true">Edit Panel</div>
107
+ ```typescript
108
+ const pv = inject(PermissionValidatorService);
406
109
 
407
- <!-- Show fallback content -->
408
- <button *hasPermission="'admin:manage'; else noAccess">Admin</button>
409
- <ng-template #noAccess>
410
- <span>No access</span>
411
- </ng-template>
110
+ pv.hasPermission('user.create'); // true
111
+ pv.hasPermission('report.export'); // true — wildcard match
112
+ pv.isPermissionsLoaded(); // boolean
412
113
  ```
413
114
 
414
- ---
415
-
416
- ## Pipes
417
-
418
- ### TranslatePipe
419
-
420
- Translate i18n keys with optional parameter interpolation:
421
-
422
115
  ```html
423
- <!-- Basic translation -->
424
- <span>{{ 'common.save' | translate }}</span>
425
-
426
- <!-- With parameters -->
427
- <span>{{ 'pagination.showing' | translate: { from: 1, to: 10, total: 100 } }}</span>
116
+ <!-- Removes element from DOM when permission fails -->
117
+ <button *hasPermission="'user.create'">Create User</button>
428
118
  ```
429
119
 
430
- The pipe automatically subscribes to language change events and re-renders on switch.
431
-
432
120
  ---
433
121
 
434
- ## Guards
435
-
436
- | Guard | Description |
437
- |-------|-------------|
438
- | `AuthenticatedGuard` | Redirects unauthenticated users to `/auth/login` |
439
- | `GuestGuard` | Redirects authenticated users away from auth pages |
440
- | `PermissionGuard` | Blocks route if user lacks required permission |
122
+ ## 4. Permission Guards
441
123
 
442
124
  ```typescript
443
- // app.routes.ts
125
+ import { permissionGuard, anyPermissionGuard, allPermissionsGuard } from '@flusys/ng-shared';
126
+
444
127
  export const routes: Routes = [
445
128
  {
446
- path: 'products',
447
- canActivate: [AuthenticatedGuard],
448
- loadComponent: () => import('./pages/product-list.component'),
129
+ path: 'users',
130
+ canActivate: [permissionGuard('user.view')], // single
131
+ loadComponent: () => import('./user-list.component'),
132
+ },
133
+ {
134
+ path: 'reports',
135
+ canActivate: [anyPermissionGuard(['report.view', 'report.export'])], // OR
136
+ loadComponent: () => import('./reports.component'),
449
137
  },
450
138
  {
451
139
  path: 'admin',
452
- canActivate: [PermissionGuard],
453
- data: { permission: 'admin:manage' },
454
- loadComponent: () => import('./pages/admin.component'),
140
+ canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])], // AND
141
+ loadComponent: () => import('./admin.component'),
455
142
  },
456
143
  ];
457
144
  ```
458
145
 
459
- ---
460
-
461
- ## Modules
462
-
463
- ### AngularModule
464
-
465
- Pre-aggregated Angular common imports for use in components:
466
-
467
- ```typescript
468
- import { AngularModule } from '@flusys/ng-shared';
469
-
470
- @Component({
471
- imports: [AngularModule],
472
- // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterModule, etc.
473
- })
474
- export class MyComponent {}
475
- ```
476
-
477
- ### PrimeModule
478
-
479
- Pre-aggregated PrimeNG component imports:
480
-
481
- ```typescript
482
- import { PrimeModule } from '@flusys/ng-shared';
483
-
484
- @Component({
485
- imports: [PrimeModule],
486
- // Includes: ButtonModule, TableModule, DialogModule, InputTextModule, etc.
487
- })
488
- export class MyComponent {}
489
- ```
146
+ All three guards redirect to `/` by default. Pass a second argument to change it: `permissionGuard('user.view', '/access-denied')`.
490
147
 
491
148
  ---
492
149
 
493
- ## Troubleshooting
494
-
495
- **`No provider for USER_PROVIDER`**
496
-
497
- You need to register the auth providers in `app.config.ts`:
498
-
499
- ```typescript
500
- import { provideAuthProviders } from '@flusys/ng-auth';
501
-
502
- providers: [
503
- ...provideAuthProviders(),
504
- ]
505
- ```
506
-
507
- **`FileUrlService returns null for valid fileId`**
150
+ ## 5. TranslatePipe
508
151
 
509
- The file may not exist or the storage module isn't registered:
152
+ ```html
153
+ {{ 'shared.save' | translate }}
510
154
 
511
- ```typescript
512
- // Ensure storage is enabled in APP_CONFIG
513
- services: {
514
- storage: { enabled: true }
515
- }
155
+ {{ 'pagination.showing' | translate: { from: 1, to: 10 } }}
516
156
  ```
517
157
 
518
- **`TranslatePipe shows raw key (e.g., "common.save")`**
519
-
520
- Localization isn't initialized or the key doesn't exist in translation files. Provide `TRANSLATE_ADAPTER` in `app.config.ts` via `provideLocalization()`.
521
-
522
- **`HasPermissionDirective always hides content`**
523
-
524
- `PermissionValidatorService` has no data — ensure `ng-iam` is enabled and permissions are loaded after login.
525
-
526
- ---
527
158
 
528
159
  ## License
529
160