@flusys/ng-shared 1.1.1-beta → 3.0.0-rc

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,24 +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';
6
+ import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
7
+ import { of, firstValueFrom, map as map$1 } from 'rxjs';
8
8
  import { map, tap, catchError } from 'rxjs/operators';
9
- import * as i1$2 from '@angular/forms';
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
13
  import { AvatarModule } from 'primeng/avatar';
14
- import * as i1$3 from 'primeng/button';
14
+ import * as i1$2 from 'primeng/button';
15
15
  import { ButtonModule } from 'primeng/button';
16
16
  import { CardModule } from 'primeng/card';
17
- import * as i1$1 from 'primeng/checkbox';
17
+ import * as i1 from 'primeng/checkbox';
18
18
  import { CheckboxModule } from 'primeng/checkbox';
19
19
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
20
20
  import { DatePickerModule } from 'primeng/datepicker';
21
- import * as i5 from 'primeng/dialog';
21
+ import * as i4 from 'primeng/dialog';
22
22
  import { DialogModule } from 'primeng/dialog';
23
23
  import { DividerModule } from 'primeng/divider';
24
24
  import { FileUploadModule } from 'primeng/fileupload';
@@ -38,7 +38,7 @@ import * as i2$1 from 'primeng/progressbar';
38
38
  import { ProgressBarModule } from 'primeng/progressbar';
39
39
  import { RadioButtonModule } from 'primeng/radiobutton';
40
40
  import { RippleModule } from 'primeng/ripple';
41
- import * as i3 from 'primeng/select';
41
+ import * as i3$1 from 'primeng/select';
42
42
  import { SelectModule } from 'primeng/select';
43
43
  import { SelectButtonModule } from 'primeng/selectbutton';
44
44
  import { SkeletonModule } from 'primeng/skeleton';
@@ -52,12 +52,132 @@ import { ToastModule } from 'primeng/toast';
52
52
  import { ToggleSwitchModule } from 'primeng/toggleswitch';
53
53
  import { TooltipModule } from 'primeng/tooltip';
54
54
  import { TreeTableModule } from 'primeng/treetable';
55
- import { toSignal, toObservable } from '@angular/core/rxjs-interop';
56
- import * as i3$1 from 'primeng/api';
57
- import { MessageService } from 'primeng/api';
55
+ import { MessageService, ConfirmationService } from 'primeng/api';
56
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
58
57
 
59
- ;
60
- ;
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
+ };
61
181
 
62
182
  /**
63
183
  * Common file type filters
@@ -151,9 +271,7 @@ class PlatformService {
151
271
  }
152
272
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
153
273
  type: Injectable,
154
- args: [{
155
- providedIn: 'root'
156
- }]
274
+ args: [{ providedIn: 'root' }]
157
275
  }] });
158
276
 
159
277
  class CookieService {
@@ -175,96 +293,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
175
293
 
176
294
  /**
177
295
  * Service to fetch file URLs from the backend.
178
- * Uses POST /file-manager/get-files endpoint which:
179
- * - Handles presigned URLs for cloud storage (AWS S3, Azure)
180
- * - Auto-refreshes expired URLs
181
- * - Validates file access permissions
182
- * - Works with all storage providers (Local, S3, Azure, SFTP)
296
+ * Handles presigned URLs for cloud storage, auto-refresh, and caching.
183
297
  */
184
298
  class FileUrlService {
185
299
  http = inject(HttpClient);
186
300
  appConfig = inject(APP_CONFIG);
187
- /** Cache of file URLs by file ID */
188
- urlCache = signal(new Map(), ...(ngDevMode ? [{ debugName: "urlCache" }] : []));
189
- /**
190
- * Get file URL by ID from cache (reactive signal)
191
- */
301
+ _cache = signal(new Map(), ...(ngDevMode ? [{ debugName: "_cache" }] : []));
302
+ cache = this._cache.asReadonly();
303
+ /** Get file URL by ID from cache (synchronous) */
192
304
  getFileUrl(fileId) {
193
305
  if (!fileId)
194
306
  return null;
195
- return this.urlCache().get(fileId)?.url ?? null;
307
+ return this._cache().get(fileId)?.url ?? null;
196
308
  }
197
- /**
198
- * Get file URL signal (computed from cache)
199
- */
309
+ /** Get file URL as computed signal */
200
310
  fileUrlSignal(fileId) {
201
311
  return computed(() => this.getFileUrl(fileId));
202
312
  }
203
- /**
204
- * Fetch file URLs from backend and update cache.
205
- * Skips IDs already in cache to prevent duplicate calls.
206
- * Returns Observable of fetched files (including cached ones).
207
- */
313
+ /** Fetch file URLs from backend and update cache */
208
314
  fetchFileUrls(fileIds, forceRefresh = false) {
209
315
  if (!fileIds.length)
210
316
  return of([]);
211
- const cache = this.urlCache();
212
- // Filter out IDs already in cache (unless force refresh)
317
+ const cache = this._cache();
213
318
  const missingIds = forceRefresh
214
319
  ? fileIds
215
320
  : fileIds.filter((id) => !cache.has(id));
216
- // If all files are cached, return from cache
217
- if (missingIds.length === 0) {
218
- const cachedFiles = fileIds
219
- .map((id) => cache.get(id))
220
- .filter((f) => !!f);
221
- return of(cachedFiles);
321
+ if (!missingIds.length) {
322
+ return of(this.getFromCache(fileIds));
222
323
  }
223
324
  const requestDto = missingIds.map((id) => ({ id }));
224
325
  return this.http
225
326
  .post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto)
226
- .pipe(map((response) => response.data ?? []), tap((files) => {
227
- // Update cache with new files
228
- const newCache = new Map(this.urlCache());
229
- files.forEach((file) => newCache.set(file.id, file));
230
- this.urlCache.set(newCache);
231
- }), map(() => {
232
- // Return all requested files (cached + newly fetched)
233
- const allCache = this.urlCache();
234
- return fileIds
235
- .map((id) => allCache.get(id))
236
- .filter((f) => !!f);
237
- }), catchError(() => of([])));
327
+ .pipe(map((response) => response.data ?? []), tap((files) => this.addToCache(files)), map(() => this.getFromCache(fileIds)), catchError(() => of([])));
238
328
  }
239
- /**
240
- * Fetch a single file URL.
241
- * Uses cache if available to prevent duplicate calls.
242
- * Returns Observable of file info or null if not found.
243
- */
329
+ /** Fetch single file URL (delegates to fetchFileUrls) */
244
330
  fetchSingleFileUrl(fileId, forceRefresh = false) {
245
- // Return from cache immediately if available
246
- if (!forceRefresh) {
247
- const cached = this.urlCache().get(fileId);
248
- if (cached) {
249
- return of(cached);
250
- }
251
- }
252
331
  return this.fetchFileUrls([fileId], forceRefresh).pipe(map((files) => files[0] ?? null));
253
332
  }
254
- /**
255
- * Clear the URL cache.
256
- * Useful on logout or when switching contexts.
257
- */
333
+ /** Clear entire cache */
258
334
  clearCache() {
259
- this.urlCache.set(new Map());
335
+ this._cache.set(new Map());
260
336
  }
261
- /**
262
- * Remove specific file from cache.
263
- */
337
+ /** Remove specific file from cache */
264
338
  removeFromCache(fileId) {
265
- const cache = new Map(this.urlCache());
266
- cache.delete(fileId);
267
- 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);
268
359
  }
