@flusys/ng-shared 1.0.0-beta → 1.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.
@@ -1,22 +1,24 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, HostListener, NgModule, Injector, runInInjectionContext, resource, Component, model, untracked, forwardRef, viewChild, ChangeDetectionStrategy, InjectionToken, DestroyRef, afterNextRender, isDevMode } from '@angular/core';
3
- import * as i1 from '@angular/common';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, NgModule, Injector, runInInjectionContext, resource, model, untracked, forwardRef, ChangeDetectionStrategy, Component, DestroyRef, viewChild, afterNextRender, InjectionToken, isDevMode } from '@angular/core';
3
+ import * as i3 from '@angular/common';
4
4
  import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe } from '@angular/common';
5
5
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
6
- import { APP_CONFIG, getServiceUrl, ApiLoaderService } from '@flusys/ng-core';
7
- import { of, firstValueFrom, skip, debounceTime, distinctUntilChanged, tap as tap$1, map as map$1 } from 'rxjs';
8
- import { tap, catchError, map } from 'rxjs/operators';
9
- import * as i1$2 from '@angular/forms';
6
+ import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
7
+ import { of, firstValueFrom, map as map$1 } from 'rxjs';
8
+ import { map, tap, catchError } from 'rxjs/operators';
9
+ import * as i1$1 from '@angular/forms';
10
10
  import { NgControl, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
11
- import { RouterOutlet, RouterLink, Router } from '@angular/router';
11
+ import { RouterOutlet, RouterLink, RouterLinkActive, Router, ActivatedRoute } from '@angular/router';
12
12
  import { AutoCompleteModule } from 'primeng/autocomplete';
13
- import * as i1$3 from 'primeng/button';
13
+ import { AvatarModule } from 'primeng/avatar';
14
+ import * as i1$2 from 'primeng/button';
14
15
  import { ButtonModule } from 'primeng/button';
15
16
  import { CardModule } from 'primeng/card';
16
- import * as i2 from 'primeng/checkbox';
17
+ import * as i1 from 'primeng/checkbox';
17
18
  import { CheckboxModule } from 'primeng/checkbox';
19
+ import { ConfirmDialogModule } from 'primeng/confirmdialog';
18
20
  import { DatePickerModule } from 'primeng/datepicker';
19
- import * as i6 from 'primeng/dialog';
21
+ import * as i4 from 'primeng/dialog';
20
22
  import { DialogModule } from 'primeng/dialog';
21
23
  import { DividerModule } from 'primeng/divider';
22
24
  import { FileUploadModule } from 'primeng/fileupload';
@@ -24,7 +26,7 @@ import { IconFieldModule } from 'primeng/iconfield';
24
26
  import { ImageModule } from 'primeng/image';
25
27
  import { InputIconModule } from 'primeng/inputicon';
26
28
  import { InputNumberModule } from 'primeng/inputnumber';
27
- import * as i1$1 from 'primeng/inputtext';
29
+ import * as i2 from 'primeng/inputtext';
28
30
  import { InputTextModule } from 'primeng/inputtext';
29
31
  import { ListboxModule } from 'primeng/listbox';
30
32
  import { MultiSelectModule } from 'primeng/multiselect';
@@ -36,24 +38,146 @@ import * as i2$1 from 'primeng/progressbar';
36
38
  import { ProgressBarModule } from 'primeng/progressbar';
37
39
  import { RadioButtonModule } from 'primeng/radiobutton';
38
40
  import { RippleModule } from 'primeng/ripple';
39
- import * as i3 from 'primeng/select';
41
+ import * as i3$1 from 'primeng/select';
40
42
  import { SelectModule } from 'primeng/select';
41
43
  import { SelectButtonModule } from 'primeng/selectbutton';
44
+ import { SkeletonModule } from 'primeng/skeleton';
42
45
  import { SplitButtonModule } from 'primeng/splitbutton';
43
46
  import { StepsModule } from 'primeng/steps';
44
47
  import { TableModule } from 'primeng/table';
45
48
  import { TabsModule } from 'primeng/tabs';
46
49
  import { TagModule } from 'primeng/tag';
47
50
  import { TextareaModule } from 'primeng/textarea';
51
+ import { ToastModule } from 'primeng/toast';
48
52
  import { ToggleSwitchModule } from 'primeng/toggleswitch';
49
53
  import { TooltipModule } from 'primeng/tooltip';
50
54
  import { TreeTableModule } from 'primeng/treetable';
51
- import { toSignal, toObservable } from '@angular/core/rxjs-interop';
52
- import * as i4 from 'primeng/api';
53
- import { MessageService } from 'primeng/api';
55
+ import { MessageService, ConfirmationService } from 'primeng/api';
56
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
54
57
 
55
- ;
56
- ;
58
+ /**
59
+ * Centralized Permission Codes
60
+ *
61
+ * Single source of truth for all permission codes used across the application.
62
+ * Use these constants instead of hardcoded strings to prevent typos and enable easy refactoring.
63
+ *
64
+ * Naming Convention: <entity>.<action>
65
+ * - entity: The resource being accessed (e.g., user, role, company)
66
+ * - action: The operation being performed (create, read, update, delete, assign)
67
+ */
68
+ // ==================== AUTH MODULE ====================
69
+ const USER_PERMISSIONS = {
70
+ CREATE: 'user.create',
71
+ READ: 'user.read',
72
+ UPDATE: 'user.update',
73
+ DELETE: 'user.delete',
74
+ };
75
+ const COMPANY_PERMISSIONS = {
76
+ CREATE: 'company.create',
77
+ READ: 'company.read',
78
+ UPDATE: 'company.update',
79
+ DELETE: 'company.delete',
80
+ };
81
+ const BRANCH_PERMISSIONS = {
82
+ CREATE: 'branch.create',
83
+ READ: 'branch.read',
84
+ UPDATE: 'branch.update',
85
+ DELETE: 'branch.delete',
86
+ };
87
+ // ==================== IAM MODULE ====================
88
+ const ACTION_PERMISSIONS = {
89
+ CREATE: 'action.create',
90
+ READ: 'action.read',
91
+ UPDATE: 'action.update',
92
+ DELETE: 'action.delete',
93
+ };
94
+ const ROLE_PERMISSIONS = {
95
+ CREATE: 'role.create',
96
+ READ: 'role.read',
97
+ UPDATE: 'role.update',
98
+ DELETE: 'role.delete',
99
+ };
100
+ const ROLE_ACTION_PERMISSIONS = {
101
+ READ: 'role-action.read',
102
+ ASSIGN: 'role-action.assign',
103
+ };
104
+ const USER_ROLE_PERMISSIONS = {
105
+ READ: 'user-role.read',
106
+ ASSIGN: 'user-role.assign',
107
+ };
108
+ const USER_ACTION_PERMISSIONS = {
109
+ READ: 'user-action.read',
110
+ ASSIGN: 'user-action.assign',
111
+ };
112
+ const COMPANY_ACTION_PERMISSIONS = {
113
+ READ: 'company-action.read',
114
+ ASSIGN: 'company-action.assign',
115
+ };
116
+ // ==================== STORAGE MODULE ====================
117
+ const FILE_PERMISSIONS = {
118
+ CREATE: 'file.create',
119
+ READ: 'file.read',
120
+ UPDATE: 'file.update',
121
+ DELETE: 'file.delete',
122
+ };
123
+ const FOLDER_PERMISSIONS = {
124
+ CREATE: 'folder.create',
125
+ READ: 'folder.read',
126
+ UPDATE: 'folder.update',
127
+ DELETE: 'folder.delete',
128
+ };
129
+ const STORAGE_CONFIG_PERMISSIONS = {
130
+ CREATE: 'storage-config.create',
131
+ READ: 'storage-config.read',
132
+ UPDATE: 'storage-config.update',
133
+ DELETE: 'storage-config.delete',
134
+ };
135
+ // ==================== EMAIL MODULE ====================
136
+ const EMAIL_CONFIG_PERMISSIONS = {
137
+ CREATE: 'email-config.create',
138
+ READ: 'email-config.read',
139
+ UPDATE: 'email-config.update',
140
+ DELETE: 'email-config.delete',
141
+ };
142
+ const EMAIL_TEMPLATE_PERMISSIONS = {
143
+ CREATE: 'email-template.create',
144
+ READ: 'email-template.read',
145
+ UPDATE: 'email-template.update',
146
+ DELETE: 'email-template.delete',
147
+ };
148
+ // ==================== FORM BUILDER MODULE ====================
149
+ const FORM_PERMISSIONS = {
150
+ CREATE: 'form.create',
151
+ READ: 'form.read',
152
+ UPDATE: 'form.update',
153
+ DELETE: 'form.delete',
154
+ };
155
+ // ==================== AGGREGATED EXPORTS ====================
156
+ /**
157
+ * All permission codes grouped by module
158
+ */
159
+ const PERMISSIONS = {
160
+ // Auth
161
+ USER: USER_PERMISSIONS,
162
+ COMPANY: COMPANY_PERMISSIONS,
163
+ BRANCH: BRANCH_PERMISSIONS,
164
+ // IAM
165
+ ACTION: ACTION_PERMISSIONS,
166
+ ROLE: ROLE_PERMISSIONS,
167
+ ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
168
+ USER_ROLE: USER_ROLE_PERMISSIONS,
169
+ USER_ACTION: USER_ACTION_PERMISSIONS,
170
+ COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
171
+ // Storage
172
+ FILE: FILE_PERMISSIONS,
173
+ FOLDER: FOLDER_PERMISSIONS,
174
+ STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
175
+ // Email
176
+ EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
177
+ EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
178
+ // Form Builder
179
+ FORM: FORM_PERMISSIONS,
180
+ };
57
181
 
58
182
  /**
59
183
  * Common file type filters
@@ -147,9 +271,7 @@ class PlatformService {
147
271
  }
148
272
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
149
273
  type: Injectable,
150
- args: [{
151
- providedIn: 'root'
152
- }]
274
+ args: [{ providedIn: 'root' }]
153
275
  }] });
154
276
 
155
277
  class CookieService {
@@ -171,67 +293,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
171
293
 
172
294
  /**
173
295
  * Service to fetch file URLs from the backend.
174
- * Uses POST /file-manager/get-files endpoint which:
175
- * - Handles presigned URLs for cloud storage (AWS S3, Azure)
176
- * - Auto-refreshes expired URLs
177
- * - Validates file access permissions
178
- * - Works with all storage providers (Local, S3, Azure, SFTP)
296
+ * Handles presigned URLs for cloud storage, auto-refresh, and caching.
179
297
  */
180
298
  class FileUrlService {
181
299
  http = inject(HttpClient);
182
300
  appConfig = inject(APP_CONFIG);
183
- /** Cache of file URLs by file ID */
184
- urlCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "urlCache" }] : []));
185
- /**
186
- * Get file URL by ID from cache (reactive signal)
187
- */
301
+ _cache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_cache" }] : []));
302
+ cache = this._cache.asReadonly();
303
+ /** Get file URL by ID from cache (synchronous) */
188
304
  getFileUrl(fileId) {
189
305
  if (!fileId)
190
306
  return null;
191
- return this.urlCache().get(fileId)?.url ?? null;
307
+ return this._cache().get(fileId)?.url ?? null;
192
308
  }
193
- /**
194
- * Get file URL signal (computed from cache)
195
- */
309
+ /** Get file URL as computed signal */
196
310
  fileUrlSignal(fileId) {
197
311
  return computed(() => this.getFileUrl(fileId));
198
312
  }
199
- /**
200
- * Fetch file URLs from backend and update cache.
201
- * Returns Observable of fetched files.
202
- */
203
- fetchFileUrls(fileIds) {
313
+ /** Fetch file URLs from backend and update cache */
314
+ fetchFileUrls(fileIds, forceRefresh = false) {
204
315
  if (!fileIds.length)
205
316
  return of([]);
206
- const requestDto = fileIds.map((id) => ({ id }));
207
- return this.http.post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto).pipe(tap((files) => {
208
- // Update cache
209
- const cache = new Map(this.urlCache());
210
- files.forEach((file) => cache.set(file.id, file));
211
- this.urlCache.set(cache);
212
- }), catchError(() => of([])));
213
- }
214
- /**
215
- * Fetch a single file URL.
216
- * Returns Observable of file info or null if not found.
217
- */
218
- fetchSingleFileUrl(fileId) {
219
- return this.fetchFileUrls([fileId]).pipe(map((files) => files[0] ?? null));
220
- }
221
- /**
222
- * Clear the URL cache.
223
- * Useful on logout or when switching contexts.
224
- */
317
+ const cache = this._cache();
318
+ const missingIds = forceRefresh
319
+ ? fileIds
320
+ : fileIds.filter((id) => !cache.has(id));
321
+ if (!missingIds.length) {
322
+ return of(this.getFromCache(fileIds));
323
+ }
324
+ const requestDto = missingIds.map((id) => ({ id }));
325
+ return this.http
326
+ .post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto)
327
+ .pipe(map((response) => response.data ?? []), tap((files) => this.addToCache(files)), map(() => this.getFromCache(fileIds)), catchError(() => of([])));
328
+ }
329
+ /** Fetch single file URL (delegates to fetchFileUrls) */
330
+ fetchSingleFileUrl(fileId, forceRefresh = false) {
331
+ return this.fetchFileUrls([fileId], forceRefresh).pipe(map((files) => files[0] ?? null));
332
+ }
333
+ /** Clear entire cache */
225
334
  clearCache() {
226
- this.urlCache.set(new Map());
335
+ this._cache.set(new Map());
227
336
  }
228
- /**
229
- * Remove specific file from cache.
230
- */
337
+ /** Remove specific file from cache */
231
338
  removeFromCache(fileId) {
232
- const cache = new Map(this.urlCache());
233
- cache.delete(fileId);
234
- this.urlCache.set(cache);
339
+ this._cache.update((cache) => {
340
+ const next = new Map(cache);
341
+ next.delete(fileId);
342
+ return next;
343
+ });
344
+ }
345
+ addToCache(files) {
346
+ this._cache.update((cache) => {
347
+ const next = new Map(cache);
348
+ for (const file of files) {
349
+ next.set(file.id, file);
350
+ }
351
+ return next;
352
+ });
353
+ }
354
+ getFromCache(fileIds) {
355
+ const cache = this._cache();
356
+ return fileIds
357
+ .map((id) => cache.get(id))
358
+ .filter((f) => f !== undefined);
235
359
  }