269
360
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
270
361
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, providedIn: 'root' });
@@ -275,79 +366,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
275
366
  }] });
276
367
 
277
368
  /**
278
- * Permission Validator Service
279
- *
280
- * Centralized service for permission validation across all packages.
281
- * Provides signal-based state management and reactive permission checking.
282
- *
283
- * Features:
284
- * - Signal-based permission storage
285
- * - Individual permission checks
286
- * - Permission change detection
287
- * - Reactive permission state updates
288
- *
289
- * Usage:
290
- * ```typescript
291
- * // In component
292
- * readonly permissionValidator = inject(PermissionValidatorService);
293
- *
294
- * // Set permissions (typically from auth/IAM)
295
- * this.permissionValidator.setPermissions(['user.view', 'user.create']);
296
- *
297
- * // Check individual permission
298
- * if (this.permissionValidator.hasPermission('user.view')) {
299
- * // Show UI element
300
- * }
301
- *
302
- * // Access current permissions reactively
303
- * const permissions = this.permissionValidator.permissions();
304
- * ```
305
- *
306
- * @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.
307
431
  */
308
432
  class PermissionValidatorService {
309
- // ==================== SIGNALS ====================
310
- /**
311
- * Private writable signal for permissions
312
- */
313
433
  _permissions = signal([], ...(ngDevMode ? [{ debugName: "_permissions" }] : []));
314
- /**
315
- * Readonly permission signal for external consumers
316
- */
317
434
  permissions = this._permissions.asReadonly();
318
- /**
319
- * Private writable signal for loaded state
320
- */
321
435
  _isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "_isLoaded" }] : []));
322
- /**
323
- * Set permissions (replaces existing permissions)
324
- * @param permissions - Array of permission codes
325
- */
436
+ isLoaded = this._isLoaded.asReadonly();
437
+ /** Set permissions (replaces existing) */
326
438
  setPermissions(permissions) {
327
439
  this._permissions.set(permissions);
328
440
  this._isLoaded.set(true);
329
441
  }
330
- /**
331
- * Clear all permissions
332
- */
442
+ /** Clear all permissions */
333
443
  clearPermissions() {
334
444
  this._permissions.set([]);
335
445
  this._isLoaded.set(false);
336
446
  }
337
- // ==================== PERMISSION CHECKING ====================
338
- /**
339
- * Check if user has a specific permission
340
- * @param permissionCode - Required permission code
341
- * @returns True if user has permission
342
- */
343
- hasPermission(permissionCode) {
344
- return this.permissions().includes(permissionCode);
447
+ /** Check if user has permission (supports wildcards: '*', 'module.*') */
448
+ hasPermission(code) {
449
+ return hasPermission(code, this._permissions());
345
450
  }
346
- // ==================== UTILITY METHODS ====================
347
- /**
348
- * Check if permissions are loaded
349
- * @returns True if permissions have been loaded
350
- */
451
+ /** @deprecated Use `isLoaded()` signal instead */
351
452
  isPermissionsLoaded() {
352
453
  return this._isLoaded();
353
454
  }
@@ -356,9 +457,7 @@ class PermissionValidatorService {
356
457
  }
357
458
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, decorators: [{
358
459
  type: Injectable,
359
- args: [{
360
- providedIn: 'root',
361
- }]
460
+ args: [{ providedIn: 'root' }]
362
461
  }] });
363
462
 
364
463
  class EditModeElementChangerDirective {
@@ -421,46 +520,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
421
520
  type: Directive,
422
521
  args: [{
423
522
  selector: '[appEditModeElementChanger]',
424
- standalone: true,
425
523
  }]
426
524
  }], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
427
525
 
428
- /** Evaluate permission logic (string or ILogicNode) against user permissions */
429
- function evaluatePermission(logic, permissions) {
430
- if (!logic)
431
- return false;
432
- if (typeof logic === 'string')
433
- return permissions.includes(logic);
434
- return evaluateLogicNode(logic, permissions);
435
- }
436
- /** Recursively evaluate an ILogicNode tree */
437
- function evaluateLogicNode(node, permissions) {
438
- switch (node.type) {
439
- case 'action':
440
- return node.actionId ? permissions.includes(node.actionId) : false;
441
- case 'group':
442
- if (!node.children || node.children.length === 0)
443
- return false;
444
- return node.operator === 'AND'
445
- ? node.children.every((child) => evaluateLogicNode(child, permissions))
446
- : node.children.some((child) => evaluateLogicNode(child, permissions));
447
- default:
448
- return false;
449
- }
450
- }
451
- /** Check if user has ANY of the specified permissions (OR logic) */
452
- function hasAnyPermission(permissionCodes, permissions) {
453
- if (!permissionCodes?.length)
454
- return false;
455
- return permissionCodes.some((code) => permissions.includes(code));
456
- }
457
- /** Check if user has ALL of the specified permissions (AND logic) */
458
- function hasAllPermissions(permissionCodes, permissions) {
459
- if (!permissionCodes?.length)
460
- return false;
461
- return permissionCodes.every((code) => permissions.includes(code));
462
- }
463
-
464
526
  /**
465
527
  * HasPermission Directive
466
528
  *
@@ -581,7 +643,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
581
643
  type: Directive,
582
644
  args: [{
583
645
  selector: '[hasPermission]',
584
- standalone: true,
585
646
  }]
586
647
  }], ctorParameters: () => [], propDecorators: { hasPermission: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPermission", required: false }] }] } });
587
648
 
@@ -610,7 +671,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
610
671
  '[src]': 'imageSrc()',
611
672
  '(error)': 'onError()',
612
673
  },
613
- standalone: true,
614
674
  }]
615
675
  }], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }] } });
616
676
 
@@ -648,18 +708,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
648
708
  type: Directive,
649
709
  args: [{
650
710
  selector: '[appPreventDefault]',
651
- standalone: true,
711
+ host: {
712
+ '(click)': 'onClick($event)',
713
+ '(keydown)': 'onKeydown($event)',
714
+ '(keyup)': 'onKeyup($event)',
715
+ },
652
716
  }]
653
- }], 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: [{
654
- type: HostListener,
655
- args: ['click', ['$event']]
656
- }], onKeydown: [{
657
- type: HostListener,
658
- args: ['keydown', ['$event']]
659
- }], onKeyup: [{
660
- type: HostListener,
661
- args: ['keyup', ['$event']]
662
- }] } });
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"] }] } });
663
718
 
664
719
  class AngularModule {
665
720
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -668,6 +723,7 @@ class AngularModule {
668
723
  ReactiveFormsModule,
669
724
  RouterOutlet,
670
725
  RouterLink,
726
+ RouterLinkActive,
671
727
  IsEmptyImageDirective,
672
728
  NgOptimizedImage,
673
729
  NgComponentOutlet,
@@ -676,6 +732,7 @@ class AngularModule {
676
732
  ReactiveFormsModule,
677
733
  RouterOutlet,
678
734
  RouterLink,
735
+ RouterLinkActive,
679
736
  IsEmptyImageDirective,
680
737
  NgOptimizedImage,
681
738
  NgComponentOutlet,
@@ -695,6 +752,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
695
752
  ReactiveFormsModule,
696
753
  RouterOutlet,
697
754
  RouterLink,
755
+ RouterLinkActive,
698
756
  IsEmptyImageDirective,
699
757
  NgOptimizedImage,
700
758
  NgComponentOutlet,
@@ -707,6 +765,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
707
765
  ReactiveFormsModule,
708
766
  RouterOutlet,
709
767
  RouterLink,
768
+ RouterLinkActive,
710
769
  IsEmptyImageDirective,
711
770
  NgOptimizedImage,
712
771
  NgComponentOutlet,
@@ -883,7 +942,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
883
942
  */
884
943
  class ApiResourceService {
885
944
  baseUrl;
886
- loaderService = inject(ApiLoaderService);
887
945
  injector = inject(Injector);
888
946
  http;
889
947
  moduleApiName;
@@ -906,6 +964,12 @@ class ApiResourceService {
906
964
  _listResource = null;
907
965
  /** Whether the list resource has been initialized */
908
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" }] : []));
909
973
  /** Get or create the list resource (lazy initialization) */
910
974
  get listResource() {
911
975
  if (!this._listResource) {
@@ -932,20 +996,50 @@ class ApiResourceService {
932
996
  return this.fetchAllAsync(search, filter);
933
997
  } });
934
998
  });
999
+ // Signal that resource is now initialized - triggers computed re-evaluation
1000
+ this._resourceInitSignal.set(true);
935
1001
  }
936
1002
  // ==========================================================================
937
1003
  // Computed State Accessors
938
1004
  // ==========================================================================
939
- /** Whether data is currently loading */
940
- isLoading = computed(() => this._listResource?.isLoading() ?? false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
941
- /** List data array */
942
- data = computed(() => this._listResource?.value()?.data ?? [], ...(ngDevMode ? [{ debugName: "data" }] : []));
943
- /** Total count of items */
944
- total = computed(() => this._listResource?.value()?.meta?.total ?? 0, ...(ngDevMode ? [{ debugName: "total" }] : []));
945
- /** Pagination metadata */
946
- pageInfo = computed(() => this._listResource?.value()?.meta, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
947
- /** 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
+ */
948
1041
  hasMore = computed(() => {
1042
+ this._resourceInitSignal(); // Track initialization
949
1043
  const meta = this._listResource?.value()?.meta;
950
1044
  if (!meta)
951
1045
  return false;
@@ -1120,38 +1214,6 @@ class ApiResourceService {
1120
1214
  }
1121
1215
  }
1122
1216
 
1123
- class IconComponent {
1124
- icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1125
- iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1126
- IconTypeEnum = IconTypeEnum; // Needed for template reference
1127
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1128
- 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: `
1129
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1130
- <i [ngClass]="icon()"></i>
1131
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1132
- <img [alt]="icon()" [src]="icon()" />
1133
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1134
- {{ icon() }}
1135
- }@else{ I } }
1136
- `, 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"] }] });
1137
- }
1138
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
1139
- type: Component,
1140
- args: [{
1141
- selector: 'lib-icon',
1142
- imports: [AngularModule],
1143
- template: `
1144
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1145
- <i [ngClass]="icon()"></i>
1146
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1147
- <img [alt]="icon()" [src]="icon()" />
1148
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1149
- {{ icon() }}
1150
- }@else{ I } }
1151
- `,
1152
- }]
1153
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1154
-
1155
1217
  /**
1156
1218
  * Base class for form controls that support ALL Angular form patterns:
1157
1219
  *
@@ -1275,6 +1337,77 @@ function provideValueAccessor(component) {
1275
1337
  };
1276
1338
  }
1277
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
+
1278
1411
  /**
1279
1412
  * Lazy-loading multi-select component with search, pagination, and select-all.
1280
1413
  *
@@ -1284,6 +1417,10 @@ function provideValueAccessor(component) {
1284
1417
  * - Signal forms: `[formField]="formTree.field"`
1285
1418
  */
1286
1419
  class LazyMultiSelectComponent extends BaseFormControl {
1420
+ destroyRef = inject(DestroyRef);
1421
+ onDocumentClickBound = this.handleDocumentClick.bind(this);
1422
+ // View references
1423
+ pSelectRef = viewChild.required('pSelect');
1287
1424
  // Inputs
1288
1425
  placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1289
1426
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1291,14 +1428,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
1291
1428
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1292
1429
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1293
1430
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1294
- /** Two-way bound value using model() for signal forms compatibility */
1431
+ // Two-way bound value
1295
1432
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1296
1433
  // Outputs
1297
1434
  onSearch = output();
1298
1435
  onPagination = output();
1299
- // UI signals
1436
+ // UI state
1300
1437
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1301
- /** Computed: Display text for selected values (replaces getSelectedValue method) */
1438
+ openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1439
+ // Computed values
1302
1440
  selectedValueDisplay = computed(() => {
1303
1441
  const selectedValues = this.value() ?? [];
1304
1442
  if (selectedValues.length === 0)
@@ -1307,44 +1445,59 @@ class LazyMultiSelectComponent extends BaseFormControl {
1307
1445
  return `${selectedValues.length} Items Selected`;
1308
1446
  }
1309
1447
  return this.selectDataList()
1310
- .filter(item => selectedValues.includes(item.value))
1311
- .map(item => item.label)
1448
+ .filter((item) => selectedValues.includes(item.value))
1449
+ .map((item) => item.label)
1312
1450
  .join(', ');
1313
1451
  }, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
1314
- /** Computed: Whether all items are selected (replaces isSelectAll signal) */
1315
1452
  isSelectAll = computed(() => {
1316
1453
  const selectedValues = this.value() ?? [];
1317
- const allValues = this.selectDataList().map(item => item.value);
1454
+ const allValues = this.selectDataList().map((item) => item.value);
1318
1455
  if (selectedValues.length === 0 || allValues.length === 0)
1319
1456
  return false;
1320
- return allValues.every(val => selectedValues.includes(val));
1457
+ return allValues.every((val) => selectedValues.includes(val));
1321
1458
  }, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
1322
1459
  constructor() {
1323
1460
  super();
1324
1461
  this.initializeFormControl();
1325
- // Search debounce effect
1326
- runInInjectionContext(this.injector, () => {
1327
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1((value) => {
1328
- this.onSearch.emit(value);
1329
- })), { 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);
1330
1489
  });
1331
1490
  }
1332
- onScrollBound = this.onScroll.bind(this);
1333
- multiScrollContainer = viewChild.required('multiScrollContainer');
1334
1491
  onScroll(event) {
1335
- const el = event.target;
1336
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1337
- if (nearBottom && !this.isLoading()) {
1338
- const pagination = this.pagination();
1339
- const nextPage = pagination.currentPage + 1;
1340
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1341
- if (hasMore) {
1342
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1343
- }
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);
1344
1499
  }
1345
1500
  }
1346
- pSelectRef = viewChild.required('pSelect');
1347
- openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1348
1501
  onSelectClick(event) {
1349
1502
  if (this.disabled())
1350
1503
  return;
@@ -1363,7 +1516,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1363
1516
  }
1364
1517
  }
1365
1518
  isSelected(data) {
1366
- return this.value()?.includes(data.value);
1519
+ return this.value()?.includes(data.value) ?? false;
1367
1520
  }
1368
1521
  key(option) {
1369
1522
  return option.value;
@@ -1376,8 +1529,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1376
1529
  }
1377
1530
  }
1378
1531
  else {
1379
- const updated = previousValue.filter((v) => v !== option.value);
1380
- this.value.set(updated);
1532
+ this.value.set(previousValue.filter((v) => v !== option.value));
1381
1533
  }
1382
1534
  }
1383
1535
  changeSelectAll(event) {
@@ -1393,15 +1545,12 @@ class LazyMultiSelectComponent extends BaseFormControl {
1393
1545
  this.value.set([]);
1394
1546
  }
1395
1547
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1396
- 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: "component", type: i1$1.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: 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 });
1397
1549
  }