236
360
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
237
361
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, providedIn: 'root' });
@@ -242,79 +366,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
242
366
  }] });
243
367
 
244
368
  /**
245
- * Permission Validator Service
246
- *
247
- * Centralized service for permission validation across all packages.
248
- * Provides signal-based state management and reactive permission checking.
249
- *
250
- * Features:
251
- * - Signal-based permission storage
252
- * - Individual permission checks
253
- * - Permission change detection
254
- * - Reactive permission state updates
255
- *
256
- * Usage:
257
- * ```typescript
258
- * // In component
259
- * readonly permissionValidator = inject(PermissionValidatorService);
260
- *
261
- * // Set permissions (typically from auth/IAM)
262
- * this.permissionValidator.setPermissions(['user.view', 'user.create']);
263
- *
264
- * // Check individual permission
265
- * if (this.permissionValidator.hasPermission('user.view')) {
266
- * // Show UI element
267
- * }
268
- *
269
- * // Access current permissions reactively
270
- * const permissions = this.permissionValidator.permissions();
271
- * ```
272
- *
273
- * @packageDocumentation
369
+ * Check if user has a specific permission using wildcard matching.
370
+ * Supports:
371
+ * - Exact match: 'user.create' matches 'user.create'
372
+ * - Full wildcard: '*' matches everything
373
+ * - Module wildcard: 'user.*' matches 'user.create', 'user.read', etc.
374
+ */
375
+ function hasPermission(requiredPermission, userPermissions) {
376
+ // Exact match
377
+ if (userPermissions.includes(requiredPermission))
378
+ return true;
379
+ // Wildcard matching
380
+ for (const permission of userPermissions) {
381
+ // Full wildcard - grants all permissions
382
+ if (permission === '*')
383
+ return true;
384
+ // Module wildcard (e.g., 'user.*' matches 'user.create')
385
+ if (permission.endsWith('.*') &&
386
+ requiredPermission.startsWith(permission.slice(0, -1))) {
387
+ return true;
388
+ }
389
+ }
390
+ return false;
391
+ }
392
+ /** Evaluate permission logic (string or ILogicNode) against user permissions */
393
+ function evaluatePermission(logic, permissions) {
394
+ if (!logic)
395
+ return false;
396
+ if (typeof logic === 'string')
397
+ return hasPermission(logic, permissions);
398
+ return evaluateLogicNode(logic, permissions);
399
+ }
400
+ /** Recursively evaluate an ILogicNode tree */
401
+ function evaluateLogicNode(node, permissions) {
402
+ switch (node.type) {
403
+ case 'action':
404
+ return node.actionId ? hasPermission(node.actionId, permissions) : false;
405
+ case 'group':
406
+ if (!node.children || node.children.length === 0)
407
+ return false;
408
+ return node.operator === 'AND'
409
+ ? node.children.every((child) => evaluateLogicNode(child, permissions))
410
+ : node.children.some((child) => evaluateLogicNode(child, permissions));
411
+ default:
412
+ return false;
413
+ }
414
+ }
415
+ /** Check if user has ANY of the specified permissions (OR logic) */
416
+ function hasAnyPermission(permissionCodes, permissions) {
417
+ if (!permissionCodes?.length)
418
+ return false;
419
+ return permissionCodes.some((code) => hasPermission(code, permissions));
420
+ }
421
+ /** Check if user has ALL of the specified permissions (AND logic) */
422
+ function hasAllPermissions(permissionCodes, permissions) {
423
+ if (!permissionCodes?.length)
424
+ return false;
425
+ return permissionCodes.every((code) => hasPermission(code, permissions));
426
+ }
427
+
428
+ /**
429
+ * Permission state management service.
430
+ * Provides signal-based storage and permission checking with wildcard support.
274
431
  */
275
432
  class PermissionValidatorService {
276
- // ==================== SIGNALS ====================
277
- /**
278
- * Private writable signal for permissions
279
- */
280
433
  _permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
281
- /**
282
- * Readonly permission signal for external consumers
283
- */
284
434
  permissions = this._permissions.asReadonly();
285
- /**
286
- * Private writable signal for loaded state
287
- */
288
435
  _isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "_isLoaded" }] : []));
289
- /**
290
- * Set permissions (replaces existing permissions)
291
- * @param permissions - Array of permission codes
292
- */
436
+ isLoaded = this._isLoaded.asReadonly();
437
+ /** Set permissions (replaces existing) */
293
438
  setPermissions(permissions) {
294
439
  this._permissions.set(permissions);
295
440
  this._isLoaded.set(true);
296
441
  }
297
- /**
298
- * Clear all permissions
299
- */
442
+ /** Clear all permissions */
300
443
  clearPermissions() {
301
444
  this._permissions.set([]);
302
445
  this._isLoaded.set(false);
303
446
  }
304
- // ==================== PERMISSION CHECKING ====================
305
- /**
306
- * Check if user has a specific permission
307
- * @param permissionCode - Required permission code
308
- * @returns True if user has permission
309
- */
310
- hasPermission(permissionCode) {
311
- return this.permissions().includes(permissionCode);
447
+ /** Check if user has permission (supports wildcards: '*', 'module.*') */
448
+ hasPermission(code) {
449
+ return hasPermission(code, this._permissions());
312
450
  }
313
- // ==================== UTILITY METHODS ====================
314
- /**
315
- * Check if permissions are loaded
316
- * @returns True if permissions have been loaded
317
- */
451
+ /** @deprecated Use `isLoaded()` signal instead */
318
452
  isPermissionsLoaded() {
319
453
  return this._isLoaded();
320
454
  }
@@ -323,9 +457,7 @@ class PermissionValidatorService {
323
457
  }
324
458
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, decorators: [{
325
459
  type: Injectable,
326
- args: [{
327
- providedIn: 'root',
328
- }]
460
+ args: [{ providedIn: 'root' }]
329
461
  }] });
330
462
 
331
463
  class EditModeElementChangerDirective {
@@ -388,46 +520,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
388
520
  type: Directive,
389
521
  args: [{
390
522
  selector: '[appEditModeElementChanger]',
391
- standalone: true,
392
523
  }]
393
524
  }], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
394
525
 
395
- /** Evaluate permission logic (string or ILogicNode) against user permissions */
396
- function evaluatePermission(logic, permissions) {
397
- if (!logic)
398
- return false;
399
- if (typeof logic === 'string')
400
- return permissions.includes(logic);
401
- return evaluateLogicNode(logic, permissions);
402
- }
403
- /** Recursively evaluate an ILogicNode tree */
404
- function evaluateLogicNode(node, permissions) {
405
- switch (node.type) {
406
- case 'action':
407
- return node.actionId ? permissions.includes(node.actionId) : false;
408
- case 'group':
409
- if (!node.children || node.children.length === 0)
410
- return false;
411
- return node.operator === 'AND'
412
- ? node.children.every((child) => evaluateLogicNode(child, permissions))
413
- : node.children.some((child) => evaluateLogicNode(child, permissions));
414
- default:
415
- return false;
416
- }
417
- }
418
- /** Check if user has ANY of the specified permissions (OR logic) */
419
- function hasAnyPermission(permissionCodes, permissions) {
420
- if (!permissionCodes?.length)
421
- return false;
422
- return permissionCodes.some((code) => permissions.includes(code));
423
- }
424
- /** Check if user has ALL of the specified permissions (AND logic) */
425
- function hasAllPermissions(permissionCodes, permissions) {
426
- if (!permissionCodes?.length)
427
- return false;
428
- return permissionCodes.every((code) => permissions.includes(code));
429
- }
430
-
431
526
  /**
432
527
  * HasPermission Directive
433
528
  *
@@ -548,7 +643,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
548
643
  type: Directive,
549
644
  args: [{
550
645
  selector: '[hasPermission]',
551
- standalone: true,
552
646
  }]
553
647
  }], ctorParameters: () => [], propDecorators: { hasPermission: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPermission", required: false }] }] } });
554
648
 
@@ -577,7 +671,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
577
671
  '[src]': 'imageSrc()',
578
672
  '(error)': 'onError()',
579
673
  },
580
- standalone: true,
581
674
  }]
582
675
  }], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }] } });
583
676
 
@@ -615,18 +708,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
615
708
  type: Directive,
616
709
  args: [{
617
710
  selector: '[appPreventDefault]',
618
- standalone: true,
711
+ host: {
712
+ '(click)': 'onClick($event)',
713
+ '(keydown)': 'onKeydown($event)',
714
+ '(keyup)': 'onKeyup($event)',
715
+ },
619
716
  }]
620
- }], propDecorators: { eventType: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventType", required: false }] }], preventKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventKey", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], onClick: [{
621
- type: HostListener,
622
- args: ['click', ['$event']]
623
- }], onKeydown: [{
624
- type: HostListener,
625
- args: ['keydown', ['$event']]
626
- }], onKeyup: [{
627
- type: HostListener,
628
- args: ['keyup', ['$event']]
629
- }] } });
717
+ }], propDecorators: { eventType: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventType", required: false }] }], preventKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventKey", required: false }] }], action: [{ type: i0.Output, args: ["action"] }] } });
630
718
 
631
719
  class AngularModule {
632
720
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -635,6 +723,7 @@ class AngularModule {
635
723
  ReactiveFormsModule,
636
724
  RouterOutlet,
637
725
  RouterLink,
726
+ RouterLinkActive,
638
727
  IsEmptyImageDirective,
639
728
  NgOptimizedImage,
640
729
  NgComponentOutlet,
@@ -643,6 +732,7 @@ class AngularModule {
643
732
  ReactiveFormsModule,
644
733
  RouterOutlet,
645
734
  RouterLink,
735
+ RouterLinkActive,
646
736
  IsEmptyImageDirective,
647
737
  NgOptimizedImage,
648
738
  NgComponentOutlet,
@@ -662,6 +752,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
662
752
  ReactiveFormsModule,
663
753
  RouterOutlet,
664
754
  RouterLink,
755
+ RouterLinkActive,
665
756
  IsEmptyImageDirective,
666
757
  NgOptimizedImage,
667
758
  NgComponentOutlet,
@@ -674,6 +765,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
674
765
  ReactiveFormsModule,
675
766
  RouterOutlet,
676
767
  RouterLink,
768
+ RouterLinkActive,
677
769
  IsEmptyImageDirective,
678
770
  NgOptimizedImage,
679
771
  NgComponentOutlet,
@@ -684,109 +776,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
684
776
 
685
777
  class PrimeModule {
686
778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
687
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [InputTextModule,
688
- TagModule,
689
- SelectButtonModule,
690
- PasswordModule,
779
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [AutoCompleteModule,
780
+ AvatarModule,
691
781
  ButtonModule,
692
- TooltipModule,
782
+ CardModule,
693
783
  CheckboxModule,
694
- StepsModule,
695
- RippleModule,
696
- PanelModule,
697
- PaginatorModule,
698
- TableModule,
699
- InputNumberModule,
700
- TextareaModule,
701
- ProgressBarModule,
784
+ ConfirmDialogModule,
785
+ DatePickerModule,
786
+ DialogModule,
787
+ DividerModule,
702
788
  FileUploadModule,
703
- CardModule,
704
- SelectModule,
705
- InputIconModule,
706
789
  IconFieldModule,
707
- PopoverModule,
790
+ ImageModule,
791
+ InputIconModule,
792
+ InputNumberModule,
793
+ InputTextModule,
708
794
  ListboxModule,
795
+ MultiSelectModule,
796
+ PaginatorModule,
797
+ PanelModule,
798
+ PasswordModule,
799
+ PopoverModule,
800
+ ProgressBarModule,
709
801
  RadioButtonModule,
710
- ToggleSwitchModule,
711
- ImageModule,
712
- DatePickerModule,
802
+ RippleModule,
803
+ SelectButtonModule,
804
+ SelectModule,
805
+ SkeletonModule,
713
806
  SplitButtonModule,
714
- DividerModule,
715
- MultiSelectModule,
716
- AutoCompleteModule,
807
+ StepsModule,
808
+ TableModule,
717
809
  TabsModule,
718
- DialogModule,
719
- TreeTableModule] });
720
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [InputTextModule,
721
810
  TagModule,
722
- SelectButtonModule,
723
- PasswordModule,
724
- ButtonModule,
811
+ TextareaModule,
812
+ ToastModule,
813
+ ToggleSwitchModule,
725
814
  TooltipModule,
815
+ TreeTableModule] });
816
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
817
+ AvatarModule,
818
+ ButtonModule,
819
+ CardModule,
726
820
  CheckboxModule,
727
- StepsModule,
728
- RippleModule,
729
- PanelModule,
730
- PaginatorModule,
731
- TableModule,
732
- InputNumberModule,
733
- TextareaModule,
734
- ProgressBarModule,
821
+ ConfirmDialogModule,
822
+ DatePickerModule,
823
+ DialogModule,
824
+ DividerModule,
735
825
  FileUploadModule,
736
- CardModule,
737
- SelectModule,
738
- InputIconModule,
739
826
  IconFieldModule,
740
- PopoverModule,
827
+ ImageModule,
828
+ InputIconModule,
829
+ InputNumberModule,
830
+ InputTextModule,
741
831
  ListboxModule,
832
+ MultiSelectModule,
833
+ PaginatorModule,
834
+ PanelModule,
835
+ PasswordModule,
836
+ PopoverModule,
837
+ ProgressBarModule,
742
838
  RadioButtonModule,
743
- ToggleSwitchModule,
744
- ImageModule,
745
- DatePickerModule,
839
+ RippleModule,
840
+ SelectButtonModule,
841
+ SelectModule,
842
+ SkeletonModule,
746
843
  SplitButtonModule,
747
- DividerModule,
748
- MultiSelectModule,
749
- AutoCompleteModule,
844
+ StepsModule,
845
+ TableModule,
750
846
  TabsModule,
751
- DialogModule,
847
+ TagModule,
848
+ TextareaModule,
849
+ ToastModule,
850
+ ToggleSwitchModule,
851
+ TooltipModule,
752
852
  TreeTableModule] });
753
853
  }
754
854
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, decorators: [{
755
855
  type: NgModule,
756
856
  args: [{
757
857
  exports: [
758
- InputTextModule,
759
- TagModule,
760
- SelectButtonModule,
761
- PasswordModule,
858
+ AutoCompleteModule,
859
+ AvatarModule,
762
860
  ButtonModule,
763
- TooltipModule,
764
- CheckboxModule,
765
- StepsModule,
766
- RippleModule,
767
- PanelModule,
768
- PaginatorModule,
769
- TableModule,
770
- InputNumberModule,
771
- TextareaModule,
772
- ProgressBarModule,
773
- FileUploadModule,
774
861
  CardModule,
775
- SelectModule,
776
- InputIconModule,
862
+ CheckboxModule,
863
+ ConfirmDialogModule,
864
+ DatePickerModule,
865
+ DialogModule,
866
+ DividerModule,
867
+ FileUploadModule,
777
868
  IconFieldModule,
778
- PopoverModule,
869
+ ImageModule,
870
+ InputIconModule,
871
+ InputNumberModule,
872
+ InputTextModule,
779
873
  ListboxModule,
874
+ MultiSelectModule,
875
+ PaginatorModule,
876
+ PanelModule,
877
+ PasswordModule,
878
+ PopoverModule,
879
+ ProgressBarModule,
780
880
  RadioButtonModule,
781
- ToggleSwitchModule,
782
- ImageModule,
783
- DatePickerModule,
881
+ RippleModule,
882
+ SelectButtonModule,
883
+ SelectModule,
884
+ SkeletonModule,
784
885
  SplitButtonModule,
785
- DividerModule,
786
- MultiSelectModule,
787
- AutoCompleteModule,
886
+ StepsModule,
887
+ TableModule,
788
888
  TabsModule,
789
- DialogModule,
889
+ TagModule,
890
+ TextareaModule,
891
+ ToastModule,
892
+ ToggleSwitchModule,
893
+ TooltipModule,
790
894
  TreeTableModule,
791
895
  ],
792
896
  }]
@@ -838,7 +942,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
838
942
  */