1398
1550
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1399
1551
  type: Component,
1400
- 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"] }]
1401
- }], 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: [{
1402
- type: HostListener,
1403
- args: ['document:click', ['$event']]
1404
- }] } });
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"] }] } });
1405
1554
 
1406
1555
  /**
1407
1556
  * Lazy-loading single select component with search and pagination.
@@ -1412,6 +1561,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1412
1561
  * - Signal forms: `[formField]="formTree.field"`
1413
1562
  */
1414
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');
1415
1569
  // Inputs
1416
1570
  placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1417
1571
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -1421,77 +1575,85 @@ class LazySelectComponent extends BaseFormControl {
1421
1575
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1422
1576
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1423
1577
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1424
- /** Two-way bound value using model() for signal forms compatibility */
1578
+ // Two-way bound value
1425
1579
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1426
1580
  // Outputs
1427
1581
  onSearch = output();
1428
1582
  onPagination = output();
1429
- // UI signals
1583
+ // UI state
1430
1584
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1431
- // Effect hooks
1585
+ isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1432
1586
  constructor() {
1433
1587
  super();
1434
1588
  this.initializeFormControl();
1435
- runInInjectionContext(this.injector, () => {
1436
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1(value => {
1437
- this.onSearch.emit(value);
1438
- })), { 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
+ }
1439
1615
  });
1440
1616
  }
1441
- // Signal to toggle panel
1442
- isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1443
- scrollTargetEl = null;
1444
- onScrollBound = this.onScroll.bind(this);
1445
- scrollContainer = viewChild.required('scrollContainer');
1446
1617
  onScroll(event) {
1447
- const el = event.target;
1448
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1449
- if (nearBottom && !this.isLoading()) {
1450
- const pagination = this.pagination();
1451
- const nextPage = pagination.currentPage + 1;
1452
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1453
- if (hasMore) {
1454
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1455
- }
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);
1456
1625
  }
1457
1626
  }
1458
- // Toggle panel and manage scroll event
1459
1627
  showPanel() {
1460
1628
  if (this.disabled())
1461
1629
  return;
1462
- this.isPanelShow.update(prev => !prev);
1630
+ this.isPanelShow.update((prev) => !prev);
1463
1631
  const isNowVisible = this.isPanelShow();
1464
1632
  if (isNowVisible) {
1465
- setTimeout(() => {
1633
+ afterNextRender(() => {
1466
1634
  const containerEl = this.scrollContainer().nativeElement;
1467
1635
  const target = containerEl.querySelector('.p-select-list-container');
1468
1636
  if (target) {
1469
1637
  target.addEventListener('scroll', this.onScrollBound);
1470
1638
  this.scrollTargetEl = target;
1471
1639
  }
1472
- }, 0);
1640
+ }, { injector: this.injector });
1473
1641
  }
1474
- else {
1475
- if (this.scrollTargetEl) {
1476
- this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1477
- this.scrollTargetEl = null;
1478
- }
1642
+ else if (this.scrollTargetEl) {
1643
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1644
+ this.scrollTargetEl = null;
1479
1645
  }
1480
1646
  }
1481
1647
  onBlur() {
1482
1648
  this.markAsTouched();
1483
1649
  }
1484
1650
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1485
- 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: i2.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 });
1486
1652
  }
1487
1653
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
1488
1654
  type: Component,
1489
- args: [{ selector: 'lib-lazy-select', imports: [
1490
- AngularModule,
1491
- PrimeModule,
1492
- EditModeElementChangerDirective
1493
- ], 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" }]
1494
- }], 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"] }] } });
1495
1657
 
1496
1658
  /**
1497
1659
  * Injection Tokens for Provider Interfaces
@@ -1589,56 +1751,33 @@ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1589
1751
  */
1590
1752
  const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
1591
1753
 
1592
- const DEFAULT_PAGE_SIZE$2 = 20;
1754
+ const DEFAULT_PAGE_SIZE$1 = 20;
1593
1755
  /**
1594
- * User Select Component - Single user selection with lazy loading.
1756
+ * Base class for user selection components.
1757
+ * Provides shared user loading, pagination, and search functionality.
1595
1758
  *
1596
- * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1597
- *
1598
- * Features:
1599
- * - Search with debouncing (handled by lazy-select)
1600
- * - Infinite scroll pagination
1601
- * - Filter active users by default (configurable)
1602
- * - Supports additional filters via `additionalFilters` input
1603
- *
1604
- * @example
1605
- * ```html
1606
- * <!-- Simple usage - uses USER_PROVIDER internally -->
1607
- * <lib-user-select
1608
- * [(value)]="selectedUserId"
1609
- * [isEditMode]="true"
1610
- * />
1611
- *
1612
- * <!-- With custom loadUsers function -->
1613
- * <lib-user-select
1614
- * [(value)]="selectedUserId"
1615
- * [isEditMode]="true"
1616
- * [loadUsers]="customLoadUsers"
1617
- * />
1618
- * ```
1759
+ * Subclasses must implement:
1760
+ * - `setupValueEffect()` to track value changes and emit selection events
1619
1761
  */
1620
- class UserSelectComponent {
1762
+ class BaseUserSelectComponent {
1621
1763
  destroyRef = inject(DestroyRef);
1764
+ injector = inject(Injector);
1622
1765
  userProvider = inject(USER_PROVIDER);
1623
1766
  abortController = null;
1624
- // Optional: custom function to load users (uses USER_PROVIDER if not provided)
1625
- loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1626
1767
  // Inputs
1768
+ loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1627
1769
  placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1628
1770
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1629
1771
  filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1630
1772
  additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
1631
- pageSize = input(DEFAULT_PAGE_SIZE$2, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1632
- // Two-way bound value
1633
- value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1773
+ pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1634
1774
  // Outputs
1635
- userSelected = output();
1636
1775
  onError = output();
1637
1776
  // Internal state
1638
1777
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1639
1778
  users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
1640
1779
  total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
1641
- 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" }] : []));
1642
1781
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1643
1782
  // Computed dropdown data
1644
1783
  dropdownUsers = computed(() => this.users().map((user) => ({
@@ -1661,20 +1800,8 @@ class UserSelectComponent {
1661
1800
  afterNextRender(() => {
1662
1801
  this.fetchUsers();
1663
1802
  });
1664
- // Emit selected user when value changes
1665
- effect(() => {
1666
- const selectedId = this.value();
1667
- const users = this.users();
1668
- untracked(() => {
1669
- if (selectedId) {
1670
- const user = users.find((u) => u.id === selectedId);
1671
- this.userSelected.emit(user ?? null);
1672
- }
1673
- else {
1674
- this.userSelected.emit(null);
1675
- }
1676
- });
1677
- });
1803
+ // Setup value change tracking (implemented by subclass)
1804
+ this.setupValueEffect();
1678
1805
  }
1679
1806
  handleSearch(search) {
1680
1807
  this.searchTerm.set(search);
@@ -1686,6 +1813,12 @@ class UserSelectComponent {
1686
1813
  this.pagination.set(pagination);
1687
1814
  this.fetchUsers(true);
1688
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
+ }
1689
1822
  async fetchUsers(append = false) {
1690
1823
  if (this.isLoading())
1691
1824
  return;
@@ -1743,14 +1876,63 @@ class UserSelectComponent {
1743
1876
  })),
1744
1877
  })));
1745
1878
  }
1746
- /** Reload users (useful when filters change externally) */
1747
- reload() {
1748
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1749
- this.users.set([]);
1750
- 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
+ });
1751
1933
  }