839
943
  class ApiResourceService {
840
944
  baseUrl;
841
- loaderService = inject(ApiLoaderService);
842
945
  injector = inject(Injector);
843
946
  http;
844
947
  moduleApiName;
@@ -861,6 +964,12 @@ class ApiResourceService {
861
964
  _listResource = null;
862
965
  /** Whether the list resource has been initialized */
863
966
  _resourceInitialized = false;
967
+ /**
968
+ * Signal to track resource initialization for computed signals.
969
+ * This allows computed signals to re-evaluate when the resource is created.
970
+ * Without this, computed signals would not detect when _listResource changes from null.
971
+ */
972
+ _resourceInitSignal = signal(false, ...(ngDevMode ? [{ debugName: "_resourceInitSignal" }] : []));
864
973
  /** Get or create the list resource (lazy initialization) */
865
974
  get listResource() {
866
975
  if (!this._listResource) {
@@ -887,20 +996,50 @@ class ApiResourceService {
887
996
  return this.fetchAllAsync(search, filter);
888
997
  } });
889
998
  });
999
+ // Signal that resource is now initialized - triggers computed re-evaluation
1000
+ this._resourceInitSignal.set(true);
890
1001
  }
891
1002
  // ==========================================================================
892
1003
  // Computed State Accessors
893
1004
  // ==========================================================================
894
- /** Whether data is currently loading */
895
- isLoading = computed(() => this._listResource?.isLoading() ?? false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
896
- /** List data array */
897
- data = computed(() => this._listResource?.value()?.data ?? [], ...(ngDevMode ? [{ debugName: "data" }] : []));
898
- /** Total count of items */
899
- total = computed(() => this._listResource?.value()?.meta?.total ?? 0, ...(ngDevMode ? [{ debugName: "total" }] : []));
900
- /** Pagination metadata */
901
- pageInfo = computed(() => this._listResource?.value()?.meta, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
902
- /** Whether there are more pages */
1005
+ /**
1006
+ * Whether data is currently loading.
1007
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1008
+ */
1009
+ isLoading = computed(() => {
1010
+ this._resourceInitSignal(); // Track initialization
1011
+ return this._listResource?.isLoading() ?? false;
1012
+ }, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1013
+ /**
1014
+ * List data array.
1015
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1016
+ */
1017
+ data = computed(() => {
1018
+ this._resourceInitSignal(); // Track initialization
1019
+ return this._listResource?.value()?.data ?? [];
1020
+ }, ...(ngDevMode ? [{ debugName: "data" }] : []));
1021
+ /**
1022
+ * Total count of items.
1023
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1024
+ */
1025
+ total = computed(() => {
1026
+ this._resourceInitSignal(); // Track initialization
1027
+ return this._listResource?.value()?.meta?.total ?? 0;
1028
+ }, ...(ngDevMode ? [{ debugName: "total" }] : []));
1029
+ /**
1030
+ * Pagination metadata.
1031
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1032
+ */
1033
+ pageInfo = computed(() => {
1034
+ this._resourceInitSignal(); // Track initialization
1035
+ return this._listResource?.value()?.meta;
1036
+ }, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
1037
+ /**
1038
+ * Whether there are more pages.
1039
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1040
+ */
903
1041
  hasMore = computed(() => {
1042
+ this._resourceInitSignal(); // Track initialization
904
1043
  const meta = this._listResource?.value()?.meta;
905
1044
  if (!meta)
906
1045
  return false;
@@ -1075,38 +1214,6 @@ class ApiResourceService {
1075
1214
  }
1076
1215
  }
1077
1216
 
1078
- class IconComponent {
1079
- icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1080
- iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1081
- IconTypeEnum = IconTypeEnum; // Needed for template reference
1082
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1083
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: IconComponent, isStandalone: true, selector: "lib-icon", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, iconType: { classPropertyName: "iconType", publicName: "iconType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1084
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1085
- <i [ngClass]="icon()"></i>
1086
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1087
- <img [alt]="icon()" [src]="icon()" />
1088
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1089
- {{ icon() }}
1090
- }@else{ I } }
1091
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }] });
1092
- }
1093
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
1094
- type: Component,
1095
- args: [{
1096
- selector: 'lib-icon',
1097
- imports: [AngularModule],
1098
- template: `
1099
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1100
- <i [ngClass]="icon()"></i>
1101
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1102
- <img [alt]="icon()" [src]="icon()" />
1103
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1104
- {{ icon() }}
1105
- }@else{ I } }
1106
- `,
1107
- }]
1108
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1109
-
1110
1217
  /**
1111
1218
  * Base class for form controls that support ALL Angular form patterns:
1112
1219
  *
@@ -1230,6 +1337,77 @@ function provideValueAccessor(component) {
1230
1337
  };
1231
1338
  }
1232
1339
 
1340
+ class IconComponent {
1341
+ icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1342
+ iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1343
+ IconTypeEnum = IconTypeEnum; // Needed for template reference
1344
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1345
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: IconComponent, isStandalone: true, selector: "lib-icon", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, iconType: { classPropertyName: "iconType", publicName: "iconType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1346
+ @if (icon()) {
1347
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1348
+ <i [class]="icon()"></i>
1349
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1350
+ <img [alt]="icon()" [src]="icon()" />
1351
+ } @else {
1352
+ <i class="pi pi-question"></i>
1353
+ }
1354
+ }
1355
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1356
+ }
1357
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
1358
+ type: Component,
1359
+ args: [{
1360
+ selector: 'lib-icon',
1361
+ imports: [AngularModule],
1362
+ changeDetection: ChangeDetectionStrategy.OnPush,
1363
+ template: `
1364
+ @if (icon()) {
1365
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1366
+ <i [class]="icon()"></i>
1367
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1368
+ <img [alt]="icon()" [src]="icon()" />
1369
+ } @else {
1370
+ <i class="pi pi-question"></i>
1371
+ }
1372
+ }
1373
+ `,
1374
+ }]
1375
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1376
+
1377
+ /**
1378
+ * Check if scroll has reached near bottom and calculate next page if available.
1379
+ * Returns next pagination if should load more, null otherwise.
1380
+ *
1381
+ * @example
1382
+ * ```typescript
1383
+ * onScroll(event: Event): void {
1384
+ * const nextPagination = checkScrollPagination(event, {
1385
+ * pagination: this.pagination(),
1386
+ * total: this.total(),
1387
+ * isLoading: this.isLoading(),
1388
+ * });
1389
+ * if (nextPagination) {
1390
+ * this.onPagination.emit(nextPagination);
1391
+ * }
1392
+ * }
1393
+ * ```
1394
+ */
1395
+ function checkScrollPagination(event, config) {
1396
+ const el = event.target;
1397
+ if (!(el instanceof HTMLElement))
1398
+ return null;
1399
+ const threshold = config.threshold ?? 50;
1400
+ const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
1401
+ if (!nearBottom || config.isLoading)
1402
+ return null;
1403
+ const { pagination, total } = config;
1404
+ const nextPage = pagination.currentPage + 1;
1405
+ const hasMore = nextPage * pagination.pageSize < (total ?? 0);
1406
+ if (!hasMore)
1407
+ return null;
1408
+ return { ...pagination, currentPage: nextPage };
1409
+ }
1410
+
1233
1411
  /**
1234
1412
  * Lazy-loading multi-select component with search, pagination, and select-all.
1235
1413
  *
@@ -1239,6 +1417,10 @@ function provideValueAccessor(component) {
1239
1417
  * - Signal forms: `[formField]="formTree.field"`
1240
1418
  */
1241
1419
  class LazyMultiSelectComponent extends BaseFormControl {
1420
+ destroyRef = inject(DestroyRef);
1421
+ onDocumentClickBound = this.handleDocumentClick.bind(this);
1422
+ // View references
1423
+ pSelectRef = viewChild.required('pSelect');
1242
1424
  // Inputs
1243
1425
  placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1244
1426
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1246,14 +1428,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
1246
1428
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1247
1429
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1248
1430
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1249
- /** Two-way bound value using model() for signal forms compatibility */
1431
+ // Two-way bound value
1250
1432
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1251
1433
  // Outputs
1252
1434
  onSearch = output();
1253
1435
  onPagination = output();
1254
- // UI signals
1436
+ // UI state
1255
1437
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1256
- /** Computed: Display text for selected values (replaces getSelectedValue method) */
1438
+ openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1439
+ // Computed values
1257
1440
  selectedValueDisplay = computed(() => {
1258
1441
  const selectedValues = this.value() ?? [];
1259
1442
  if (selectedValues.length === 0)
@@ -1262,44 +1445,59 @@ class LazyMultiSelectComponent extends BaseFormControl {
1262
1445
  return `${selectedValues.length} Items Selected`;
1263
1446
  }
1264
1447
  return this.selectDataList()
1265
- .filter(item => selectedValues.includes(item.value))
1266
- .map(item => item.label)
1448
+ .filter((item) => selectedValues.includes(item.value))
1449
+ .map((item) => item.label)
1267
1450
  .join(', ');
1268
1451
  }, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
1269
- /** Computed: Whether all items are selected (replaces isSelectAll signal) */
1270
1452
  isSelectAll = computed(() => {
1271
1453
  const selectedValues = this.value() ?? [];
1272
- const allValues = this.selectDataList().map(item => item.value);
1454
+ const allValues = this.selectDataList().map((item) => item.value);
1273
1455
  if (selectedValues.length === 0 || allValues.length === 0)
1274
1456
  return false;
1275
- return allValues.every(val => selectedValues.includes(val));
1457
+ return allValues.every((val) => selectedValues.includes(val));
1276
1458
  }, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
1277
1459
  constructor() {
1278
1460
  super();
1279
1461
  this.initializeFormControl();
1280
- // Search debounce effect
1281
- runInInjectionContext(this.injector, () => {
1282
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1((value) => {
1283
- this.onSearch.emit(value);
1284
- })), { initialValue: this.searchTerm() });
1462
+ // Search debounce using effect
1463
+ let debounceTimeout = null;
1464
+ let previousValue = this.searchTerm();
1465
+ effect((onCleanup) => {
1466
+ const currentValue = this.searchTerm();
1467
+ // Skip unchanged values
1468
+ if (currentValue === previousValue)
1469
+ return;
1470
+ previousValue = currentValue;
1471
+ // Clear existing timeout
1472
+ if (debounceTimeout)
1473
+ clearTimeout(debounceTimeout);
1474
+ // Debounced emit
1475
+ debounceTimeout = setTimeout(() => {
1476
+ this.onSearch.emit(currentValue);
1477
+ }, 500);
1478
+ onCleanup(() => {
1479
+ if (debounceTimeout)
1480
+ clearTimeout(debounceTimeout);
1481
+ });
1482
+ });
1483
+ // Document click listener for closing dropdown
1484
+ afterNextRender(() => {
1485
+ document.addEventListener('click', this.onDocumentClickBound);
1486
+ });
1487
+ this.destroyRef.onDestroy(() => {
1488
+ document.removeEventListener('click', this.onDocumentClickBound);
1285
1489
  });
1286
1490
  }
1287
- onScrollBound = this.onScroll.bind(this);
1288
- multiScrollContainer = viewChild.required('multiScrollContainer');
1289
1491
  onScroll(event) {
1290
- const el = event.target;
1291
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1292
- if (nearBottom && !this.isLoading()) {
1293
- const pagination = this.pagination();
1294
- const nextPage = pagination.currentPage + 1;
1295
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1296
- if (hasMore) {
1297
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1298
- }
1492
+ const nextPagination = checkScrollPagination(event, {
1493
+ pagination: this.pagination(),
1494
+ total: this.total(),
1495
+ isLoading: this.isLoading(),
1496
+ });
1497
+ if (nextPagination) {
1498
+ this.onPagination.emit(nextPagination);
1299
1499
  }
1300
1500
  }
1301
- pSelectRef = viewChild.required('pSelect');
1302
- openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1303
1501
  onSelectClick(event) {
1304
1502
  if (this.disabled())
1305
1503
  return;
@@ -1318,7 +1516,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1318
1516
  }
1319
1517
  }
1320
1518
  isSelected(data) {
1321
- return this.value()?.includes(data.value);
1519
+ return this.value()?.includes(data.value) ?? false;
1322
1520
  }
1323
1521
  key(option) {
1324
1522
  return option.value;
@@ -1331,8 +1529,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1331
1529
  }
1332
1530
  }
1333
1531
  else {
1334
- const updated = previousValue.filter((v) => v !== option.value);
1335
- this.value.set(updated);
1532
+ this.value.set(previousValue.filter((v) => v !== option.value));
1336
1533
  }
1337
1534
  }
1338
1535
  changeSelectAll(event) {
@@ -1348,15 +1545,12 @@ class LazyMultiSelectComponent extends BaseFormControl {
1348
1545
  this.value.set([]);
1349
1546
  }
1350
1547
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1351
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: LazyMultiSelectComponent, isStandalone: true, selector: "lib-lazy-multi-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, host: { listeners: { "document:click": "handleDocumentClick($event)" } }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ propertyName: "multiScrollContainer", first: true, predicate: ["multiScrollContainer"], descendants: true, isSignal: true }, { propertyName: "pSelectRef", first: true, predicate: ["pSelect"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1548
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: LazyMultiSelectComponent, isStandalone: true, selector: "lib-lazy-multi-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ propertyName: "pSelectRef", first: true, predicate: ["pSelect"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\" [class.p-disabled]=\"disabled()\">\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\">{{ selectedValueDisplay() }}</span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ placeHolder() }}</span>\n }\n\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\n @if (openOptions()) {\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1352
1549
  }
1353
1550
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1354
1551
  type: Component,
1355
- args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"] }]
1356
- }], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], multiScrollContainer: [{ type: i0.ViewChild, args: ['multiScrollContainer', { isSignal: true }] }], pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], handleDocumentClick: [{
1357
- type: HostListener,
1358
- args: ['document:click', ['$event']]
1359
- }] } });
1552
+ args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\" [class.p-disabled]=\"disabled()\">\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\">{{ selectedValueDisplay() }}</span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ placeHolder() }}</span>\n }\n\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\n @if (openOptions()) {\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"] }]
1553
+ }], ctorParameters: () => [], propDecorators: { pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }] } });
1360
1554
 