1752
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1753
- 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: `
1754
1936
  <lib-lazy-select
1755
1937
  [(value)]="value"
1756
1938
  [placeHolder]="placeHolder()"
@@ -1770,7 +1952,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1770
1952
  type: Component,
1771
1953
  args: [{
1772
1954
  selector: 'lib-user-select',
1773
- standalone: true,
1774
1955
  imports: [AngularModule, PrimeModule, LazySelectComponent],
1775
1956
  template: `
1776
1957
  <lib-lazy-select
@@ -1789,9 +1970,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1789
1970
  `,
1790
1971
  changeDetection: ChangeDetectionStrategy.OnPush,
1791
1972
  }]
1792
- }], 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"] }] } });
1793
1974
 
1794
- const DEFAULT_PAGE_SIZE$1 = 20;
1795
1975
  /**
1796
1976
  * User Multi-Select Component - Multiple user selection with lazy loading.
1797
1977
  *
@@ -1820,50 +2000,12 @@ const DEFAULT_PAGE_SIZE$1 = 20;
1820
2000
  * />
1821
2001
  * ```
1822
2002
  */
1823
- class UserMultiSelectComponent {
1824
- destroyRef = inject(DestroyRef);
1825
- userProvider = inject(USER_PROVIDER);
1826
- abortController = null;
1827
- // Optional: custom function to load users (uses USER_PROVIDER if not provided)
1828
- loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1829
- // Inputs
1830
- placeHolder = input('Select Users', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1831
- isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1832
- filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1833
- additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
1834
- pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
2003
+ class UserMultiSelectComponent extends BaseUserSelectComponent {
1835
2004
  // Two-way bound value
1836
2005
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1837
2006
  // Outputs
1838
2007
  usersSelected = output();
1839
- onError = output();
1840
- // Internal state
1841
- isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1842
- users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
1843
- total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
1844
- pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1845
- searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1846
- // Computed dropdown data
1847
- dropdownUsers = computed(() => this.users().map((user) => ({
1848
- label: user.name || user.email,
1849
- value: user.id,
1850
- })), ...(ngDevMode ? [{ debugName: "dropdownUsers" }] : []));
1851
- constructor() {
1852
- // Cleanup on destroy
1853
- this.destroyRef.onDestroy(() => {
1854
- this.abortController?.abort();
1855
- });
1856
- // Update page size from input
1857
- effect(() => {
1858
- const size = this.pageSize();
1859
- untracked(() => {
1860
- this.pagination.update((p) => ({ ...p, pageSize: size }));
1861
- });
1862
- });
1863
- // Load initial users after render
1864
- afterNextRender(() => {
1865
- this.fetchUsers();
1866
- });
2008
+ setupValueEffect() {
1867
2009
  // Emit selected users when value changes
1868
2010
  effect(() => {
1869
2011
  const selectedIds = this.value() ?? [];
@@ -1874,81 +2016,8 @@ class UserMultiSelectComponent {
1874
2016
  });
1875
2017
  });
1876
2018
  }
1877
- handleSearch(search) {
1878
- this.searchTerm.set(search);
1879
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1880
- this.users.set([]);
1881
- this.fetchUsers();
1882
- }
1883
- handlePagination(pagination) {
1884
- this.pagination.set(pagination);
1885
- this.fetchUsers(true);
1886
- }
1887
- async fetchUsers(append = false) {
1888
- if (this.isLoading())
1889
- return;
1890
- // Cancel previous request
1891
- this.abortController?.abort();
1892
- this.abortController = new AbortController();
1893
- this.isLoading.set(true);
1894
- try {
1895
- const pag = this.pagination();
1896
- const filter = {
1897
- page: pag.currentPage,
1898
- pageSize: pag.pageSize,
1899
- search: this.searchTerm(),
1900
- ...this.additionalFilters(),
1901
- };
1902
- // Use custom loadUsers if provided, otherwise use USER_PROVIDER
1903
- const customLoadUsers = this.loadUsers();
1904
- const response = await firstValueFrom(customLoadUsers
1905
- ? customLoadUsers(filter)
1906
- : this.loadUsersFromProvider(filter));
1907
- if (response.success && response.data) {
1908
- if (append) {
1909
- this.users.update((current) => [...current, ...response.data]);
1910
- }
1911
- else {
1912
- this.users.set(response.data);
1913
- }
1914
- this.total.set(response.meta?.total);
1915
- }
1916
- }
1917
- catch (error) {
1918
- if (error.name !== 'AbortError') {
1919
- this.onError.emit(error);
1920
- }
1921
- }
1922
- finally {
1923
- this.isLoading.set(false);
1924
- }
1925
- }
1926
- /** Load users from USER_PROVIDER with active filter */
1927
- loadUsersFromProvider(filter) {
1928
- return this.userProvider
1929
- .getUsers({
1930
- page: filter.page,
1931
- pageSize: filter.pageSize,
1932
- search: filter.search,
1933
- isActive: this.filterActive() ? true : undefined,
1934
- })
1935
- .pipe(map$1((res) => ({
1936
- ...res,
1937
- data: res.data?.map((u) => ({
1938
- id: u.id,
1939
- name: u.name,
1940
- email: u.email,
1941
- })),
1942
- })));
1943
- }
1944
- /** Reload users (useful when filters change externally) */
1945
- reload() {
1946
- this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1947
- this.users.set([]);
1948
- this.fetchUsers();
1949
- }
1950
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1951
- 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: `
1952
2021
  <lib-lazy-multi-select
1953
2022
  [(value)]="value"
1954
2023
  [placeHolder]="placeHolder()"
@@ -1966,7 +2035,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1966
2035
  type: Component,
1967
2036
  args: [{
1968
2037
  selector: 'lib-user-multi-select',
1969
- standalone: true,
1970
2038
  imports: [AngularModule, PrimeModule, LazyMultiSelectComponent],
1971
2039
  template: `
1972
2040
  <lib-lazy-multi-select
@@ -1983,47 +2051,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1983
2051
  `,
1984
2052
  changeDetection: ChangeDetectionStrategy.OnPush,
1985
2053
  }]
1986
- }], 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"] }] } });
1987
2055
 
1988
2056
  /**
1989
2057
  * File Uploader Component - Drag & drop file upload with type filtering.
1990
2058
  *
1991
2059
  * Pass your own `uploadFile` function - works with any storage API.
1992
- *
1993
- * Features:
1994
- * - Drag & drop support
1995
- * - File type filtering (images, documents, etc.)
1996
- * - Upload progress indication
1997
- * - Multiple file support (optional)
1998
- * - Image compression options
1999
- *
2000
- * @example
2001
- * ```typescript
2002
- * // In component
2003
- * readonly uploadService = inject(UploadService);
2004
- *
2005
- * readonly uploadFile: UploadFileFn = (file, options) =>
2006
- * this.uploadService.uploadSingleFile(file, options);
2007
- * ```
2008
- *
2009
- * ```html
2010
- * <!-- Single image upload -->
2011
- * <lib-file-uploader
2012
- * [uploadFile]="uploadFile"
2013
- * [acceptTypes]="['image/*']"
2014
- * [multiple]="false"
2015
- * (fileUploaded)="onFileUploaded($event)"
2016
- * />
2017
- *
2018
- * <!-- Multiple document upload -->
2019
- * <lib-file-uploader
2020
- * [uploadFile]="uploadFile"
2021
- * [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
2022
- * [multiple]="true"
2023
- * [maxFiles]="5"
2024
- * (filesUploaded)="onFilesUploaded($event)"
2025
- * />
2026
- * ```
2027
2060
  */
2028
2061
  class FileUploaderComponent {
2029
2062
  messageService = inject(MessageService);
@@ -2169,11 +2202,7 @@ class FileUploaderComponent {
2169
2202
  });
2170
2203
  }
2171
2204
  catch (error) {
2172
- this.messageService.add({
2173
- severity: 'error',
2174
- summary: 'Upload Failed',
2175
- detail: error.message || 'Failed to upload file',
2176
- });
2205
+ // Error toast handled by global interceptor
2177
2206
  this.onError.emit(error);
2178
2207
  }
2179
2208
  finally {
@@ -2195,37 +2224,41 @@ class FileUploaderComponent {
2195
2224
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2196
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: `
2197
2226
  <div
2198
- class="file-uploader"
2199
- [class.drag-over]="isDragOver()"
2200
- [class.disabled]="disabled()"
2227
+ class="w-full"
2228
+ [class.opacity-60]="disabled()"
2201
2229
  (dragover)="onDragOver($event)"
2202
2230
  (dragleave)="onDragLeave($event)"
2203
2231
  (drop)="onDrop($event)"
2204
2232
  >
2205
- <!-- Upload Area -->
2206
- <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
+ >
2207
2240
  @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>
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>
2211
2244
  @if (uploadProgress() > 0) {
2212
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2245
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2213
2246
  }
2214
2247
  </div>
2215
2248
  } @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">
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">
2219
2252
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2220
2253
  </p>
2221
- <p class="text-sm text-color-secondary">
2254
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2222
2255
  @if (acceptTypesDisplay()) {
2223
2256
  Allowed: {{ acceptTypesDisplay() }}
2224
2257
  } @else {
2225
2258
  All file types allowed
2226
2259
  }
2227
2260
  @if (maxSizeMb()) {
2228
- (Max {{ maxSizeMb() }}MB)
2261
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2229
2262
  }
2230
2263
  </p>
2231
2264
  </div>
@@ -2236,71 +2269,76 @@ class FileUploaderComponent {
2236
2269
  <input
2237
2270
  #fileInput
2238
2271
  type="file"
2272
+ class="hidden"
2239
2273
  [accept]="acceptString()"
2240
2274
  [multiple]="multiple()"
2241
2275
  [disabled]="disabled() || isUploading()"
2242
2276
  (change)="onFileSelected($event)"
2243
- class="hidden"
2244
2277
  />
2245
2278
 
2246
- <!-- Selected Files Preview -->
2279
+ <!-- Selected Files Preview - Responsive layout -->
2247
2280
  @if (selectedFiles().length > 0 && showPreview()) {
2248
- <div class="selected-files mt-3">
2281
+ <div class="mt-3 space-y-2">
2249
2282
  @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"
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
2257
2288
  icon="pi pi-times"
2258
- class="p-button-text p-button-rounded p-button-sm"
2259
- (click)="removeFile(file)"
2289
+ [text]="true"
2290
+ [rounded]="true"
2291
+ size="small"
2292
+ severity="secondary"
2260
2293
  [disabled]="isUploading()"
2261
- ></button>
2294
+ (onClick)="removeFile(file)"
2295
+ />
2262
2296
  </div>
2263
2297
  }
2264
2298
  </div>
2265
2299
  }
2266
2300
  </div>
2267
- `, 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 });
2268
2302
  }
2269
2303
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
2270
2304
  type: Component,
2271
- args: [{ selector: 'lib-file-uploader', standalone: true, imports: [AngularModule, PrimeModule], template: `
2305
+ args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
2272
2306
  <div
2273
- class="file-uploader"
2274
- [class.drag-over]="isDragOver()"
2275
- [class.disabled]="disabled()"
2307
+ class="w-full"
2308
+ [class.opacity-60]="disabled()"
2276
2309
  (dragover)="onDragOver($event)"
2277
2310
  (dragleave)="onDragLeave($event)"
2278
2311
  (drop)="onDrop($event)"
2279
2312
  >
2280
- <!-- Upload Area -->
2281
- <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
+ >
2282
2320
  @if (isUploading()) {
2283
- <div class="uploading-state">
2284
- <i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
2285
- <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>
2286
2324
  @if (uploadProgress() > 0) {
2287
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2325
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2288
2326
  }
2289
2327
  </div>
2290
2328
  } @else {
2291
- <div class="idle-state text-center">
2292
- <i class="pi pi-cloud-upload text-4xl text-primary"></i>
2293
- <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">
2294
2332
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2295
2333
  </p>
2296
- <p class="text-sm text-color-secondary">
2334
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2297
2335
  @if (acceptTypesDisplay()) {
2298
2336
  Allowed: {{ acceptTypesDisplay() }}
2299
2337
  } @else {
2300
2338
  All file types allowed
2301
2339
  }
2302
2340
  @if (maxSizeMb()) {
2303
- (Max {{ maxSizeMb() }}MB)
2341
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2304
2342
  }
2305
2343
  </p>
2306
2344
  </div>
@@ -2311,35 +2349,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2311
2349
  <input
2312
2350
  #fileInput
2313
2351
  type="file"
2352
+ class="hidden"
2314
2353
  [accept]="acceptString()"
2315
2354
  [multiple]="multiple()"
2316
2355
  [disabled]="disabled() || isUploading()"
2317
2356
  (change)="onFileSelected($event)"
2318
- class="hidden"
2319
2357
  />
2320
2358
 
2321
- <!-- Selected Files Preview -->
2359
+ <!-- Selected Files Preview - Responsive layout -->
2322
2360
  @if (selectedFiles().length > 0 && showPreview()) {
2323
- <div class="selected-files mt-3">
2361
+ <div class="mt-3 space-y-2">
2324
2362
  @for (file of selectedFiles(); track file.name) {
2325
- <div class="file-item flex align-items-center gap-2 p-2 border-round surface-border border-1 mb-2">
2326
- <i [class]="getFileIcon(file)"></i>
2327
- <span class="flex-1 text-overflow-ellipsis overflow-hidden">{{ file.name }}</span>
2328
- <span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
2329
- <button
2330
- pButton
2331
- 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
2332
2368
  icon="pi pi-times"
2333
- class="p-button-text p-button-rounded p-button-sm"
2334
- (click)="removeFile(file)"
2369
+ [text]="true"
2370
+ [rounded]="true"
2371
+ size="small"
2372
+ severity="secondary"
2335
2373
  [disabled]="isUploading()"
2336
- ></button>
2374
+ (onClick)="removeFile(file)"
2375
+ />
2337
2376
  </div>
2338
2377
  }
2339
2378
  </div>
2340
2379
  }
2341
2380
  </div>
2342
- `, 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"] }]
2343
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"] }] } });
2344
2383
 
2345
2384
  const DEFAULT_PAGE_SIZE = 20;
@@ -2454,16 +2493,14 @@ class FileSelectorDialogComponent {
2454
2493
  }, 500);
2455
2494
  }
2456
2495
  onScroll(event) {
2457
- const el = event.target;
2458
- const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
2459
- if (nearBottom && !this.isLoading()) {
2460
- const pag = this.pagination();
2461
- const nextPage = pag.currentPage + 1;
2462
- const hasMore = nextPage * pag.pageSize < (this.total() ?? 0);
2463
- if (hasMore) {
2464
- this.pagination.update((p) => ({ ...p, currentPage: nextPage }));
2465
- this.fetchFiles(true);
2466
- }
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);
2467
2504
  }
2468
2505
  }
2469
2506
  toggleSelection(file) {
@@ -2574,11 +2611,13 @@ class FileSelectorDialogComponent {
2574
2611
  [closable]="true"
2575
2612
  [draggable]="false"
2576
2613
  [resizable]="false"
2577
- [style]="{ width: '800px', maxHeight: '90vh' }"
2614
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2615
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2616
+ styleClass="file-selector-dialog"
2578
2617
  (onHide)="onDialogHide()"
2579
2618
  >
2580
2619
  <!-- Search Bar -->
2581
- <div class="flex gap-2 mb-3">
2620
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2582
2621
  <span class="p-input-icon-left flex-1">
2583
2622
  <i class="pi pi-search"></i>
2584
2623
  <input
@@ -2591,25 +2630,25 @@ class FileSelectorDialogComponent {
2591
2630
  />
2592
2631
  </span>
2593
2632
  @if (multiple()) {
2594
- <span class="text-sm text-color-secondary align-self-center">
2633
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2595
2634
  {{ selectedFiles().length }} selected
2596
2635
  </span>
2597
2636
  }
2598
2637
  </div>
2599
2638
 
2600
- <!-- File Grid -->
2639
+ <!-- File Grid - Responsive columns -->
2601
2640
  <div
2602
2641
  class="file-grid"
2603
2642
  #scrollContainer
2604
2643
  (scroll)="onScroll($event)"
2605
2644
  >
2606
2645
  @if (isLoading() && files().length === 0) {
2607
- <div class="flex justify-content-center p-4">
2608
- <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>
2609
2648
  </div>
2610
2649
  } @else if (files().length === 0) {
2611
- <div class="text-center p-4 text-color-secondary">
2612
- <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>
2613
2652
  <p>No files found</p>
2614
2653
  </div>
2615
2654
  } @else {
@@ -2623,54 +2662,59 @@ class FileSelectorDialogComponent {
2623
2662
  <!-- File Preview -->
2624
2663
  <div class="file-preview">
2625
2664
  @if (isImage(file) && file.url) {
2626
- <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" />
2627
2666
  } @else {
2628
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2667
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2629
2668
  }
2630
2669
  @if (isSelected(file)) {
2631
2670
  <div class="selected-overlay">
2632
- <i class="pi pi-check"></i>
2671
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2633
2672
  </div>
2634
2673
  }
2635
2674
  </div>
2636
2675
 
2637
2676
  <!-- File Info -->
2638
- <div class="file-info">
2639
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2640
- <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>
2641
2682
  </div>
2642
2683
  </div>
2643
2684
  }
2644
2685
 
2645
2686
  @if (isLoading()) {
2646
- <div class="flex justify-content-center p-2 w-full">
2647
- <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>
2648
2689
  </div>
2649
2690
  }
2650
2691
  }
2651
2692
  </div>
2652
2693
 
2653
2694
  <!-- Footer -->
2654
- <ng-template pTemplate="footer">
2655
- <button
2656
- pButton
2657
- label="Cancel"
2658
- class="p-button-text"
2659
- (click)="onCancel()"
2660
- ></button>
2661
- <button
2662
- pButton
2663
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2664
- [disabled]="selectedFiles().length === 0"
2665
- (click)="onConfirm()"
2666
- ></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>
2667
2711
  </ng-template>
2668
2712
  </p-dialog>
2669
- `, 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:var(--p-surface-0);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: i3$1.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: i5.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 });
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 });
2670
2714
  }