1361
1555
  /**
1362
1556
  * Lazy-loading single select component with search and pagination.
@@ -1367,6 +1561,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1367
1561
  * - Signal forms: `[formField]="formTree.field"`
1368
1562
  */
1369
1563
  class LazySelectComponent extends BaseFormControl {
1564
+ destroyRef = inject(DestroyRef);
1565
+ onScrollBound = this.onScroll.bind(this);
1566
+ scrollTargetEl = null;
1567
+ // View references
1568
+ scrollContainer = viewChild.required('scrollContainer');
1370
1569
  // Inputs
1371
1570
  placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1372
1571
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -1376,77 +1575,85 @@ class LazySelectComponent extends BaseFormControl {
1376
1575
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1377
1576
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1378
1577
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1379
- /** Two-way bound value using model() for signal forms compatibility */
1578
+ // Two-way bound value
1380
1579
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1381
1580
  // Outputs
1382
1581
  onSearch = output();
1383
1582
  onPagination = output();
1384
- // UI signals
1583
+ // UI state
1385
1584
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1386
- // Effect hooks
1585
+ isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1387
1586
  constructor() {
1388
1587
  super();
1389
1588
  this.initializeFormControl();
1390
- runInInjectionContext(this.injector, () => {
1391
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1(value => {
1392
- this.onSearch.emit(value);
1393
- })), { initialValue: this.searchTerm() });
1589
+ // Search debounce using effect
1590
+ let debounceTimeout = null;
1591
+ let previousValue = this.searchTerm();
1592
+ effect((onCleanup) => {
1593
+ const currentValue = this.searchTerm();
1594
+ // Skip unchanged values
1595
+ if (currentValue === previousValue)
1596
+ return;
1597
+ previousValue = currentValue;
1598
+ // Clear existing timeout
1599
+ if (debounceTimeout)
1600
+ clearTimeout(debounceTimeout);
1601
+ // Debounced emit
1602
+ debounceTimeout = setTimeout(() => {
1603
+ this.onSearch.emit(currentValue);
1604
+ }, 500);
1605
+ onCleanup(() => {
1606
+ if (debounceTimeout)
1607
+ clearTimeout(debounceTimeout);
1608
+ });
1609
+ });
1610
+ // Cleanup scroll listener on destroy
1611
+ this.destroyRef.onDestroy(() => {
1612
+ if (this.scrollTargetEl) {
1613
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1614
+ }
1394
1615
  });
1395
1616
  }
1396
- // Signal to toggle panel
1397
- isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1398
- scrollTargetEl = null;
1399
- onScrollBound = this.onScroll.bind(this);
1400
- scrollContainer = viewChild.required('scrollContainer');
1401
1617
  onScroll(event) {
1402
- const el = event.target;
1403
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1404
- if (nearBottom && !this.isLoading()) {
1405
- const pagination = this.pagination();
1406
- const nextPage = pagination.currentPage + 1;
1407
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1408
- if (hasMore) {
1409
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1410
- }
1618
+ const nextPagination = checkScrollPagination(event, {
1619
+ pagination: this.pagination(),
1620
+ total: this.total(),
1621
+ isLoading: this.isLoading(),
1622
+ });
1623
+ if (nextPagination) {
1624
+ this.onPagination.emit(nextPagination);
1411
1625
  }
1412
1626
  }
1413
- // Toggle panel and manage scroll event
1414
1627
  showPanel() {
1415
1628
  if (this.disabled())
1416
1629
  return;
1417
- this.isPanelShow.update(prev => !prev);
1630
+ this.isPanelShow.update((prev) => !prev);
1418
1631
  const isNowVisible = this.isPanelShow();
1419
1632
  if (isNowVisible) {
1420
- setTimeout(() => {
1633
+ afterNextRender(() => {
1421
1634
  const containerEl = this.scrollContainer().nativeElement;
1422
1635
  const target = containerEl.querySelector('.p-select-list-container');
1423
1636
  if (target) {
1424
1637
  target.addEventListener('scroll', this.onScrollBound);
1425
1638
  this.scrollTargetEl = target;
1426
1639
  }
1427
- }, 0);
1640
+ }, { injector: this.injector });
1428
1641
  }
1429
- else {
1430
- if (this.scrollTargetEl) {
1431
- this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1432
- this.scrollTargetEl = null;
1433
- }
1642
+ else if (this.scrollTargetEl) {
1643
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1644
+ this.scrollTargetEl = null;
1434
1645
  }
1435
1646
  }
1436
1647
  onBlur() {
1437
1648
  this.markAsTouched();
1438
1649
  }
1439
1650
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1440
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: LazySelectComponent, isStandalone: true, selector: "lib-lazy-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: true, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: true, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazySelectComponent)], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "directive", type: EditModeElementChangerDirective, selector: "[appEditModeElementChanger]", inputs: ["isEditMode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1651
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: LazySelectComponent, isStandalone: true, selector: "lib-lazy-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: true, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: true, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazySelectComponent)], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3$1.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "directive", type: EditModeElementChangerDirective, selector: "[appEditModeElementChanger]", inputs: ["isEditMode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1441
1652
  }
1442
1653
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
1443
1654
  type: Component,
1444
- args: [{ selector: 'lib-lazy-select', imports: [
1445
- AngularModule,
1446
- PrimeModule,
1447
- EditModeElementChangerDirective
1448
- ], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
1449
- }], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: true }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }] } });
1655
+ args: [{ selector: 'lib-lazy-select', imports: [AngularModule, PrimeModule, EditModeElementChangerDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
1656
+ }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: true }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }] } });
1450
1657
 
1451
1658
  /**
1452
1659
  * Injection Tokens for Provider Interfaces
@@ -1513,57 +1720,64 @@ const AUTH_STATE_PROVIDER = new InjectionToken('AUTH_STATE_PROVIDER', {
1513
1720
  throw new Error('AUTH_STATE_PROVIDER not configured. Please provide an implementation in app.config.ts');
1514
1721
  },
1515
1722
  });
1516
-
1517
- const DEFAULT_PAGE_SIZE$2 = 20;
1518
1723
  /**
1519
- * User Select Component - Single user selection with lazy loading.
1724
+ * Profile Permission Provider Token
1520
1725
  *
1521
- * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1726
+ * Provides user permission data for profile display.
1727
+ * Optional - if not configured, profile permissions section is hidden.
1728
+ * Use with `inject(PROFILE_PERMISSION_PROVIDER, { optional: true })`.
1729
+ */
1730
+ const PROFILE_PERMISSION_PROVIDER = new InjectionToken('PROFILE_PERMISSION_PROVIDER');
1731
+ /**
1732
+ * Profile Upload Provider Token
1522
1733
  *
1523
- * Features:
1524
- * - Search with debouncing (handled by lazy-select)
1525
- * - Infinite scroll pagination
1526
- * - Filter active users by default (configurable)
1527
- * - Supports additional filters via `additionalFilters` input
1734
+ * Provides file upload functionality for profile pictures.
1735
+ * Optional - if not configured or storage not enabled, upload section is hidden.
1736
+ * Use with `inject(PROFILE_UPLOAD_PROVIDER, { optional: true })`.
1737
+ */
1738
+ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1739
+ /**
1740
+ * User List Provider Token
1741
+ *
1742
+ * Provides extra actions, columns, and data enrichment for user list.
1743
+ * Optional - if not configured, default user list behavior is used.
1744
+ * Use with `inject(USER_LIST_PROVIDER, { optional: true })`.
1528
1745
  *
1529
1746
  * @example
1530
- * ```html
1531
- * <!-- Simple usage - uses USER_PROVIDER internally -->
1532
- * <lib-user-select
1533
- * [(value)]="selectedUserId"
1534
- * [isEditMode]="true"
1535
- * />
1747
+ * // In app.config.ts
1748
+ * providers: [
1749
+ * { provide: USER_LIST_PROVIDER, useClass: MyUserListProvider },
1750
+ * ]
1751
+ */
1752
+ const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
1753
+
1754
+ const DEFAULT_PAGE_SIZE$1 = 20;
1755
+ /**
1756
+ * Base class for user selection components.
1757
+ * Provides shared user loading, pagination, and search functionality.
1536
1758
  *
1537
- * <!-- With custom loadUsers function -->
1538
- * <lib-user-select
1539
- * [(value)]="selectedUserId"
1540
- * [isEditMode]="true"
1541
- * [loadUsers]="customLoadUsers"
1542
- * />
1543
- * ```
1759
+ * Subclasses must implement:
1760
+ * - `setupValueEffect()` to track value changes and emit selection events
1544
1761
  */