2671
2715
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2672
2716
  type: Component,
2673
- args: [{ selector: 'lib-file-selector-dialog', standalone: true, imports: [AngularModule, PrimeModule], template: `
2717
+ args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
2674
2718
  <p-dialog
2675
2719
  [header]="header()"
2676
2720
  [(visible)]="visible"
@@ -2678,11 +2722,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2678
2722
  [closable]="true"
2679
2723
  [draggable]="false"
2680
2724
  [resizable]="false"
2681
- [style]="{ width: '800px', maxHeight: '90vh' }"
2725
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2726
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2727
+ styleClass="file-selector-dialog"
2682
2728
  (onHide)="onDialogHide()"
2683
2729
  >
2684
2730
  <!-- Search Bar -->
2685
- <div class="flex gap-2 mb-3">
2731
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2686
2732
  <span class="p-input-icon-left flex-1">
2687
2733
  <i class="pi pi-search"></i>
2688
2734
  <input
@@ -2695,25 +2741,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2695
2741
  />
2696
2742
  </span>
2697
2743
  @if (multiple()) {
2698
- <span class="text-sm text-color-secondary align-self-center">
2744
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2699
2745
  {{ selectedFiles().length }} selected
2700
2746
  </span>
2701
2747
  }
2702
2748
  </div>
2703
2749
 
2704
- <!-- File Grid -->
2750
+ <!-- File Grid - Responsive columns -->
2705
2751
  <div
2706
2752
  class="file-grid"
2707
2753
  #scrollContainer
2708
2754
  (scroll)="onScroll($event)"
2709
2755
  >
2710
2756
  @if (isLoading() && files().length === 0) {
2711
- <div class="flex justify-content-center p-4">
2712
- <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>
2713
2759
  </div>
2714
2760
  } @else if (files().length === 0) {
2715
- <div class="text-center p-4 text-color-secondary">
2716
- <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>
2717
2763
  <p>No files found</p>
2718
2764
  </div>
2719
2765
  } @else {
@@ -2727,182 +2773,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2727
2773
  <!-- File Preview -->
2728
2774
  <div class="file-preview">
2729
2775
  @if (isImage(file) && file.url) {
2730
- <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" />
2731
2777
  } @else {
2732
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2778
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2733
2779
  }
2734
2780
  @if (isSelected(file)) {
2735
2781
  <div class="selected-overlay">
2736
- <i class="pi pi-check"></i>
2782
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2737
2783
  </div>
2738
2784
  }
2739
2785
  </div>
2740
2786
 
2741
2787
  <!-- File Info -->
2742
- <div class="file-info">
2743
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2744
- <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>
2745
2793
  </div>
2746
2794
  </div>
2747
2795
  }
2748
2796
 
2749
2797
  @if (isLoading()) {
2750
- <div class="flex justify-content-center p-2 w-full">
2751
- <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>
2752
2800
  </div>
2753
2801
  }
2754
2802
  }
2755
2803
  </div>
2756
2804
 
2757
2805
  <!-- Footer -->
2758
- <ng-template pTemplate="footer">
2759
- <button
2760
- pButton
2761
- label="Cancel"
2762
- class="p-button-text"
2763
- (click)="onCancel()"
2764
- ></button>
2765
- <button
2766
- pButton
2767
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2768
- [disabled]="selectedFiles().length === 0"
2769
- (click)="onConfirm()"
2770
- ></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>
2771
2822
  </ng-template>
2772
2823
  </p-dialog>
2773
- `, 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:var(--p-surface-0);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"] }]
2774
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"] }] } });
2775
2826
 
2776
- /** Log only in dev mode */
2777
- const devLog = (message) => {
2778
- if (isDevMode())
2779
- console.log(message);
2780
- };
2781
- /**
2782
- * Permission Guard
2783
- *
2784
- * Route-level guard for permission-based access control.
2785
- * Validates permissions before allowing navigation.
2786
- *
2787
- * Features:
2788
- * - Single permission check
2789
- * - Complex ILogicNode logic trees
2790
- * - Configurable redirect URL
2791
- * - Debug logging for denied access
2792
- *
2793
- * @example
2794
- * ```typescript
2795
- * // Simple permission check
2796
- * { path: 'users', canActivate: [permissionGuard('user.view')] }
2797
- *
2798
- * // Complex logic
2799
- * { path: 'admin', canActivate: [permissionGuard({
2800
- * id: 'root',
2801
- * type: 'group',
2802
- * operator: 'AND',
2803
- * children: [
2804
- * { id: '1', type: 'action', actionId: 'admin.view' },
2805
- * { id: '2', type: 'action', actionId: 'admin.manage' }
2806
- * ]
2807
- * })] }
2808
- *
2809
- * // With custom redirect
2810
- * { path: 'users', canActivate: [permissionGuard('user.view', '/access-denied')] }
2811
- * ```
2812
- */
2813
- function permissionGuard(permission, redirectTo = '/') {
2827
+ function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
2814
2828
  return () => {
2815
2829
  const permissionValidator = inject(PermissionValidatorService);
2816
2830
  const router = inject(Router);
2817
- // Check if permissions are loaded
2818
2831
  if (!permissionValidator.isPermissionsLoaded()) {
2819
- devLog('[permissionGuard] Permissions not loaded, denying access to route');
2832
+ if (isDevMode()) {
2833
+ console.log(`[${guardName}] Permissions not loaded, denying access`);
2834
+ }
2820
2835
  return router.createUrlTree([redirectTo]);
2821
2836
  }
2822
- const userPermissions = permissionValidator.permissions();
2823
- const hasPermission = evaluatePermission(permission, userPermissions);
2824
- if (!hasPermission) {
2825
- const permissionCode = typeof permission === 'string' ? permission : 'complex-logic';
2826
- devLog(`[permissionGuard] Access denied - missing permission: ${permissionCode}`);
2837
+ if (!evaluate(permissionValidator.permissions())) {
2838
+ if (isDevMode()) {
2839
+ console.log(`[${guardName}] ${getDenialMessage()}`);
2840
+ }
2827
2841
  return router.createUrlTree([redirectTo]);
2828
2842
  }
2829
2843
  return true;
2830
2844
  };
2831
2845
  }
2832
2846
  /**
2833
- * Any Permission Guard (OR logic)
2847
+ * Permission Guard - Single permission or ILogicNode check.
2834
2848
  *
2835
- * 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.
2836
2861
  *
2837
2862
  * @example
2838
2863
  * ```typescript
2839
- * // Allow if user has view OR create permission
2840
2864
  * { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
2841
2865
  * ```
2842
2866
  */
2843
2867
  function anyPermissionGuard(permissions, redirectTo = '/') {
2844
- return () => {
2845
- const permissionValidator = inject(PermissionValidatorService);
2846
- const router = inject(Router);
2847
- // Validate permissions array
2848
- if (!permissions || permissions.length === 0) {
2849
- devLog('[anyPermissionGuard] Empty permissions array provided, denying access');
2850
- return router.createUrlTree([redirectTo]);
2851
- }
2852
- // Check if permissions are loaded
2853
- if (!permissionValidator.isPermissionsLoaded()) {
2854
- devLog('[anyPermissionGuard] Permissions not loaded, denying access to route');
2855
- return router.createUrlTree([redirectTo]);
2856
- }
2857
- const userPermissions = permissionValidator.permissions();
2858
- const hasPermission = hasAnyPermission(permissions, userPermissions);
2859
- if (!hasPermission) {
2860
- devLog(`[anyPermissionGuard] Access denied - missing any of: ${permissions.join(', ')}`);
2861
- return router.createUrlTree([redirectTo]);
2862
- }
2863
- return true;
2864
- };
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(', ')}`);
2865
2877
  }
2866
2878
  /**
2867
- * All Permissions Guard (AND logic)
2868
- *
2869
- * Allows access only if user has ALL of the specified permissions.
2879
+ * All Permissions Guard (AND logic) - Access only if user has ALL permissions.
2870
2880
  *
2871
2881
  * @example
2872
2882
  * ```typescript
2873
- * // Allow only if user has BOTH view AND create permissions
2874
2883
  * { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
2875
2884
  * ```
2876
2885
  */
2877
2886
  function allPermissionsGuard(permissions, redirectTo = '/') {
2878
- return () => {
2879
- const permissionValidator = inject(PermissionValidatorService);
2880
- const router = inject(Router);
2881
- // Validate permissions array
2882
- if (!permissions || permissions.length === 0) {
2883
- devLog('[allPermissionsGuard] Empty permissions array provided, denying access');
2884
- return router.createUrlTree([redirectTo]);
2885
- }
2886
- // Check if permissions are loaded
2887
- if (!permissionValidator.isPermissionsLoaded()) {
2888
- devLog('[allPermissionsGuard] Permissions not loaded, denying access to route');
2889
- return router.createUrlTree([redirectTo]);
2890
- }
2891
- const userPermissions = permissionValidator.permissions();
2892
- const hasPermission = hasAllPermissions(permissions, userPermissions);
2893
- if (!hasPermission) {
2894
- devLog(`[allPermissionsGuard] Access denied - missing all of: ${permissions.join(', ')}`);
2895
- 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;
2896
3018
  }
2897
- return true;
2898
- };
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 });
2899
3244
  }
3245
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, decorators: [{
3246
+ type: Directive
3247
+ }] });
2900
3248
 
2901
- // Interfaces
3249
+ // Constants
2902
3250
 
2903
3251
  /**
2904
3252
  * Generated bundle index. Do not edit.
2905
3253
  */
2906
3254
 
2907
- 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, PROFILE_PERMISSION_PROVIDER, PROFILE_UPLOAD_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, USER_LIST_PROVIDER, 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 };
2908
3256
  //# sourceMappingURL=flusys-ng-shared.mjs.map