1545
- class UserSelectComponent {
1762
+ class BaseUserSelectComponent {
1546
1763
  destroyRef = inject(DestroyRef);
1764
+ injector = inject(Injector);
1547
1765
  userProvider = inject(USER_PROVIDER);
1548
1766
  abortController = null;
1549
- // Optional: custom function to load users (uses USER_PROVIDER if not provided)
1550
- loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1551
1767
  // Inputs
1768
+ loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1552
1769
  placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1553
1770
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1554
1771
  filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1555
1772
  additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
1556
- pageSize = input(DEFAULT_PAGE_SIZE$2, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1557
- // Two-way bound value
1558
- value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1773
+ pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1559
1774
  // Outputs
1560
- userSelected = output();
1561
1775
  onError = output();
1562
1776
  // Internal state
1563
1777
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1564
1778
  users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
1565
1779
  total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
1566
- pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$2, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1780
+ pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1567
1781
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1568
1782
  // Computed dropdown data
1569
1783
  dropdownUsers = computed(() => this.users().map((user) => ({
@@ -1586,20 +1800,8 @@ class UserSelectComponent {
1586
1800
  afterNextRender(() => {
1587
1801
  this.fetchUsers();
1588
1802
  });
1589
- // Emit selected user when value changes
1590
- effect(() => {
1591
- const selectedId = this.value();
1592
- const users = this.users();
1593
- untracked(() => {
1594
- if (selectedId) {
1595
- const user = users.find((u) => u.id === selectedId);
1596
- this.userSelected.emit(user ?? null);
1597
- }
1598
- else {
1599
- this.userSelected.emit(null);
1600
- }
1601
- });
1602
- });
1803
+ // Setup value change tracking (implemented by subclass)
1804
+ this.setupValueEffect();
1603
1805
  }
1604
1806
  handleSearch(search) {
1605
1807
  this.searchTerm.set(search);
@@ -1611,6 +1813,12 @@ class UserSelectComponent {
1611
1813
  this.pagination.set(pagination);
1612
1814
  this.fetchUsers(true);
1613
1815
  }
1816
+ /** Reload users (useful when filters change externally) */
1817
+ reload() {
1818
+ this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1819
+ this.users.set([]);
1820
+ this.fetchUsers();
1821
+ }
1614
1822
  async fetchUsers(append = false) {
1615
1823
  if (this.isLoading())
1616
1824
  return;
@@ -1668,14 +1876,63 @@ class UserSelectComponent {
1668
1876
  })),
1669
1877
  })));
1670
1878
  }
1671
- /** Reload users (useful when filters change externally) */
1672
- reload() {
1673
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1674
- this.users.set([]);
1675
- this.fetchUsers();
1879
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseUserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1880
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: BaseUserSelectComponent, isStandalone: true, inputs: { loadUsers: { classPropertyName: "loadUsers", publicName: "loadUsers", isSignal: true, isRequired: false, transformFunction: null }, placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, filterActive: { classPropertyName: "filterActive", publicName: "filterActive", isSignal: true, isRequired: false, transformFunction: null }, additionalFilters: { classPropertyName: "additionalFilters", publicName: "additionalFilters", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onError: "onError" }, ngImport: i0 });
1881
+ }
1882
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseUserSelectComponent, decorators: [{
1883
+ type: Directive
1884
+ }], ctorParameters: () => [], propDecorators: { loadUsers: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadUsers", required: false }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], filterActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterActive", required: false }] }], additionalFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "additionalFilters", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
1885
+
1886
+ /**
1887
+ * User Select Component - Single user selection with lazy loading.
1888
+ *
1889
+ * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1890
+ *
1891
+ * Features:
1892
+ * - Search with debouncing (handled by lazy-select)
1893
+ * - Infinite scroll pagination
1894
+ * - Filter active users by default (configurable)
1895
+ * - Supports additional filters via `additionalFilters` input
1896
+ *
1897
+ * @example
1898
+ * ```html
1899
+ * <!-- Simple usage - uses USER_PROVIDER internally -->
1900
+ * <lib-user-select
1901
+ * [(value)]="selectedUserId"
1902
+ * [isEditMode]="true"
1903
+ * />
1904
+ *
1905
+ * <!-- With custom loadUsers function -->
1906
+ * <lib-user-select
1907
+ * [(value)]="selectedUserId"
1908
+ * [isEditMode]="true"
1909
+ * [loadUsers]="customLoadUsers"
1910
+ * />
1911
+ * ```
1912
+ */
1913
+ class UserSelectComponent extends BaseUserSelectComponent {
1914
+ // Two-way bound value
1915
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1916
+ // Outputs
1917
+ userSelected = output();
1918
+ setupValueEffect() {
1919
+ // Emit selected user when value changes
1920
+ effect(() => {
1921
+ const selectedId = this.value();
1922
+ const users = this.users();
1923
+ untracked(() => {
1924
+ if (selectedId) {
1925
+ const user = users.find((u) => u.id === selectedId);
1926
+ this.userSelected.emit(user ?? null);
1927
+ }
1928
+ else {
1929
+ this.userSelected.emit(null);
1930
+ }
1931
+ });
1932
+ });
1676
1933
  }
1677
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1678
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserSelectComponent, isStandalone: true, selector: "lib-user-select", inputs: { loadUsers: { classPropertyName: "loadUsers", publicName: "loadUsers", isSignal: true, isRequired: false, transformFunction: null }, placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, filterActive: { classPropertyName: "filterActive", publicName: "filterActive", isSignal: true, isRequired: false, transformFunction: null }, additionalFilters: { classPropertyName: "additionalFilters", publicName: "additionalFilters", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", userSelected: "userSelected", onError: "onError" }, ngImport: i0, template: `
1934
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1935
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserSelectComponent, isStandalone: true, selector: "lib-user-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", userSelected: "userSelected" }, usesInheritance: true, ngImport: i0, template: `
1679
1936
  <lib-lazy-select
1680
1937
  [(value)]="value"
1681
1938
  [placeHolder]="placeHolder()"
@@ -1695,7 +1952,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1695
1952
  type: Component,
1696
1953
  args: [{
1697
1954
  selector: 'lib-user-select',
1698
- standalone: true,
1699
1955
  imports: [AngularModule, PrimeModule, LazySelectComponent],
1700
1956
  template: `
1701
1957
  <lib-lazy-select
@@ -1714,166 +1970,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1714
1970
  `,
1715
1971
  changeDetection: ChangeDetectionStrategy.OnPush,
1716
1972
  }]
1717
- }], ctorParameters: () => [], propDecorators: { loadUsers: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadUsers", required: false }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], filterActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterActive", required: false }] }], additionalFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "additionalFilters", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
1973
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
1718
1974
 
1719
- const DEFAULT_PAGE_SIZE$1 = 20;
1720
- /**
1721
- * User Multi-Select Component - Multiple user selection with lazy loading.
1722
- *
1723
- * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1724
- *
1725
- * Features:
1726
- * - Search with debouncing (handled by lazy-multi-select)
1727
- * - Infinite scroll pagination
1728
- * - Select all / deselect all
1729
- * - Filter active users by default (configurable)
1730
- * - Supports additional filters via `additionalFilters` input
1731
- *
1732
- * @example
1733
- * ```html
1734
- * <!-- Simple usage - uses USER_PROVIDER internally -->
1735
- * <lib-user-multi-select
1736
- * [(value)]="selectedUserIds"
1737
- * [isEditMode]="true"
1738
- * />
1739
- *
1740
- * <!-- With custom loadUsers function -->
1741
- * <lib-user-multi-select
1742
- * [(value)]="selectedUserIds"
1743
- * [isEditMode]="true"
1744
- * [loadUsers]="customLoadUsers"
1745
- * />
1746
- * ```
1747
- */
1748
- class UserMultiSelectComponent {
1749
- destroyRef = inject(DestroyRef);
1750
- userProvider = inject(USER_PROVIDER);
1751
- abortController = null;
1752
- // Optional: custom function to load users (uses USER_PROVIDER if not provided)
1753
- loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1754
- // Inputs
1755
- placeHolder = input('Select Users', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1756
- isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1757
- filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1758
- additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
1759
- pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1760
- // Two-way bound value
1761
- value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1762
- // Outputs
1763
- usersSelected = output();
1764
- onError = output();
1765
- // Internal state
1766
- isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1767
- users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
1768
- total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
1769
- pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1770
- searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1771
- // Computed dropdown data
1772
- dropdownUsers = computed(() => this.users().map((user) => ({
1773
- label: user.name || user.email,
1774
- value: user.id,
1775
- })), ...(ngDevMode ? [{ debugName: "dropdownUsers" }] : []));
1776
- constructor() {
1777
- // Cleanup on destroy
1778
- this.destroyRef.onDestroy(() => {
1779
- this.abortController?.abort();
1780
- });
1781
- // Update page size from input
1782
- effect(() => {
1783
- const size = this.pageSize();
1784
- untracked(() => {
1785
- this.pagination.update((p) => ({ ...p, pageSize: size }));
1786
- });
1787
- });
1788
- // Load initial users after render
1789
- afterNextRender(() => {
1790
- this.fetchUsers();
1791
- });
1792
- // Emit selected users when value changes
1793
- effect(() => {
1794
- const selectedIds = this.value() ?? [];
1795
- const users = this.users();
1796
- untracked(() => {
1797
- const selectedUsers = users.filter((u) => selectedIds.includes(u.id));
1798
- this.usersSelected.emit(selectedUsers);
1799
- });
1800
- });
1801
- }
1802
- handleSearch(search) {
1803
- this.searchTerm.set(search);
1804
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1805
- this.users.set([]);
1806
- this.fetchUsers();
1807
- }
1808
- handlePagination(pagination) {
1809
- this.pagination.set(pagination);
1810
- this.fetchUsers(true);
1811
- }
1812
- async fetchUsers(append = false) {
1813
- if (this.isLoading())
1814
- return;
1815
- // Cancel previous request
1816
- this.abortController?.abort();
1817
- this.abortController = new AbortController();
1818
- this.isLoading.set(true);
1819
- try {
1820
- const pag = this.pagination();
1821
- const filter = {
1822
- page: pag.currentPage,
1823
- pageSize: pag.pageSize,
1824
- search: this.searchTerm(),
1825
- ...this.additionalFilters(),
1826
- };
1827
- // Use custom loadUsers if provided, otherwise use USER_PROVIDER
1828
- const customLoadUsers = this.loadUsers();
1829
- const response = await firstValueFrom(customLoadUsers
1830
- ? customLoadUsers(filter)
1831
- : this.loadUsersFromProvider(filter));
1832
- if (response.success && response.data) {
1833
- if (append) {
1834
- this.users.update((current) => [...current, ...response.data]);
1835
- }
1836
- else {
1837
- this.users.set(response.data);
1838
- }
1839
- this.total.set(response.meta?.total);
1840
- }
1841
- }
1842
- catch (error) {
1843
- if (error.name !== 'AbortError') {
1844
- this.onError.emit(error);
1845
- }
1846
- }
1847
- finally {
1848
- this.isLoading.set(false);
1849
- }
1850
- }
1851
- /** Load users from USER_PROVIDER with active filter */
1852
- loadUsersFromProvider(filter) {
1853
- return this.userProvider
1854
- .getUsers({
1855
- page: filter.page,
1856
- pageSize: filter.pageSize,
1857
- search: filter.search,
1858
- isActive: this.filterActive() ? true : undefined,
1859
- })
1860
- .pipe(map$1((res) => ({
1861
- ...res,
1862
- data: res.data?.map((u) => ({
1863
- id: u.id,
1864
- name: u.name,
1865
- email: u.email,
1866
- })),
1867
- })));
1868
- }
1869
- /** Reload users (useful when filters change externally) */
1870
- reload() {
1871
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1872
- this.users.set([]);
1873
- this.fetchUsers();
1975
+ /**
1976
+ * User Multi-Select Component - Multiple user selection with lazy loading.
1977
+ *
1978
+ * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1979
+ *
1980
+ * Features:
1981
+ * - Search with debouncing (handled by lazy-multi-select)
1982
+ * - Infinite scroll pagination
1983
+ * - Select all / deselect all
1984
+ * - Filter active users by default (configurable)
1985
+ * - Supports additional filters via `additionalFilters` input
1986
+ *
1987
+ * @example
1988
+ * ```html
1989
+ * <!-- Simple usage - uses USER_PROVIDER internally -->
1990
+ * <lib-user-multi-select
1991
+ * [(value)]="selectedUserIds"
1992
+ * [isEditMode]="true"
1993
+ * />
1994
+ *
1995
+ * <!-- With custom loadUsers function -->
1996
+ * <lib-user-multi-select
1997
+ * [(value)]="selectedUserIds"
1998
+ * [isEditMode]="true"
1999
+ * [loadUsers]="customLoadUsers"
2000
+ * />
2001
+ * ```
2002
+ */
2003
+ class UserMultiSelectComponent extends BaseUserSelectComponent {
2004
+ // Two-way bound value
2005
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
2006
+ // Outputs
2007
+ usersSelected = output();
2008
+ setupValueEffect() {
2009
+ // Emit selected users when value changes
2010
+ effect(() => {
2011
+ const selectedIds = this.value() ?? [];
2012
+ const users = this.users();
2013
+ untracked(() => {
2014
+ const selectedUsers = users.filter((u) => selectedIds.includes(u.id));
2015
+ this.usersSelected.emit(selectedUsers);
2016
+ });
2017
+ });
1874
2018
  }
1875
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1876
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserMultiSelectComponent, isStandalone: true, selector: "lib-user-multi-select", inputs: { loadUsers: { classPropertyName: "loadUsers", publicName: "loadUsers", isSignal: true, isRequired: false, transformFunction: null }, placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, filterActive: { classPropertyName: "filterActive", publicName: "filterActive", isSignal: true, isRequired: false, transformFunction: null }, additionalFilters: { classPropertyName: "additionalFilters", publicName: "additionalFilters", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", usersSelected: "usersSelected", onError: "onError" }, ngImport: i0, template: `
2019
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2020
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.3", type: UserMultiSelectComponent, isStandalone: true, selector: "lib-user-multi-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", usersSelected: "usersSelected" }, usesInheritance: true, ngImport: i0, template: `
1877
2021
  <lib-lazy-multi-select
1878
2022
  [(value)]="value"
1879
2023
  [placeHolder]="placeHolder()"
@@ -1891,7 +2035,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1891
2035
  type: Component,
1892
2036
  args: [{
1893
2037
  selector: 'lib-user-multi-select',
1894
- standalone: true,
1895
2038
  imports: [AngularModule, PrimeModule, LazyMultiSelectComponent],
1896
2039
  template: `
1897
2040
  <lib-lazy-multi-select
@@ -1908,47 +2051,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1908
2051
  `,
1909
2052
  changeDetection: ChangeDetectionStrategy.OnPush,
1910
2053
  }]
1911
- }], ctorParameters: () => [], propDecorators: { loadUsers: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadUsers", required: false }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], filterActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterActive", required: false }] }], additionalFilters: [{ type: i0.Input, args: [{ isSignal: true, alias: "additionalFilters", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
2054
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
1912
2055
 
1913
2056
  /**
1914
2057
  * File Uploader Component - Drag & drop file upload with type filtering.
1915
2058
  *
1916
2059
  * Pass your own `uploadFile` function - works with any storage API.
1917
- *
1918
- * Features:
1919
- * - Drag & drop support
1920
- * - File type filtering (images, documents, etc.)
1921
- * - Upload progress indication
1922
- * - Multiple file support (optional)
1923
- * - Image compression options
1924
- *
1925
- * @example
1926
- * ```typescript
1927
- * // In component
1928
- * readonly uploadService = inject(UploadService);
1929
- *
1930
- * readonly uploadFile: UploadFileFn = (file, options) =>
1931
- * this.uploadService.uploadSingleFile(file, options);
1932
- * ```
1933
- *
1934
- * ```html
1935
- * <!-- Single image upload -->
1936
- * <lib-file-uploader
1937
- * [uploadFile]="uploadFile"
1938
- * [acceptTypes]="['image/*']"
1939
- * [multiple]="false"
1940
- * (fileUploaded)="onFileUploaded($event)"
1941
- * />
1942
- *
1943
- * <!-- Multiple document upload -->
1944
- * <lib-file-uploader
1945
- * [uploadFile]="uploadFile"
1946
- * [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
1947
- * [multiple]="true"
1948
- * [maxFiles]="5"
1949
- * (filesUploaded)="onFilesUploaded($event)"
1950
- * />
1951
- * ```
1952
2060
  */
1953
2061
  class FileUploaderComponent {
1954
2062
  messageService = inject(MessageService);
@@ -2094,11 +2202,7 @@ class FileUploaderComponent {
2094
2202
  });
2095
2203
  }
2096
2204
  catch (error) {
2097
- this.messageService.add({
2098
- severity: 'error',
2099
- summary: 'Upload Failed',
2100
- detail: error.message || 'Failed to upload file',
2101
- });
2205
+ // Error toast handled by global interceptor
2102
2206
  this.onError.emit(error);
2103
2207
  }
2104
2208
  finally {
@@ -2120,37 +2224,41 @@ class FileUploaderComponent {
2120
2224
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2121
2225
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: FileUploaderComponent, isStandalone: true, selector: "lib-file-uploader", inputs: { uploadFile: { classPropertyName: "uploadFile", publicName: "uploadFile", isSignal: true, isRequired: true, transformFunction: null }, acceptTypes: { classPropertyName: "acceptTypes", publicName: "acceptTypes", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxSizeMb: { classPropertyName: "maxSizeMb", publicName: "maxSizeMb", isSignal: true, isRequired: false, transformFunction: null }, uploadOptions: { classPropertyName: "uploadOptions", publicName: "uploadOptions", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileUploaded: "fileUploaded", filesUploaded: "filesUploaded", onError: "onError", fileSelected: "fileSelected" }, ngImport: i0, template: `
2122
2226
  <div
2123
- class="file-uploader"
2124
- [class.drag-over]="isDragOver()"
2125
- [class.disabled]="disabled()"
2227
+ class="w-full"
2228
+ [class.opacity-60]="disabled()"
2126
2229
  (dragover)="onDragOver($event)"
2127
2230
  (dragleave)="onDragLeave($event)"
2128
2231
  (drop)="onDrop($event)"
2129
2232
  >
2130
- <!-- Upload Area -->
2131
- <div class="upload-area" (click)="fileInput.click()">
2233
+ <!-- Upload Area - Responsive padding -->
2234
+ <div
2235
+ class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
2236
+ [class.drag-over]="isDragOver()"
2237
+ [class.cursor-not-allowed]="disabled()"
2238
+ (click)="fileInput.click()"
2239
+ >
2132
2240
  @if (isUploading()) {
2133
- <div class="uploading-state">
2134
- <i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
2135
- <p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
2241
+ <div class="flex flex-col items-center">
2242
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2243
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2136
2244
  @if (uploadProgress() > 0) {
2137
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2245
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2138
2246
  }
2139
2247
  </div>
2140
2248
  } @else {
2141
- <div class="idle-state text-center">
2142
- <i class="pi pi-cloud-upload text-4xl text-primary"></i>
2143
- <p class="mt-2 mb-1 font-semibold">
2249
+ <div class="flex flex-col items-center">
2250
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2251
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2144
2252
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2145
2253
  </p>
2146
- <p class="text-sm text-color-secondary">
2254
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2147
2255
  @if (acceptTypesDisplay()) {
2148
2256
  Allowed: {{ acceptTypesDisplay() }}
2149
2257
  } @else {
2150
2258
  All file types allowed
2151
2259
  }
2152
2260
  @if (maxSizeMb()) {
2153
- (Max {{ maxSizeMb() }}MB)
2261
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2154
2262
  }
2155
2263
  </p>
2156
2264
  </div>
@@ -2161,71 +2269,76 @@ class FileUploaderComponent {
2161
2269
  <input
2162
2270
  #fileInput
2163
2271
  type="file"
2272
+ class="hidden"
2164
2273
  [accept]="acceptString()"
2165
2274
  [multiple]="multiple()"
2166
2275
  [disabled]="disabled() || isUploading()"
2167
2276
  (change)="onFileSelected($event)"
2168
- class="hidden"
2169
2277
  />
2170
2278
 
2171
- <!-- Selected Files Preview -->
2279
+ <!-- Selected Files Preview - Responsive layout -->
2172
2280
  @if (selectedFiles().length > 0 && showPreview()) {
2173
- <div class="selected-files mt-3">
2281
+ <div class="mt-3 space-y-2">
2174
2282
  @for (file of selectedFiles(); track file.name) {
2175
- <div class="file-item flex align-items-center gap-2 p-2 border-round surface-border border-1 mb-2">
2176
- <i [class]="getFileIcon(file)"></i>
2177
- <span class="flex-1 text-overflow-ellipsis overflow-hidden">{{ file.name }}</span>
2178
- <span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
2179
- <button
2180
- pButton
2181
- type="button"
2283
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2284
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2285
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2286
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2287
+ <p-button
2182
2288
  icon="pi pi-times"
2183
- class="p-button-text p-button-rounded p-button-sm"
2184
- (click)="removeFile(file)"
2289
+ [text]="true"
2290
+ [rounded]="true"
2291
+ size="small"
2292
+ severity="secondary"
2185
2293
  [disabled]="isUploading()"
2186
- ></button>
2294
+ (onClick)="removeFile(file)"
2295
+ />
2187
2296
  </div>
2188
2297
  }
2189
2298
  </div>
2190
2299
  }
2191
2300
  </div>
2192
- `, isInline: true, styles: [".file-uploader{width:100%}.upload-area{border:2px dashed var(--surface-border);border-radius:var(--border-radius);padding:2rem;cursor:pointer;transition:all .2s;background:var(--surface-ground)}.upload-area:hover{border-color:var(--primary-color);background:var(--surface-hover)}.drag-over .upload-area{border-color:var(--primary-color);background:var(--primary-100)}.disabled .upload-area{opacity:.6;cursor:not-allowed}.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$3.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i2$1.ProgressBar, selector: "p-progressBar, p-progressbar, p-progress-bar", inputs: ["value", "showValue", "styleClass", "valueStyleClass", "unit", "mode", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2301
+ `, isInline: true, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1$2.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i2$1.ProgressBar, selector: "p-progressBar, p-progressbar, p-progress-bar", inputs: ["value", "showValue", "styleClass", "valueStyleClass", "unit", "mode", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2193
2302
  }
2194
2303
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
2195
2304
  type: Component,
2196
- args: [{ selector: 'lib-file-uploader', standalone: true, imports: [AngularModule, PrimeModule], template: `
2305
+ args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
2197
2306
  <div
2198
- class="file-uploader"
2199
- [class.drag-over]="isDragOver()"
2200
- [class.disabled]="disabled()"
2307
+ class="w-full"
2308
+ [class.opacity-60]="disabled()"
2201
2309
  (dragover)="onDragOver($event)"
2202
2310
  (dragleave)="onDragLeave($event)"
2203
2311
  (drop)="onDrop($event)"
2204
2312
  >
2205
- <!-- Upload Area -->
2206
- <div class="upload-area" (click)="fileInput.click()">
2313
+ <!-- Upload Area - Responsive padding -->
2314
+ <div
2315
+ class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
2316
+ [class.drag-over]="isDragOver()"
2317
+ [class.cursor-not-allowed]="disabled()"
2318
+ (click)="fileInput.click()"
2319
+ >
2207
2320
  @if (isUploading()) {
2208
- <div class="uploading-state">
2209
- <i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
2210
- <p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
2321
+ <div class="flex flex-col items-center">
2322
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2323
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2211
2324
  @if (uploadProgress() > 0) {
2212
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2325
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2213
2326
  }
2214
2327
  </div>
2215
2328
  } @else {
2216
- <div class="idle-state text-center">
2217
- <i class="pi pi-cloud-upload text-4xl text-primary"></i>
2218
- <p class="mt-2 mb-1 font-semibold">
2329
+ <div class="flex flex-col items-center">
2330
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2331
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2219
2332
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2220
2333
  </p>
2221
- <p class="text-sm text-color-secondary">
2334
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2222
2335
  @if (acceptTypesDisplay()) {
2223
2336
  Allowed: {{ acceptTypesDisplay() }}
2224
2337
  } @else {
2225
2338
  All file types allowed
2226
2339
  }
2227
2340
  @if (maxSizeMb()) {
2228
- (Max {{ maxSizeMb() }}MB)
2341
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2229
2342
  }
2230
2343
  </p>
2231
2344
  </div>
@@ -2236,35 +2349,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2236
2349
  <input
2237
2350
  #fileInput
2238
2351
  type="file"
2352
+ class="hidden"
2239
2353
  [accept]="acceptString()"
2240
2354
  [multiple]="multiple()"
2241
2355
  [disabled]="disabled() || isUploading()"
2242
2356
  (change)="onFileSelected($event)"
2243
- class="hidden"
2244
2357
  />
2245
2358
 
2246
- <!-- Selected Files Preview -->
2359
+ <!-- Selected Files Preview - Responsive layout -->
2247
2360
  @if (selectedFiles().length > 0 && showPreview()) {
2248
- <div class="selected-files mt-3">
2361
+ <div class="mt-3 space-y-2">
2249
2362
  @for (file of selectedFiles(); track file.name) {
2250
- <div class="file-item flex align-items-center gap-2 p-2 border-round surface-border border-1 mb-2">
2251
- <i [class]="getFileIcon(file)"></i>
2252
- <span class="flex-1 text-overflow-ellipsis overflow-hidden">{{ file.name }}</span>
2253
- <span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
2254
- <button
2255
- pButton
2256
- type="button"
2363
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2364
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2365
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2366
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2367
+ <p-button
2257
2368
  icon="pi pi-times"
2258
- class="p-button-text p-button-rounded p-button-sm"
2259
- (click)="removeFile(file)"
2369
+ [text]="true"
2370
+ [rounded]="true"
2371
+ size="small"
2372
+ severity="secondary"
2260
2373
  [disabled]="isUploading()"
2261
- ></button>
2374
+ (onClick)="removeFile(file)"
2375
+ />
2262
2376
  </div>
2263
2377
  }
2264
2378
  </div>
2265
2379
  }
2266
2380
  </div>
2267
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-uploader{width:100%}.upload-area{border:2px dashed var(--surface-border);border-radius:var(--border-radius);padding:2rem;cursor:pointer;transition:all .2s;background:var(--surface-ground)}.upload-area:hover{border-color:var(--primary-color);background:var(--surface-hover)}.drag-over .upload-area{border-color:var(--primary-color);background:var(--primary-100)}.disabled .upload-area{opacity:.6;cursor:not-allowed}.hidden{display:none}\n"] }]
2381
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"] }]
2268
2382
  }], propDecorators: { uploadFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFile", required: true }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeMb", required: false }] }], uploadOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadOptions", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], fileUploaded: [{ type: i0.Output, args: ["fileUploaded"] }], filesUploaded: [{ type: i0.Output, args: ["filesUploaded"] }], onError: [{ type: i0.Output, args: ["onError"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }] } });
2269
2383
 
2270
2384
  const DEFAULT_PAGE_SIZE = 20;
@@ -2379,16 +2493,14 @@ class FileSelectorDialogComponent {
2379
2493
  }, 500);
2380
2494
  }
2381
2495
  onScroll(event) {
2382
- const el = event.target;
2383
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
2384
- if (nearBottom && !this.isLoading()) {
2385
- const pag = this.pagination();
2386
- const nextPage = pag.currentPage + 1;
2387
- const hasMore = nextPage * pag.pageSize < (this.total() ?? 0);
2388
- if (hasMore) {
2389
- this.pagination.update((p) => ({ ...p, currentPage: nextPage }));
2390
- this.fetchFiles(true);
2391
- }
2496
+ const nextPagination = checkScrollPagination(event, {
2497
+ pagination: this.pagination(),
2498
+ total: this.total(),
2499
+ isLoading: this.isLoading(),
2500
+ });
2501
+ if (nextPagination) {
2502
+ this.pagination.set(nextPagination);
2503
+ this.fetchFiles(true);
2392
2504
  }
2393
2505
  }
2394
2506
  toggleSelection(file) {
@@ -2499,11 +2611,13 @@ class FileSelectorDialogComponent {
2499
2611
  [closable]="true"
2500
2612
  [draggable]="false"
2501
2613
  [resizable]="false"
2502
- [style]="{ width: '800px', maxHeight: '90vh' }"
2614
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2615
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2616
+ styleClass="file-selector-dialog"
2503
2617
  (onHide)="onDialogHide()"
2504
2618
  >
2505
2619
  <!-- Search Bar -->
2506
- <div class="flex gap-2 mb-3">
2620
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2507
2621
  <span class="p-input-icon-left flex-1">
2508
2622
  <i class="pi pi-search"></i>
2509
2623
  <input
@@ -2516,25 +2630,25 @@ class FileSelectorDialogComponent {
2516
2630
  />
2517
2631
  </span>
2518
2632
  @if (multiple()) {
2519
- <span class="text-sm text-color-secondary align-self-center">
2633
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2520
2634
  {{ selectedFiles().length }} selected
2521
2635
  </span>
2522
2636
  }
2523
2637
  </div>
2524
2638
 
2525
- <!-- File Grid -->
2639
+ <!-- File Grid - Responsive columns -->
2526
2640
  <div
2527
2641
  class="file-grid"
2528
2642
  #scrollContainer
2529
2643
  (scroll)="onScroll($event)"
2530
2644
  >
2531
2645
  @if (isLoading() && files().length === 0) {
2532
- <div class="flex justify-content-center p-4">
2533
- <i class="pi pi-spin pi-spinner text-4xl"></i>
2646
+ <div class="col-span-full flex justify-center p-4">
2647
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2534
2648
  </div>
2535
2649
  } @else if (files().length === 0) {
2536
- <div class="text-center p-4 text-color-secondary">
2537
- <i class="pi pi-inbox text-4xl mb-2"></i>
2650
+ <div class="col-span-full text-center p-4 text-color-secondary">
2651
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2538
2652
  <p>No files found</p>
2539
2653
  </div>
2540
2654
  } @else {
@@ -2548,54 +2662,59 @@ class FileSelectorDialogComponent {
2548
2662
  <!-- File Preview -->
2549
2663
  <div class="file-preview">
2550
2664
  @if (isImage(file) && file.url) {
2551
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2665
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2552
2666
  } @else {
2553
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2667
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2554
2668
  }
2555
2669
  @if (isSelected(file)) {
2556
2670
  <div class="selected-overlay">
2557
- <i class="pi pi-check"></i>
2671
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2558
2672
  </div>
2559
2673
  }
2560
2674
  </div>
2561
2675
 
2562
2676
  <!-- File Info -->
2563
- <div class="file-info">
2564
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2565
- <span class="file-size">{{ formatSize(file.size) }}</span>
2677
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2678
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2679
+ {{ file.name }}
2680
+ </span>
2681
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2566
2682
  </div>
2567
2683
  </div>
2568
2684
  }
2569
2685
 
2570
2686
  @if (isLoading()) {
2571
- <div class="flex justify-content-center p-2 w-full">
2572
- <i class="pi pi-spin pi-spinner"></i>
2687
+ <div class="col-span-full flex justify-center p-2">
2688
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2573
2689
  </div>
2574
2690
  }
2575
2691
  }
2576
2692
  </div>
2577
2693
 
2578
2694
  <!-- Footer -->
2579
- <ng-template pTemplate="footer">
2580
- <button
2581
- pButton
2582
- label="Cancel"
2583
- class="p-button-text"
2584
- (click)="onCancel()"
2585
- ></button>
2586
- <button
2587
- pButton
2588
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2589
- [disabled]="selectedFiles().length === 0"
2590
- (click)="onConfirm()"
2591
- ></button>
2695
+ <ng-template #footer>
2696
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2697
+ <button
2698
+ pButton
2699
+ label="Cancel"
2700
+ class="p-button-text w-full sm:w-auto"
2701
+ (click)="onCancel()"
2702
+ ></button>
2703
+ <button
2704
+ pButton
2705
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2706
+ [disabled]="selectedFiles().length === 0"
2707
+ class="w-full sm:w-auto"
2708
+ (click)="onConfirm()"
2709
+ ></button>
2710
+ </div>
2592
2711
  </ng-template>
2593
2712
  </p-dialog>
2594
- `, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;max-height:400px;overflow-y:auto;padding:.5rem}.file-card{border:2px solid var(--surface-border);border-radius:var(--border-radius);cursor:pointer;transition:all .2s;overflow:hidden}.file-card:hover:not(.disabled){border-color:var(--primary-color)}.file-card.selected{border-color:var(--primary-color);background:var(--primary-50)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:100px;display:flex;align-items:center;justify-content:center;background:var(--surface-ground)}.preview-image{width:100%;height:100%;object-fit:cover}.preview-icon{font-size:3rem;color:var(--text-color-secondary)}.selected-overlay{position:absolute;inset:0;background:rgba(var(--primary-color-rgb),.3);display:flex;align-items:center;justify-content:center}.selected-overlay i{font-size:2rem;color:var(--primary-color);background:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "directive", type: i4.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i1$3.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i6.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2713
+ `, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$2.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i4.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2595
2714
  }
2596
2715
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2597
2716
  type: Component,
2598
- args: [{ selector: 'lib-file-selector-dialog', standalone: true, imports: [AngularModule, PrimeModule], template: `
2717
+ args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
2599
2718
  <p-dialog
2600
2719
  [header]="header()"
2601
2720
  [(visible)]="visible"
@@ -2603,11 +2722,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2603
2722
  [closable]="true"
2604
2723
  [draggable]="false"
2605
2724
  [resizable]="false"
2606
- [style]="{ width: '800px', maxHeight: '90vh' }"
2725
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2726
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2727
+ styleClass="file-selector-dialog"
2607
2728
  (onHide)="onDialogHide()"
2608
2729
  >
2609
2730
  <!-- Search Bar -->
2610
- <div class="flex gap-2 mb-3">
2731
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2611
2732
  <span class="p-input-icon-left flex-1">
2612
2733
  <i class="pi pi-search"></i>
2613
2734
  <input
@@ -2620,25 +2741,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2620
2741
  />
2621
2742
  </span>
2622
2743
  @if (multiple()) {
2623
- <span class="text-sm text-color-secondary align-self-center">
2744
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2624
2745
  {{ selectedFiles().length }} selected
2625
2746
  </span>
2626
2747
  }
2627
2748
  </div>
2628
2749
 
2629
- <!-- File Grid -->
2750
+ <!-- File Grid - Responsive columns -->
2630
2751
  <div
2631
2752
  class="file-grid"
2632
2753
  #scrollContainer
2633
2754
  (scroll)="onScroll($event)"
2634
2755
  >
2635
2756
  @if (isLoading() && files().length === 0) {
2636
- <div class="flex justify-content-center p-4">
2637
- <i class="pi pi-spin pi-spinner text-4xl"></i>
2757
+ <div class="col-span-full flex justify-center p-4">
2758
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2638
2759
  </div>
2639
2760
  } @else if (files().length === 0) {
2640
- <div class="text-center p-4 text-color-secondary">
2641
- <i class="pi pi-inbox text-4xl mb-2"></i>
2761
+ <div class="col-span-full text-center p-4 text-color-secondary">
2762
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2642
2763
  <p>No files found</p>
2643
2764
  </div>
2644
2765
  } @else {
@@ -2652,182 +2773,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2652
2773
  <!-- File Preview -->
2653
2774
  <div class="file-preview">
2654
2775
  @if (isImage(file) && file.url) {
2655
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2776
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2656
2777
  } @else {
2657
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2778
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2658
2779
  }
2659
2780
  @if (isSelected(file)) {
2660
2781
  <div class="selected-overlay">
2661
- <i class="pi pi-check"></i>
2782
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2662
2783
  </div>
2663
2784
  }
2664
2785
  </div>
2665
2786
 
2666
2787
  <!-- File Info -->
2667
- <div class="file-info">
2668
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2669
- <span class="file-size">{{ formatSize(file.size) }}</span>
2788
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2789
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2790
+ {{ file.name }}
2791
+ </span>
2792
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2670
2793
  </div>
2671
2794
  </div>
2672
2795
  }
2673
2796
 
2674
2797
  @if (isLoading()) {
2675
- <div class="flex justify-content-center p-2 w-full">
2676
- <i class="pi pi-spin pi-spinner"></i>
2798
+ <div class="col-span-full flex justify-center p-2">
2799
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2677
2800
  </div>
2678
2801
  }
2679
2802
  }
2680
2803
  </div>
2681
2804
 
2682
2805
  <!-- Footer -->
2683
- <ng-template pTemplate="footer">
2684
- <button
2685
- pButton
2686
- label="Cancel"
2687
- class="p-button-text"
2688
- (click)="onCancel()"
2689
- ></button>
2690
- <button
2691
- pButton
2692
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2693
- [disabled]="selectedFiles().length === 0"
2694
- (click)="onConfirm()"
2695
- ></button>
2806
+ <ng-template #footer>
2807
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2808
+ <button
2809
+ pButton
2810
+ label="Cancel"
2811
+ class="p-button-text w-full sm:w-auto"
2812
+ (click)="onCancel()"
2813
+ ></button>
2814
+ <button
2815
+ pButton
2816
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2817
+ [disabled]="selectedFiles().length === 0"
2818
+ class="w-full sm:w-auto"
2819
+ (click)="onConfirm()"
2820
+ ></button>
2821
+ </div>
2696
2822
  </ng-template>
2697
2823
  </p-dialog>
2698
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;max-height:400px;overflow-y:auto;padding:.5rem}.file-card{border:2px solid var(--surface-border);border-radius:var(--border-radius);cursor:pointer;transition:all .2s;overflow:hidden}.file-card:hover:not(.disabled){border-color:var(--primary-color)}.file-card.selected{border-color:var(--primary-color);background:var(--primary-50)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:100px;display:flex;align-items:center;justify-content:center;background:var(--surface-ground)}.preview-image{width:100%;height:100%;object-fit:cover}.preview-icon{font-size:3rem;color:var(--text-color-secondary)}.selected-overlay{position:absolute;inset:0;background:rgba(var(--primary-color-rgb),.3);display:flex;align-items:center;justify-content:center}.selected-overlay i{font-size:2rem;color:var(--primary-color);background:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"] }]
2824
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"] }]
2699
2825
  }], ctorParameters: () => [], propDecorators: { loadFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadFiles", required: true }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }, { type: i0.Output, args: ["visibleChange"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
2700
2826
 
2701
- /** Log only in dev mode */
2702
- const devLog = (message) => {
2703
- if (isDevMode())
2704
- console.log(message);
2705
- };
2706
- /**
2707
- * Permission Guard
2708
- *
2709
- * Route-level guard for permission-based access control.
2710
- * Validates permissions before allowing navigation.
2711
- *
2712
- * Features:
2713
- * - Single permission check
2714
- * - Complex ILogicNode logic trees
2715
- * - Configurable redirect URL
2716
- * - Debug logging for denied access
2717
- *
2718
- * @example
2719
- * ```typescript
2720
- * // Simple permission check
2721
- * { path: 'users', canActivate: [permissionGuard('user.view')] }
2722
- *
2723
- * // Complex logic
2724
- * { path: 'admin', canActivate: [permissionGuard({
2725
- * id: 'root',
2726
- * type: 'group',
2727
- * operator: 'AND',
2728
- * children: [
2729
- * { id: '1', type: 'action', actionId: 'admin.view' },
2730
- * { id: '2', type: 'action', actionId: 'admin.manage' }
2731
- * ]
2732
- * })] }
2733
- *
2734
- * // With custom redirect
2735
- * { path: 'users', canActivate: [permissionGuard('user.view', '/access-denied')] }
2736
- * ```
2737
- */
2738
- function permissionGuard(permission, redirectTo = '/') {
2827
+ function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
2739
2828
  return () => {
2740
2829
  const permissionValidator = inject(PermissionValidatorService);
2741
2830
  const router = inject(Router);
2742
- // Check if permissions are loaded
2743
2831
  if (!permissionValidator.isPermissionsLoaded()) {
2744
- devLog('[permissionGuard] Permissions not loaded, denying access to route');
2832
+ if (isDevMode()) {
2833
+ console.log(`[${guardName}] Permissions not loaded, denying access`);
2834
+ }
2745
2835
  return router.createUrlTree([redirectTo]);
2746
2836
  }
2747
- const userPermissions = permissionValidator.permissions();
2748
- const hasPermission = evaluatePermission(permission, userPermissions);
2749
- if (!hasPermission) {
2750
- const permissionCode = typeof permission === 'string' ? permission : 'complex-logic';
2751
- devLog(`[permissionGuard] Access denied - missing permission: ${permissionCode}`);
2837
+ if (!evaluate(permissionValidator.permissions())) {
2838
+ if (isDevMode()) {
2839
+ console.log(`[${guardName}] ${getDenialMessage()}`);
2840
+ }
2752
2841
  return router.createUrlTree([redirectTo]);
2753
2842
  }
2754
2843
  return true;
2755
2844
  };
2756
2845
  }
2757
2846
  /**
2758
- * Any Permission Guard (OR logic)
2847
+ * Permission Guard - Single permission or ILogicNode check.
2759
2848
  *
2760
- * Allows access if user has ANY of the specified permissions.
2849
+ * @example
2850
+ * ```typescript
2851
+ * { path: 'users', canActivate: [permissionGuard('user.view')] }
2852
+ * { path: 'admin', canActivate: [permissionGuard(logicNode, '/access-denied')] }
2853
+ * ```
2854
+ */
2855
+ function permissionGuard(permission, redirectTo = '/') {
2856
+ const code = typeof permission === 'string' ? permission : 'complex-logic';
2857
+ return createGuard('permissionGuard', redirectTo, (perms) => evaluatePermission(permission, perms), () => `Access denied - missing: ${code}`);
2858
+ }
2859
+ /**
2860
+ * Any Permission Guard (OR logic) - Access if user has ANY permission.
2761
2861
  *
2762
2862
  * @example
2763
2863
  * ```typescript
2764
- * // Allow if user has view OR create permission
2765
2864
  * { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
2766
2865
  * ```
2767
2866
  */
2768
2867
  function anyPermissionGuard(permissions, redirectTo = '/') {
2769
- return () => {
2770
- const permissionValidator = inject(PermissionValidatorService);
2771
- const router = inject(Router);
2772
- // Validate permissions array
2773
- if (!permissions || permissions.length === 0) {
2774
- devLog('[anyPermissionGuard] Empty permissions array provided, denying access');
2775
- return router.createUrlTree([redirectTo]);
2776
- }
2777
- // Check if permissions are loaded
2778
- if (!permissionValidator.isPermissionsLoaded()) {
2779
- devLog('[anyPermissionGuard] Permissions not loaded, denying access to route');
2780
- return router.createUrlTree([redirectTo]);
2781
- }
2782
- const userPermissions = permissionValidator.permissions();
2783
- const hasPermission = hasAnyPermission(permissions, userPermissions);
2784
- if (!hasPermission) {
2785
- devLog(`[anyPermissionGuard] Access denied - missing any of: ${permissions.join(', ')}`);
2786
- return router.createUrlTree([redirectTo]);
2787
- }
2788
- return true;
2789
- };
2868
+ if (!permissions?.length) {
2869
+ return () => {
2870
+ if (isDevMode()) {
2871
+ console.log('[anyPermissionGuard] Empty permissions array, denying');
2872
+ }
2873
+ return inject(Router).createUrlTree([redirectTo]);
2874
+ };
2875
+ }
2876
+ return createGuard('anyPermissionGuard', redirectTo, (perms) => hasAnyPermission(permissions, perms), () => `Access denied - missing any of: ${permissions.join(', ')}`);
2790
2877
  }
2791
2878
  /**
2792
- * All Permissions Guard (AND logic)
2793
- *
2794
- * Allows access only if user has ALL of the specified permissions.
2879
+ * All Permissions Guard (AND logic) - Access only if user has ALL permissions.
2795
2880
  *
2796
2881
  * @example
2797
2882
  * ```typescript
2798
- * // Allow only if user has BOTH view AND create permissions
2799
2883
  * { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
2800
2884
  * ```
2801
2885
  */
2802
2886
  function allPermissionsGuard(permissions, redirectTo = '/') {
2803
- return () => {
2804
- const permissionValidator = inject(PermissionValidatorService);
2805
- const router = inject(Router);
2806
- // Validate permissions array
2807
- if (!permissions || permissions.length === 0) {
2808
- devLog('[allPermissionsGuard] Empty permissions array provided, denying access');
2809
- return router.createUrlTree([redirectTo]);
2810
- }
2811
- // Check if permissions are loaded
2812
- if (!permissionValidator.isPermissionsLoaded()) {
2813
- devLog('[allPermissionsGuard] Permissions not loaded, denying access to route');
2814
- return router.createUrlTree([redirectTo]);
2815
- }
2816
- const userPermissions = permissionValidator.permissions();
2817
- const hasPermission = hasAllPermissions(permissions, userPermissions);
2818
- if (!hasPermission) {
2819
- devLog(`[allPermissionsGuard] Access denied - missing all of: ${permissions.join(', ')}`);
2820
- return router.createUrlTree([redirectTo]);
2887
+ if (!permissions?.length) {
2888
+ return () => {
2889
+ if (isDevMode()) {
2890
+ console.log('[allPermissionsGuard] Empty permissions array, denying');
2891
+ }
2892
+ return inject(Router).createUrlTree([redirectTo]);
2893
+ };
2894
+ }
2895
+ return createGuard('allPermissionsGuard', redirectTo, (perms) => hasAllPermissions(permissions, perms), () => `Access denied - missing all of: ${permissions.join(', ')}`);
2896
+ }
2897
+
2898
+ /**
2899
+ * Base class for form page components that handle create/edit operations.
2900
+ * Provides common functionality for loading existing items, form submission,
2901
+ * navigation, and toast notifications.
2902
+ *
2903
+ * ## Features
2904
+ * - Automatic route parameter handling (loads item when ID is present)
2905
+ * - Edit mode detection based on existing item
2906
+ * - Unified submit handler for create/update operations
2907
+ * - Cancel navigation
2908
+ * - Toast messages for success/validation errors
2909
+ *
2910
+ * ## Usage
2911
+ *
2912
+ * ```typescript
2913
+ * interface IProductFormModel {
2914
+ * name: string;
2915
+ * price: number;
2916
+ * }
2917
+ *
2918
+ * @Component({
2919
+ * selector: 'app-product-form',
2920
+ * standalone: true,
2921
+ * changeDetection: ChangeDetectionStrategy.OnPush,
2922
+ * template: `...`
2923
+ * })
2924
+ * export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
2925
+ * private readonly productService = inject(ProductApiService);
2926
+ *
2927
+ * // Form model signal (private writable, public readonly)
2928
+ * private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
2929
+ * readonly formModel = this._formModel.asReadonly();
2930
+ *
2931
+ * // Required abstract implementations
2932
+ * getFormModel(): Signal<IProductFormModel> {
2933
+ * return this.formModel;
2934
+ * }
2935
+ *
2936
+ * getResourceRoute(): string {
2937
+ * return '/products';
2938
+ * }
2939
+ *
2940
+ * getResourceName(): string {
2941
+ * return 'Product';
2942
+ * }
2943
+ *
2944
+ * isFormValid(): boolean {
2945
+ * const model = this.formModel();
2946
+ * return model.name.trim().length > 0 && model.price > 0;
2947
+ * }
2948
+ *
2949
+ * loadItem(id: string): void {
2950
+ * this.isLoading.set(true);
2951
+ * this.productService.findById(id)
2952
+ * .pipe(takeUntilDestroyed(this.destroyRef))
2953
+ * .subscribe({
2954
+ * next: (response) => {
2955
+ * if (response.success && response.data) {
2956
+ * this.existingItem.set(response.data);
2957
+ * this._formModel.set({
2958
+ * name: response.data.name,
2959
+ * price: response.data.price,
2960
+ * });
2961
+ * }
2962
+ * this.isLoading.set(false);
2963
+ * },
2964
+ * error: () => {
2965
+ * this.router.navigate([this.getResourceRoute()]);
2966
+ * this.isLoading.set(false);
2967
+ * },
2968
+ * });
2969
+ * }
2970
+ *
2971
+ * createItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
2972
+ * return this.productService.insert(model);
2973
+ * }
2974
+ *
2975
+ * updateItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
2976
+ * return this.productService.update({ id: this.existingItem()!.id, ...model });
2977
+ * }
2978
+ * }
2979
+ * ```
2980
+ *
2981
+ * @template T The entity/interface type being edited
2982
+ * @template TFormModel The form model interface
2983
+ */
2984
+ class BaseFormPage {
2985
+ router = inject(Router);
2986
+ route = inject(ActivatedRoute);
2987
+ messageService = inject(MessageService);
2988
+ destroyRef = inject(DestroyRef);
2989
+ routeParams = toSignal(this.route.paramMap);
2990
+ initialized = false;
2991
+ /** Loading state for async operations */
2992
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
2993
+ /** The existing item when in edit mode, null when creating */
2994
+ existingItem = signal(null, ...(ngDevMode ? [{ debugName: "existingItem" }] : []));
2995
+ /** Whether the form is in edit mode (has existing item) */
2996
+ isEditMode = computed(() => !!this.existingItem(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
2997
+ constructor() {
2998
+ effect(() => {
2999
+ const params = this.routeParams();
3000
+ if (!params || this.initialized)
3001
+ return;
3002
+ this.initialized = true;
3003
+ const id = params.get('id');
3004
+ if (id && id !== 'new') {
3005
+ this.loadItem(id);
3006
+ }
3007
+ });
3008
+ }
3009
+ /**
3010
+ * Handle form submission.
3011
+ * Validates the form, then calls createItem or updateItem based on mode.
3012
+ * Shows appropriate toast messages and navigates back on success.
3013
+ */
3014
+ onSubmit() {
3015
+ if (!this.isFormValid()) {
3016
+ this.showValidationError();
3017
+ return;
2821
3018
  }
2822
- return true;
2823
- };
3019
+ this.isLoading.set(true);
3020
+ const model = this.getFormModel()();
3021
+ const operation$ = this.isEditMode()
3022
+ ? this.updateItem(model)
3023
+ : this.createItem(model);
3024
+ operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3025
+ next: () => {
3026
+ const action = this.isEditMode() ? 'updated' : 'created';
3027
+ this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
3028
+ this.router.navigate([this.getResourceRoute()]);
3029
+ },
3030
+ error: () => {
3031
+ this.isLoading.set(false);
3032
+ },
3033
+ complete: () => {
3034
+ this.isLoading.set(false);
3035
+ },
3036
+ });
3037
+ }
3038
+ /**
3039
+ * Handle cancel action.
3040
+ * Navigates back to the resource list.
3041
+ */
3042
+ onCancel() {
3043
+ this.router.navigate([this.getResourceRoute()]);
3044
+ }
3045
+ /**
3046
+ * Show validation error toast.
3047
+ * Override to customize the validation error message.
3048
+ */
3049
+ showValidationError() {
3050
+ this.messageService.add({
3051
+ severity: 'error',
3052
+ summary: 'Validation Error',
3053
+ detail: 'Please fill in all required fields.',
3054
+ });
3055
+ }
3056
+ /**
3057
+ * Show success toast.
3058
+ * @param detail The success message to display
3059
+ */
3060
+ showSuccess(detail) {
3061
+ this.messageService.add({
3062
+ severity: 'success',
3063
+ summary: 'Success',
3064
+ detail,
3065
+ });
3066
+ }
3067
+ /**
3068
+ * Show error toast.
3069
+ * @param detail The error message to display
3070
+ */
3071
+ showError(detail) {
3072
+ this.messageService.add({
3073
+ severity: 'error',
3074
+ summary: 'Error',
3075
+ detail,
3076
+ });
3077
+ }
3078
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3079
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseFormPage, isStandalone: true, ngImport: i0 });
3080
+ }
3081
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, decorators: [{
3082
+ type: Directive
3083
+ }], ctorParameters: () => [] });
3084
+
3085
+ /**
3086
+ * Base List Page
3087
+ * Abstract directive providing common signals, computed values, and utilities
3088
+ * for list page components across all feature packages.
3089
+ *
3090
+ * Features:
3091
+ * - Pagination state management (pageSize, currentPage, total)
3092
+ * - Loading state
3093
+ * - CRUD navigation helpers
3094
+ * - Message display utilities
3095
+ * - Delete confirmation with API integration
3096
+ * - Company feature flag support
3097
+ *
3098
+ * Usage:
3099
+ * ```typescript
3100
+ * @Component({ ... })
3101
+ * export class UserListComponent extends BaseListPage<IUser> {
3102
+ * getResourceRoute(): string { return '/users'; }
3103
+ * getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
3104
+ * async loadData(): Promise<void> { ... }
3105
+ * }
3106
+ * ```
3107
+ */
3108
+ class BaseListPage {
3109
+ router = inject(Router);
3110
+ messageService = inject(MessageService);
3111
+ appConfig = inject(APP_CONFIG);
3112
+ confirmationService = inject(ConfirmationService);
3113
+ destroyRef = inject(DestroyRef);
3114
+ /** Items list */
3115
+ items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3116
+ /** Loading state */
3117
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3118
+ /** Total records for pagination */
3119
+ total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : []));
3120
+ /** Page size */
3121
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
3122
+ /** First record index (for p-table lazy load) */
3123
+ first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
3124
+ /** Current page (0-based for API, derived from first/pageSize) */
3125
+ currentPage = computed(() => Math.floor(this.first() / this.pageSize()), ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
3126
+ /** Show company info if company feature enabled */
3127
+ showCompanyInfo = computed(() => this.appConfig.enableCompanyFeature, ...(ngDevMode ? [{ debugName: "showCompanyInfo" }] : []));
3128
+ /**
3129
+ * Navigate to create page
3130
+ */
3131
+ onCreate() {
3132
+ this.router.navigate([this.getResourceRoute(), 'new']);
3133
+ }
3134
+ /**
3135
+ * Navigate to edit page
3136
+ * @param id The ID of the item to edit
3137
+ */
3138
+ onEdit(id) {
3139
+ this.router.navigate([this.getResourceRoute(), id]);
3140
+ }
3141
+ /**
3142
+ * Handle page change from p-table lazy load
3143
+ */
3144
+ onPageChange(event) {
3145
+ this.first.set(event.first ?? 0);
3146
+ this.pageSize.set(event.rows ?? 10);
3147
+ this.loadData();
3148
+ }
3149
+ /**
3150
+ * Get pagination params for API call
3151
+ * Returns 0-based page for backend API
3152
+ */
3153
+ getPaginationParams() {
3154
+ return {
3155
+ currentPage: this.currentPage(),
3156
+ pageSize: this.pageSize(),
3157
+ };
3158
+ }
3159
+ /**
3160
+ * Show success toast message
3161
+ */
3162
+ showSuccess(detail, summary = 'Success') {
3163
+ this.messageService.add({ severity: 'success', summary, detail });
3164
+ }
3165
+ /**
3166
+ * Show error toast message
3167
+ */
3168
+ showError(detail, summary = 'Error') {
3169
+ this.messageService.add({ severity: 'error', summary, detail });
3170
+ }
3171
+ /**
3172
+ * Show info toast message
3173
+ */
3174
+ showInfo(detail, summary = 'Info') {
3175
+ this.messageService.add({ severity: 'info', summary, detail });
3176
+ }
3177
+ /**
3178
+ * Show warning toast message
3179
+ */
3180
+ showWarn(detail, summary = 'Warning') {
3181
+ this.messageService.add({ severity: 'warn', summary, detail });
3182
+ }
3183
+ /**
3184
+ * Delete an item with confirmation dialog
3185
+ * @param item The item to delete
3186
+ * @param idGetter Function to extract ID from item
3187
+ * @param deleteApiCall Function that returns Observable for delete API call
3188
+ * @param options Optional configuration
3189
+ */
3190
+ onDelete(item, idGetter, deleteApiCall, options) {
3191
+ this.confirmationService.confirm({
3192
+ message: this.getDeleteConfirmMessage(item),
3193
+ header: options?.header ?? 'Confirm Delete',
3194
+ icon: 'pi pi-exclamation-triangle',
3195
+ acceptButtonStyleClass: 'p-button-danger',
3196
+ accept: () => {
3197
+ deleteApiCall(idGetter(item))
3198
+ .pipe(takeUntilDestroyed(this.destroyRef))
3199
+ .subscribe({
3200
+ next: () => {
3201
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3202
+ this.loadData();
3203
+ },
3204
+ error: () => {
3205
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3206
+ },
3207
+ });
3208
+ },
3209
+ });
3210
+ }
3211
+ /**
3212
+ * Delete an item with confirmation dialog using async/await
3213
+ * @param item The item to delete
3214
+ * @param idGetter Function to extract ID from item
3215
+ * @param deleteApiCall Async function for delete API call
3216
+ * @param options Optional configuration
3217
+ */
3218
+ async onDeleteAsync(item, idGetter, deleteApiCall, options) {
3219
+ this.confirmationService.confirm({
3220
+ message: this.getDeleteConfirmMessage(item),
3221
+ header: options?.header ?? 'Confirm Delete',
3222
+ icon: 'pi pi-exclamation-triangle',
3223
+ acceptButtonStyleClass: 'p-button-danger',
3224
+ accept: async () => {
3225
+ try {
3226
+ await deleteApiCall(idGetter(item));
3227
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3228
+ await this.loadData();
3229
+ }
3230
+ catch {
3231
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3232
+ }
3233
+ },
3234
+ });
3235
+ }
3236
+ /**
3237
+ * Navigate to a route
3238
+ */
3239
+ navigateTo(path) {
3240
+ this.router.navigate(path);
3241
+ }
3242
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3243
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseListPage, isStandalone: true, ngImport: i0 });
2824
3244
  }
3245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, decorators: [{
3246
+ type: Directive
3247
+ }] });
2825
3248
 
2826
- // Interfaces
3249
+ // Constants
2827
3250
 
2828
3251
  /**
2829
3252
  * Generated bundle index. Do not edit.
2830
3253
  */
2831
3254
 
2832
- export { AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, COMPANY_API_PROVIDER, ContactTypeEnum, CookieService, EditModeElementChangerDirective, FILE_TYPE_FILTERS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, USER_PERMISSION_PROVIDER, USER_PROVIDER, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, isFileTypeAllowed, permissionGuard };
3255
+ export { ACTION_PERMISSIONS, AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, BRANCH_PERMISSIONS, BaseFormControl, BaseFormPage, BaseListPage, BaseUserSelectComponent, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, COMPANY_PERMISSIONS, ContactTypeEnum, CookieService, EMAIL_CONFIG_PERMISSIONS, EMAIL_TEMPLATE_PERMISSIONS, EditModeElementChangerDirective, FILE_PERMISSIONS, FILE_TYPE_FILTERS, FOLDER_PERMISSIONS, FORM_PERMISSIONS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PERMISSIONS, PROFILE_PERMISSION_PROVIDER, PROFILE_UPLOAD_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, ROLE_ACTION_PERMISSIONS, ROLE_PERMISSIONS, STORAGE_CONFIG_PERMISSIONS, USER_ACTION_PERMISSIONS, USER_LIST_PROVIDER, USER_PERMISSIONS, USER_PERMISSION_PROVIDER, USER_PROVIDER, USER_ROLE_PERMISSIONS, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, checkScrollPagination, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, hasPermission, isFileTypeAllowed, permissionGuard, provideValueAccessor };
2833
3256
  //# sourceMappingURL=flusys-ng-shared.mjs.map