@flusys/ng-shared 1.1.0-beta → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
@@ -146,14 +266,12 @@ class PlatformService {
146
266
  get isServer() {
147
267
  return isPlatformServer(this.platformId);
148
268
  }
149
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
150
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, providedIn: 'root' });
269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PlatformService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
270
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PlatformService, providedIn: 'root' });
151
271
  }
152
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", 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 {
@@ -163,10 +281,10 @@ class CookieService {
163
281
  get() {
164
282
  return !this.platformService.isServer ? this.doc.cookie : this.request?.headers.get('cookie') ?? "";
165
283
  }
166
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CookieService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
167
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CookieService, providedIn: 'root' });
284
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CookieService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
285
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CookieService, providedIn: 'root' });
168
286
  }
169
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CookieService, decorators: [{
287
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: CookieService, decorators: [{
170
288
  type: Injectable,
171
289
  args: [{
172
290
  providedIn: 'root',
@@ -175,190 +293,171 @@ 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
+ });
268
344
  }
269
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
270
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, providedIn: 'root' });
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);
359
+ }
360
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUrlService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
361
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUrlService, providedIn: 'root' });
271
362
  }
272
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUrlService, decorators: [{
363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUrlService, decorators: [{
273
364
  type: Injectable,
274
365
  args: [{ providedIn: 'root' }]
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
  }
354
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
355
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, providedIn: 'root' });
455
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PermissionValidatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
456
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PermissionValidatorService, providedIn: 'root' });
356
457
  }
357
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PermissionValidatorService, decorators: [{
458
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", 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 {
@@ -414,53 +513,16 @@ class EditModeElementChangerDirective {
414
513
  }
415
514
  }
416
515
  }
417
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EditModeElementChangerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
418
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: EditModeElementChangerDirective, isStandalone: true, selector: "[appEditModeElementChanger]", inputs: { isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: EditModeElementChangerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
517
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", type: EditModeElementChangerDirective, isStandalone: true, selector: "[appEditModeElementChanger]", inputs: { isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
419
518
  }
420
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: EditModeElementChangerDirective, decorators: [{
519
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: EditModeElementChangerDirective, decorators: [{
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
  *
@@ -574,14 +636,13 @@ class HasPermissionDirective {
574
636
  this.viewCreated = false;
575
637
  }
576
638
  }
577
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HasPermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
578
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: HasPermissionDirective, isStandalone: true, selector: "[hasPermission]", inputs: { hasPermission: { classPropertyName: "hasPermission", publicName: "hasPermission", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: HasPermissionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
640
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", type: HasPermissionDirective, isStandalone: true, selector: "[hasPermission]", inputs: { hasPermission: { classPropertyName: "hasPermission", publicName: "hasPermission", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
579
641
  }
580
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: HasPermissionDirective, decorators: [{
642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: HasPermissionDirective, decorators: [{
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
 
@@ -599,10 +660,10 @@ class IsEmptyImageDirective {
599
660
  onError() {
600
661
  this.hasError.set(true);
601
662
  }
602
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IsEmptyImageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
603
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: IsEmptyImageDirective, isStandalone: true, selector: "img", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "error": "onError()" }, properties: { "src": "imageSrc()" } }, ngImport: i0 });
663
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IsEmptyImageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
664
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", type: IsEmptyImageDirective, isStandalone: true, selector: "img", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "error": "onError()" }, properties: { "src": "imageSrc()" } }, ngImport: i0 });
604
665
  }
605
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IsEmptyImageDirective, decorators: [{
666
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IsEmptyImageDirective, decorators: [{
606
667
  type: Directive,
607
668
  args: [{
608
669
  selector: 'img',
@@ -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
 
@@ -641,33 +701,29 @@ class PreventDefaultDirective {
641
701
  event.preventDefault();
642
702
  this.action.emit(event);
643
703
  }
644
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PreventDefaultDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
645
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: PreventDefaultDirective, isStandalone: true, selector: "[appPreventDefault]", inputs: { eventType: { classPropertyName: "eventType", publicName: "eventType", isSignal: true, isRequired: false, transformFunction: null }, preventKey: { classPropertyName: "preventKey", publicName: "preventKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 });
704
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PreventDefaultDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
705
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", type: PreventDefaultDirective, isStandalone: true, selector: "[appPreventDefault]", inputs: { eventType: { classPropertyName: "eventType", publicName: "eventType", isSignal: true, isRequired: false, transformFunction: null }, preventKey: { classPropertyName: "preventKey", publicName: "preventKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "keyup": "onKeyup($event)" } }, ngImport: i0 });
646
706
  }
647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PreventDefaultDirective, decorators: [{
707
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PreventDefaultDirective, decorators: [{
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
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
666
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, imports: [CommonModule,
720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
721
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, imports: [CommonModule,
667
722
  FormsModule,
668
723
  ReactiveFormsModule,
669
724
  RouterOutlet,
670
725
  RouterLink,
726
+ RouterLinkActive,
671
727
  IsEmptyImageDirective,
672
728
  NgOptimizedImage,
673
729
  NgComponentOutlet,
@@ -676,17 +732,18 @@ class AngularModule {
676
732
  ReactiveFormsModule,
677
733
  RouterOutlet,
678
734
  RouterLink,
735
+ RouterLinkActive,
679
736
  IsEmptyImageDirective,
680
737
  NgOptimizedImage,
681
738
  NgComponentOutlet,
682
739
  PreventDefaultDirective] });
683
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, providers: [DatePipe], imports: [CommonModule,
740
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, providers: [DatePipe], imports: [CommonModule,
684
741
  FormsModule,
685
742
  ReactiveFormsModule, CommonModule,
686
743
  FormsModule,
687
744
  ReactiveFormsModule] });
688
745
  }
689
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, decorators: [{
746
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, decorators: [{
690
747
  type: NgModule,
691
748
  args: [{
692
749
  imports: [
@@ -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,
@@ -716,8 +775,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
716
775
  }] });
717
776
 
718
777
  class PrimeModule {
719
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
720
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [AutoCompleteModule,
778
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
779
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, exports: [AutoCompleteModule,
721
780
  AvatarModule,
722
781
  ButtonModule,
723
782
  CardModule,
@@ -754,7 +813,7 @@ class PrimeModule {
754
813
  ToggleSwitchModule,
755
814
  TooltipModule,
756
815
  TreeTableModule] });
757
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
816
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
758
817
  AvatarModule,
759
818
  ButtonModule,
760
819
  CardModule,
@@ -792,7 +851,7 @@ class PrimeModule {
792
851
  TooltipModule,
793
852
  TreeTableModule] });
794
853
  }
795
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, decorators: [{
854
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, decorators: [{
796
855
  type: NgModule,
797
856
  args: [{
798
857
  exports: [
@@ -858,11 +917,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
858
917
  *
859
918
  * @example
860
919
  * ```typescript
861
- * // Define service
920
+ * // Define service with global apiBaseUrl
862
921
  * @Injectable({ providedIn: 'root' })
863
922
  * export class UserService extends ApiResourceService<UserDto, User> {
864
- * constructor(http: HttpClient) {
865
- * super('users', http);
923
+ * constructor() {
924
+ * super('auth/users', inject(HttpClient));
925
+ * }
926
+ * }
927
+ *
928
+ * // Define service with feature-specific baseUrl
929
+ * @Injectable({ providedIn: 'root' })
930
+ * export class FormService extends ApiResourceService<FormDto, Form> {
931
+ * constructor() {
932
+ * super('form', inject(HttpClient), 'formBuilder');
933
+ * // URL: services.formBuilder.baseUrl + '/form'
866
934
  * }
867
935
  * }
868
936
  *
@@ -883,7 +951,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
883
951
  */
884
952
  class ApiResourceService {
885
953
  baseUrl;
886
- loaderService = inject(ApiLoaderService);
887
954
  injector = inject(Injector);
888
955
  http;
889
956
  moduleApiName;
@@ -906,6 +973,12 @@ class ApiResourceService {
906
973
  _listResource = null;
907
974
  /** Whether the list resource has been initialized */
908
975
  _resourceInitialized = false;
976
+ /**
977
+ * Signal to track resource initialization for computed signals.
978
+ * This allows computed signals to re-evaluate when the resource is created.
979
+ * Without this, computed signals would not detect when _listResource changes from null.
980
+ */
981
+ _resourceInitSignal = signal(false, ...(ngDevMode ? [{ debugName: "_resourceInitSignal" }] : []));
909
982
  /** Get or create the list resource (lazy initialization) */
910
983
  get listResource() {
911
984
  if (!this._listResource) {
@@ -932,28 +1005,66 @@ class ApiResourceService {
932
1005
  return this.fetchAllAsync(search, filter);
933
1006
  } });
934
1007
  });
1008
+ // Signal that resource is now initialized - triggers computed re-evaluation
1009
+ this._resourceInitSignal.set(true);
935
1010
  }
936
1011
  // ==========================================================================
937
1012
  // Computed State Accessors
938
1013
  // ==========================================================================
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 */
1014
+ /**
1015
+ * Whether data is currently loading.
1016
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1017
+ */
1018
+ isLoading = computed(() => {
1019
+ this._resourceInitSignal(); // Track initialization
1020
+ return this._listResource?.isLoading() ?? false;
1021
+ }, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1022
+ /**
1023
+ * List data array.
1024
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1025
+ */
1026
+ data = computed(() => {
1027
+ this._resourceInitSignal(); // Track initialization
1028
+ return this._listResource?.value()?.data ?? [];
1029
+ }, ...(ngDevMode ? [{ debugName: "data" }] : []));
1030
+ /**
1031
+ * Total count of items.
1032
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1033
+ */
1034
+ total = computed(() => {
1035
+ this._resourceInitSignal(); // Track initialization
1036
+ return this._listResource?.value()?.meta?.total ?? 0;
1037
+ }, ...(ngDevMode ? [{ debugName: "total" }] : []));
1038
+ /**
1039
+ * Pagination metadata.
1040
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1041
+ */
1042
+ pageInfo = computed(() => {
1043
+ this._resourceInitSignal(); // Track initialization
1044
+ return this._listResource?.value()?.meta;
1045
+ }, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
1046
+ /**
1047
+ * Whether there are more pages.
1048
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1049
+ */
948
1050
  hasMore = computed(() => {
1051
+ this._resourceInitSignal(); // Track initialization
949
1052
  const meta = this._listResource?.value()?.meta;
950
1053
  if (!meta)
951
1054
  return false;
952
1055
  return meta.hasMore ?? (meta.page + 1) * meta.pageSize < meta.total;
953
1056
  }, ...(ngDevMode ? [{ debugName: "hasMore" }] : []));
954
- constructor(moduleApiName, http) {
1057
+ /**
1058
+ * @param moduleApiName - The API resource path (e.g., 'form' for /form-builder/form)
1059
+ * @param http - HttpClient instance
1060
+ * @param serviceName - Optional service name for feature-specific base URL (e.g., 'formBuilder')
1061
+ */
1062
+ constructor(moduleApiName, http, serviceName) {
955
1063
  this.moduleApiName = moduleApiName;
956
- this.baseUrl = inject(APP_CONFIG).apiBaseUrl + '/' + moduleApiName;
1064
+ const config = inject(APP_CONFIG);
1065
+ // Use service-specific URL if provided, otherwise fallback to global apiBaseUrl
1066
+ const serviceBaseUrl = serviceName ? getServiceUrl(config, serviceName) : config.apiBaseUrl;
1067
+ this.baseUrl = `${serviceBaseUrl}/${moduleApiName}`;
957
1068
  this.http = http;
958
1069
  // Resource is now lazy-initialized, not created in constructor
959
1070
  }
@@ -1120,38 +1231,6 @@ class ApiResourceService {
1120
1231
  }
1121
1232
  }
1122
1233
 
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
1234
  /**
1156
1235
  * Base class for form controls that support ALL Angular form patterns:
1157
1236
  *
@@ -1249,10 +1328,10 @@ class BaseFormControl {
1249
1328
  this.onTouched();
1250
1329
  }
1251
1330
  }
1252
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1253
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.3", type: BaseFormControl, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", touched: "touchedChange" }, ngImport: i0 });
1331
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1332
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", type: BaseFormControl, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", touched: "touchedChange" }, ngImport: i0 });
1254
1333
  }
1255
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormControl, decorators: [{
1334
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormControl, decorators: [{
1256
1335
  type: Directive
1257
1336
  }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }] } });
1258
1337
  /**
@@ -1275,6 +1354,77 @@ function provideValueAccessor(component) {
1275
1354
  };
1276
1355
  }
1277
1356
 
1357
+ class IconComponent {
1358
+ icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1359
+ iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1360
+ IconTypeEnum = IconTypeEnum; // Needed for template reference
1361
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1362
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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: `
1363
+ @if (icon()) {
1364
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1365
+ <i [class]="icon()"></i>
1366
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1367
+ <img [alt]="icon()" [src]="icon()" />
1368
+ } @else {
1369
+ <i class="pi pi-question"></i>
1370
+ }
1371
+ }
1372
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1373
+ }
1374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IconComponent, decorators: [{
1375
+ type: Component,
1376
+ args: [{
1377
+ selector: 'lib-icon',
1378
+ imports: [AngularModule],
1379
+ changeDetection: ChangeDetectionStrategy.OnPush,
1380
+ template: `
1381
+ @if (icon()) {
1382
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1383
+ <i [class]="icon()"></i>
1384
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1385
+ <img [alt]="icon()" [src]="icon()" />
1386
+ } @else {
1387
+ <i class="pi pi-question"></i>
1388
+ }
1389
+ }
1390
+ `,
1391
+ }]
1392
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1393
+
1394
+ /**
1395
+ * Check if scroll has reached near bottom and calculate next page if available.
1396
+ * Returns next pagination if should load more, null otherwise.
1397
+ *
1398
+ * @example
1399
+ * ```typescript
1400
+ * onScroll(event: Event): void {
1401
+ * const nextPagination = checkScrollPagination(event, {
1402
+ * pagination: this.pagination(),
1403
+ * total: this.total(),
1404
+ * isLoading: this.isLoading(),
1405
+ * });
1406
+ * if (nextPagination) {
1407
+ * this.onPagination.emit(nextPagination);
1408
+ * }
1409
+ * }
1410
+ * ```
1411
+ */
1412
+ function checkScrollPagination(event, config) {
1413
+ const el = event.target;
1414
+ if (!(el instanceof HTMLElement))
1415
+ return null;
1416
+ const threshold = config.threshold ?? 50;
1417
+ const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
1418
+ if (!nearBottom || config.isLoading)
1419
+ return null;
1420
+ const { pagination, total } = config;
1421
+ const nextPage = pagination.currentPage + 1;
1422
+ const hasMore = nextPage * pagination.pageSize < (total ?? 0);
1423
+ if (!hasMore)
1424
+ return null;
1425
+ return { ...pagination, currentPage: nextPage };
1426
+ }
1427
+
1278
1428
  /**
1279
1429
  * Lazy-loading multi-select component with search, pagination, and select-all.
1280
1430
  *
@@ -1284,6 +1434,10 @@ function provideValueAccessor(component) {
1284
1434
  * - Signal forms: `[formField]="formTree.field"`
1285
1435
  */
1286
1436
  class LazyMultiSelectComponent extends BaseFormControl {
1437
+ destroyRef = inject(DestroyRef);
1438
+ onDocumentClickBound = this.handleDocumentClick.bind(this);
1439
+ // View references
1440
+ pSelectRef = viewChild.required('pSelect');
1287
1441
  // Inputs
1288
1442
  placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1289
1443
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1291,14 +1445,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
1291
1445
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1292
1446
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1293
1447
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1294
- /** Two-way bound value using model() for signal forms compatibility */
1448
+ // Two-way bound value
1295
1449
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1296
1450
  // Outputs
1297
1451
  onSearch = output();
1298
1452
  onPagination = output();
1299
- // UI signals
1453
+ // UI state
1300
1454
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1301
- /** Computed: Display text for selected values (replaces getSelectedValue method) */
1455
+ openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1456
+ // Computed values
1302
1457
  selectedValueDisplay = computed(() => {
1303
1458
  const selectedValues = this.value() ?? [];
1304
1459
  if (selectedValues.length === 0)
@@ -1307,44 +1462,59 @@ class LazyMultiSelectComponent extends BaseFormControl {
1307
1462
  return `${selectedValues.length} Items Selected`;
1308
1463
  }
1309
1464
  return this.selectDataList()
1310
- .filter(item => selectedValues.includes(item.value))
1311
- .map(item => item.label)
1465
+ .filter((item) => selectedValues.includes(item.value))
1466
+ .map((item) => item.label)
1312
1467
  .join(', ');
1313
1468
  }, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
1314
- /** Computed: Whether all items are selected (replaces isSelectAll signal) */
1315
1469
  isSelectAll = computed(() => {
1316
1470
  const selectedValues = this.value() ?? [];
1317
- const allValues = this.selectDataList().map(item => item.value);
1471
+ const allValues = this.selectDataList().map((item) => item.value);
1318
1472
  if (selectedValues.length === 0 || allValues.length === 0)
1319
1473
  return false;
1320
- return allValues.every(val => selectedValues.includes(val));
1474
+ return allValues.every((val) => selectedValues.includes(val));
1321
1475
  }, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
1322
1476
  constructor() {
1323
1477
  super();
1324
1478
  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() });
1479
+ // Search debounce using effect
1480
+ let debounceTimeout = null;
1481
+ let previousValue = this.searchTerm();
1482
+ effect((onCleanup) => {
1483
+ const currentValue = this.searchTerm();
1484
+ // Skip unchanged values
1485
+ if (currentValue === previousValue)
1486
+ return;
1487
+ previousValue = currentValue;
1488
+ // Clear existing timeout
1489
+ if (debounceTimeout)
1490
+ clearTimeout(debounceTimeout);
1491
+ // Debounced emit
1492
+ debounceTimeout = setTimeout(() => {
1493
+ this.onSearch.emit(currentValue);
1494
+ }, 500);
1495
+ onCleanup(() => {
1496
+ if (debounceTimeout)
1497
+ clearTimeout(debounceTimeout);
1498
+ });
1499
+ });
1500
+ // Document click listener for closing dropdown
1501
+ afterNextRender(() => {
1502
+ document.addEventListener('click', this.onDocumentClickBound);
1503
+ });
1504
+ this.destroyRef.onDestroy(() => {
1505
+ document.removeEventListener('click', this.onDocumentClickBound);
1330
1506
  });
1331
1507
  }
1332
- onScrollBound = this.onScroll.bind(this);
1333
- multiScrollContainer = viewChild.required('multiScrollContainer');
1334
1508
  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
- }
1509
+ const nextPagination = checkScrollPagination(event, {
1510
+ pagination: this.pagination(),
1511
+ total: this.total(),
1512
+ isLoading: this.isLoading(),
1513
+ });
1514
+ if (nextPagination) {
1515
+ this.onPagination.emit(nextPagination);
1344
1516
  }
1345
1517
  }
1346
- pSelectRef = viewChild.required('pSelect');
1347
- openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1348
1518
  onSelectClick(event) {
1349
1519
  if (this.disabled())
1350
1520
  return;
@@ -1363,7 +1533,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1363
1533
  }
1364
1534
  }
1365
1535
  isSelected(data) {
1366
- return this.value()?.includes(data.value);
1536
+ return this.value()?.includes(data.value) ?? false;
1367
1537
  }
1368
1538
  key(option) {
1369
1539
  return option.value;
@@ -1376,8 +1546,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1376
1546
  }
1377
1547
  }
1378
1548
  else {
1379
- const updated = previousValue.filter((v) => v !== option.value);
1380
- this.value.set(updated);
1549
+ this.value.set(previousValue.filter((v) => v !== option.value));
1381
1550
  }
1382
1551
  }
1383
1552
  changeSelectAll(event) {
@@ -1392,16 +1561,13 @@ class LazyMultiSelectComponent extends BaseFormControl {
1392
1561
  event.stopPropagation();
1393
1562
  this.value.set([]);
1394
1563
  }
1395
- 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 });
1564
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1565
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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
1566
  }
1398
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1567
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1399
1568
  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
- }] } });
1569
+ 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"] }]
1570
+ }], 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
1571
 
1406
1572
  /**
1407
1573
  * Lazy-loading single select component with search and pagination.
@@ -1412,6 +1578,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1412
1578
  * - Signal forms: `[formField]="formTree.field"`
1413
1579
  */
1414
1580
  class LazySelectComponent extends BaseFormControl {
1581
+ destroyRef = inject(DestroyRef);
1582
+ onScrollBound = this.onScroll.bind(this);
1583
+ scrollTargetEl = null;
1584
+ // View references
1585
+ scrollContainer = viewChild.required('scrollContainer');
1415
1586
  // Inputs
1416
1587
  placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1417
1588
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -1421,77 +1592,85 @@ class LazySelectComponent extends BaseFormControl {
1421
1592
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1422
1593
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1423
1594
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1424
- /** Two-way bound value using model() for signal forms compatibility */
1595
+ // Two-way bound value
1425
1596
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1426
1597
  // Outputs
1427
1598
  onSearch = output();
1428
1599
  onPagination = output();
1429
- // UI signals
1600
+ // UI state
1430
1601
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1431
- // Effect hooks
1602
+ isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1432
1603
  constructor() {
1433
1604
  super();
1434
1605
  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() });
1606
+ // Search debounce using effect
1607
+ let debounceTimeout = null;
1608
+ let previousValue = this.searchTerm();
1609
+ effect((onCleanup) => {
1610
+ const currentValue = this.searchTerm();
1611
+ // Skip unchanged values
1612
+ if (currentValue === previousValue)
1613
+ return;
1614
+ previousValue = currentValue;
1615
+ // Clear existing timeout
1616
+ if (debounceTimeout)
1617
+ clearTimeout(debounceTimeout);
1618
+ // Debounced emit
1619
+ debounceTimeout = setTimeout(() => {
1620
+ this.onSearch.emit(currentValue);
1621
+ }, 500);
1622
+ onCleanup(() => {
1623
+ if (debounceTimeout)
1624
+ clearTimeout(debounceTimeout);
1625
+ });
1626
+ });
1627
+ // Cleanup scroll listener on destroy
1628
+ this.destroyRef.onDestroy(() => {
1629
+ if (this.scrollTargetEl) {
1630
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1631
+ }
1439
1632
  });
1440
1633
  }
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
1634
  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
- }
1635
+ const nextPagination = checkScrollPagination(event, {
1636
+ pagination: this.pagination(),
1637
+ total: this.total(),
1638
+ isLoading: this.isLoading(),
1639
+ });
1640
+ if (nextPagination) {
1641
+ this.onPagination.emit(nextPagination);
1456
1642
  }
1457
1643
  }
1458
- // Toggle panel and manage scroll event
1459
1644
  showPanel() {
1460
1645
  if (this.disabled())
1461
1646
  return;
1462
- this.isPanelShow.update(prev => !prev);
1647
+ this.isPanelShow.update((prev) => !prev);
1463
1648
  const isNowVisible = this.isPanelShow();
1464
1649
  if (isNowVisible) {
1465
- setTimeout(() => {
1650
+ afterNextRender(() => {
1466
1651
  const containerEl = this.scrollContainer().nativeElement;
1467
1652
  const target = containerEl.querySelector('.p-select-list-container');
1468
1653
  if (target) {
1469
1654
  target.addEventListener('scroll', this.onScrollBound);
1470
1655
  this.scrollTargetEl = target;
1471
1656
  }
1472
- }, 0);
1657
+ }, { injector: this.injector });
1473
1658
  }
1474
- else {
1475
- if (this.scrollTargetEl) {
1476
- this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1477
- this.scrollTargetEl = null;
1478
- }
1659
+ else if (this.scrollTargetEl) {
1660
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1661
+ this.scrollTargetEl = null;
1479
1662
  }
1480
1663
  }
1481
1664
  onBlur() {
1482
1665
  this.markAsTouched();
1483
1666
  }
1484
- 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 });
1667
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1668
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.5", 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
1669
  }
1487
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
1670
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, decorators: [{
1488
1671
  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 }] }] } });
1672
+ 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" }]
1673
+ }], 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
1674
 
1496
1675
  /**
1497
1676
  * Injection Tokens for Provider Interfaces
@@ -1589,56 +1768,33 @@ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1589
1768
  */
1590
1769
  const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
1591
1770
 
1592
- const DEFAULT_PAGE_SIZE$2 = 20;
1771
+ const DEFAULT_PAGE_SIZE$1 = 20;
1593
1772
  /**
1594
- * User Select Component - Single user selection with lazy loading.
1595
- *
1596
- * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1773
+ * Base class for user selection components.
1774
+ * Provides shared user loading, pagination, and search functionality.
1597
1775
  *
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
- * ```
1776
+ * Subclasses must implement:
1777
+ * - `setupValueEffect()` to track value changes and emit selection events
1619
1778
  */
1620
- class UserSelectComponent {
1779
+ class BaseUserSelectComponent {
1621
1780
  destroyRef = inject(DestroyRef);
1781
+ injector = inject(Injector);
1622
1782
  userProvider = inject(USER_PROVIDER);
1623
1783
  abortController = null;
1624
- // Optional: custom function to load users (uses USER_PROVIDER if not provided)
1625
- loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1626
1784
  // Inputs
1785
+ loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1627
1786
  placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1628
1787
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1629
1788
  filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1630
1789
  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" }] : []));
1790
+ pageSize = input(DEFAULT_PAGE_SIZE$1, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
1634
1791
  // Outputs
1635
- userSelected = output();
1636
1792
  onError = output();
1637
1793
  // Internal state
1638
1794
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1639
1795
  users = signal([], ...(ngDevMode ? [{ debugName: "users" }] : []));
1640
1796
  total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
1641
- pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$2, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1797
+ pagination = signal({ pageSize: DEFAULT_PAGE_SIZE$1, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
1642
1798
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1643
1799
  // Computed dropdown data
1644
1800
  dropdownUsers = computed(() => this.users().map((user) => ({
@@ -1661,20 +1817,8 @@ class UserSelectComponent {
1661
1817
  afterNextRender(() => {
1662
1818
  this.fetchUsers();
1663
1819
  });
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
- });
1820
+ // Setup value change tracking (implemented by subclass)
1821
+ this.setupValueEffect();
1678
1822
  }
1679
1823
  handleSearch(search) {
1680
1824
  this.searchTerm.set(search);
@@ -1686,6 +1830,12 @@ class UserSelectComponent {
1686
1830
  this.pagination.set(pagination);
1687
1831
  this.fetchUsers(true);
1688
1832
  }
1833
+ /** Reload users (useful when filters change externally) */
1834
+ reload() {
1835
+ this.pagination.update((p) => ({ ...p, currentPage: 0 }));
1836
+ this.users.set([]);
1837
+ this.fetchUsers();
1838
+ }
1689
1839
  async fetchUsers(append = false) {
1690
1840
  if (this.isLoading())
1691
1841
  return;
@@ -1743,14 +1893,63 @@ class UserSelectComponent {
1743
1893
  })),
1744
1894
  })));
1745
1895
  }
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();
1896
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseUserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1897
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.5", 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 });
1898
+ }
1899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseUserSelectComponent, decorators: [{
1900
+ type: Directive
1901
+ }], 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"] }] } });
1902
+
1903
+ /**
1904
+ * User Select Component - Single user selection with lazy loading.
1905
+ *
1906
+ * Uses USER_PROVIDER internally by default, or accepts custom `loadUsers` function.
1907
+ *
1908
+ * Features:
1909
+ * - Search with debouncing (handled by lazy-select)
1910
+ * - Infinite scroll pagination
1911
+ * - Filter active users by default (configurable)
1912
+ * - Supports additional filters via `additionalFilters` input
1913
+ *
1914
+ * @example
1915
+ * ```html
1916
+ * <!-- Simple usage - uses USER_PROVIDER internally -->
1917
+ * <lib-user-select
1918
+ * [(value)]="selectedUserId"
1919
+ * [isEditMode]="true"
1920
+ * />
1921
+ *
1922
+ * <!-- With custom loadUsers function -->
1923
+ * <lib-user-select
1924
+ * [(value)]="selectedUserId"
1925
+ * [isEditMode]="true"
1926
+ * [loadUsers]="customLoadUsers"
1927
+ * />
1928
+ * ```
1929
+ */
1930
+ class UserSelectComponent extends BaseUserSelectComponent {
1931
+ // Two-way bound value
1932
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1933
+ // Outputs
1934
+ userSelected = output();
1935
+ setupValueEffect() {
1936
+ // Emit selected user when value changes
1937
+ effect(() => {
1938
+ const selectedId = this.value();
1939
+ const users = this.users();
1940
+ untracked(() => {
1941
+ if (selectedId) {
1942
+ const user = users.find((u) => u.id === selectedId);
1943
+ this.userSelected.emit(user ?? null);
1944
+ }
1945
+ else {
1946
+ this.userSelected.emit(null);
1947
+ }
1948
+ });
1949
+ });
1751
1950
  }
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: `
1951
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1952
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.5", 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
1953
  <lib-lazy-select
1755
1954
  [(value)]="value"
1756
1955
  [placeHolder]="placeHolder()"
@@ -1766,11 +1965,10 @@ class UserSelectComponent {
1766
1965
  />
1767
1966
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: LazySelectComponent, selector: "lib-lazy-select", inputs: ["placeHolder", "optionLabel", "optionValue", "isEditMode", "isLoading", "total", "pagination", "selectDataList", "value"], outputs: ["valueChange", "onSearch", "onPagination"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1768
1967
  }
1769
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserSelectComponent, decorators: [{
1968
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserSelectComponent, decorators: [{
1770
1969
  type: Component,
1771
1970
  args: [{
1772
1971
  selector: 'lib-user-select',
1773
- standalone: true,
1774
1972
  imports: [AngularModule, PrimeModule, LazySelectComponent],
1775
1973
  template: `
1776
1974
  <lib-lazy-select
@@ -1789,9 +1987,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1789
1987
  `,
1790
1988
  changeDetection: ChangeDetectionStrategy.OnPush,
1791
1989
  }]
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"] }] } });
1990
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
1793
1991
 
1794
- const DEFAULT_PAGE_SIZE$1 = 20;
1795
1992
  /**
1796
1993
  * User Multi-Select Component - Multiple user selection with lazy loading.
1797
1994
  *
@@ -1820,50 +2017,12 @@ const DEFAULT_PAGE_SIZE$1 = 20;
1820
2017
  * />
1821
2018
  * ```
1822
2019
  */
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" }] : []));
2020
+ class UserMultiSelectComponent extends BaseUserSelectComponent {
1835
2021
  // Two-way bound value
1836
2022
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1837
2023
  // Outputs
1838
2024
  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
- });
2025
+ setupValueEffect() {
1867
2026
  // Emit selected users when value changes
1868
2027
  effect(() => {
1869
2028
  const selectedIds = this.value() ?? [];
@@ -1874,81 +2033,8 @@ class UserMultiSelectComponent {
1874
2033
  });
1875
2034
  });
1876
2035
  }
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: `
2036
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserMultiSelectComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2037
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.5", 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
2038
  <lib-lazy-multi-select
1953
2039
  [(value)]="value"
1954
2040
  [placeHolder]="placeHolder()"
@@ -1962,11 +2048,10 @@ class UserMultiSelectComponent {
1962
2048
  />
1963
2049
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: LazyMultiSelectComponent, selector: "lib-lazy-multi-select", inputs: ["placeHolder", "isEditMode", "isLoading", "total", "pagination", "selectDataList", "value"], outputs: ["valueChange", "onSearch", "onPagination"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1964
2050
  }
1965
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserMultiSelectComponent, decorators: [{
2051
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserMultiSelectComponent, decorators: [{
1966
2052
  type: Component,
1967
2053
  args: [{
1968
2054
  selector: 'lib-user-multi-select',
1969
- standalone: true,
1970
2055
  imports: [AngularModule, PrimeModule, LazyMultiSelectComponent],
1971
2056
  template: `
1972
2057
  <lib-lazy-multi-select
@@ -1983,47 +2068,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1983
2068
  `,
1984
2069
  changeDetection: ChangeDetectionStrategy.OnPush,
1985
2070
  }]
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"] }] } });
2071
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
1987
2072
 
1988
2073
  /**
1989
2074
  * File Uploader Component - Drag & drop file upload with type filtering.
1990
2075
  *
1991
2076
  * 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
2077
  */
2028
2078
  class FileUploaderComponent {
2029
2079
  messageService = inject(MessageService);
@@ -2169,11 +2219,7 @@ class FileUploaderComponent {
2169
2219
  });
2170
2220
  }
2171
2221
  catch (error) {
2172
- this.messageService.add({
2173
- severity: 'error',
2174
- summary: 'Upload Failed',
2175
- detail: error.message || 'Failed to upload file',
2176
- });
2222
+ // Error toast handled by global interceptor
2177
2223
  this.onError.emit(error);
2178
2224
  }
2179
2225
  finally {
@@ -2192,40 +2238,44 @@ class FileUploaderComponent {
2192
2238
  const mb = kb / 1024;
2193
2239
  return `${mb.toFixed(1)} MB`;
2194
2240
  }
2195
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2196
- 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: `
2241
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2242
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", 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
2243
  <div
2198
- class="file-uploader"
2199
- [class.drag-over]="isDragOver()"
2200
- [class.disabled]="disabled()"
2244
+ class="w-full"
2245
+ [class.opacity-60]="disabled()"
2201
2246
  (dragover)="onDragOver($event)"
2202
2247
  (dragleave)="onDragLeave($event)"
2203
2248
  (drop)="onDrop($event)"
2204
2249
  >
2205
- <!-- Upload Area -->
2206
- <div class="upload-area" (click)="fileInput.click()">
2250
+ <!-- Upload Area - Responsive padding -->
2251
+ <div
2252
+ 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"
2253
+ [class.drag-over]="isDragOver()"
2254
+ [class.cursor-not-allowed]="disabled()"
2255
+ (click)="fileInput.click()"
2256
+ >
2207
2257
  @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>
2258
+ <div class="flex flex-col items-center">
2259
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2260
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2211
2261
  @if (uploadProgress() > 0) {
2212
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2262
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2213
2263
  }
2214
2264
  </div>
2215
2265
  } @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">
2266
+ <div class="flex flex-col items-center">
2267
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2268
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2219
2269
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2220
2270
  </p>
2221
- <p class="text-sm text-color-secondary">
2271
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2222
2272
  @if (acceptTypesDisplay()) {
2223
2273
  Allowed: {{ acceptTypesDisplay() }}
2224
2274
  } @else {
2225
2275
  All file types allowed
2226
2276
  }
2227
2277
  @if (maxSizeMb()) {
2228
- (Max {{ maxSizeMb() }}MB)
2278
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2229
2279
  }
2230
2280
  </p>
2231
2281
  </div>
@@ -2236,71 +2286,76 @@ class FileUploaderComponent {
2236
2286
  <input
2237
2287
  #fileInput
2238
2288
  type="file"
2289
+ class="hidden"
2239
2290
  [accept]="acceptString()"
2240
2291
  [multiple]="multiple()"
2241
2292
  [disabled]="disabled() || isUploading()"
2242
2293
  (change)="onFileSelected($event)"
2243
- class="hidden"
2244
2294
  />
2245
2295
 
2246
- <!-- Selected Files Preview -->
2296
+ <!-- Selected Files Preview - Responsive layout -->
2247
2297
  @if (selectedFiles().length > 0 && showPreview()) {
2248
- <div class="selected-files mt-3">
2298
+ <div class="mt-3 space-y-2">
2249
2299
  @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"
2300
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2301
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2302
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2303
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2304
+ <p-button
2257
2305
  icon="pi pi-times"
2258
- class="p-button-text p-button-rounded p-button-sm"
2259
- (click)="removeFile(file)"
2306
+ [text]="true"
2307
+ [rounded]="true"
2308
+ size="small"
2309
+ severity="secondary"
2260
2310
  [disabled]="isUploading()"
2261
- ></button>
2311
+ (onClick)="removeFile(file)"
2312
+ />
2262
2313
  </div>
2263
2314
  }
2264
2315
  </div>
2265
2316
  }
2266
2317
  </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 });
2318
+ `, 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
2319
  }
2269
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
2320
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, decorators: [{
2270
2321
  type: Component,
2271
- args: [{ selector: 'lib-file-uploader', standalone: true, imports: [AngularModule, PrimeModule], template: `
2322
+ args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
2272
2323
  <div
2273
- class="file-uploader"
2274
- [class.drag-over]="isDragOver()"
2275
- [class.disabled]="disabled()"
2324
+ class="w-full"
2325
+ [class.opacity-60]="disabled()"
2276
2326
  (dragover)="onDragOver($event)"
2277
2327
  (dragleave)="onDragLeave($event)"
2278
2328
  (drop)="onDrop($event)"
2279
2329
  >
2280
- <!-- Upload Area -->
2281
- <div class="upload-area" (click)="fileInput.click()">
2330
+ <!-- Upload Area - Responsive padding -->
2331
+ <div
2332
+ 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"
2333
+ [class.drag-over]="isDragOver()"
2334
+ [class.cursor-not-allowed]="disabled()"
2335
+ (click)="fileInput.click()"
2336
+ >
2282
2337
  @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>
2338
+ <div class="flex flex-col items-center">
2339
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2340
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2286
2341
  @if (uploadProgress() > 0) {
2287
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2342
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2288
2343
  }
2289
2344
  </div>
2290
2345
  } @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">
2346
+ <div class="flex flex-col items-center">
2347
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2348
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2294
2349
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2295
2350
  </p>
2296
- <p class="text-sm text-color-secondary">
2351
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2297
2352
  @if (acceptTypesDisplay()) {
2298
2353
  Allowed: {{ acceptTypesDisplay() }}
2299
2354
  } @else {
2300
2355
  All file types allowed
2301
2356
  }
2302
2357
  @if (maxSizeMb()) {
2303
- (Max {{ maxSizeMb() }}MB)
2358
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2304
2359
  }
2305
2360
  </p>
2306
2361
  </div>
@@ -2311,35 +2366,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2311
2366
  <input
2312
2367
  #fileInput
2313
2368
  type="file"
2369
+ class="hidden"
2314
2370
  [accept]="acceptString()"
2315
2371
  [multiple]="multiple()"
2316
2372
  [disabled]="disabled() || isUploading()"
2317
2373
  (change)="onFileSelected($event)"
2318
- class="hidden"
2319
2374
  />
2320
2375
 
2321
- <!-- Selected Files Preview -->
2376
+ <!-- Selected Files Preview - Responsive layout -->
2322
2377
  @if (selectedFiles().length > 0 && showPreview()) {
2323
- <div class="selected-files mt-3">
2378
+ <div class="mt-3 space-y-2">
2324
2379
  @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"
2380
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2381
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2382
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2383
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2384
+ <p-button
2332
2385
  icon="pi pi-times"
2333
- class="p-button-text p-button-rounded p-button-sm"
2334
- (click)="removeFile(file)"
2386
+ [text]="true"
2387
+ [rounded]="true"
2388
+ size="small"
2389
+ severity="secondary"
2335
2390
  [disabled]="isUploading()"
2336
- ></button>
2391
+ (onClick)="removeFile(file)"
2392
+ />
2337
2393
  </div>
2338
2394
  }
2339
2395
  </div>
2340
2396
  }
2341
2397
  </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"] }]
2398
+ `, 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
2399
  }], 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
2400
 
2345
2401
  const DEFAULT_PAGE_SIZE = 20;
@@ -2454,16 +2510,14 @@ class FileSelectorDialogComponent {
2454
2510
  }, 500);
2455
2511
  }
2456
2512
  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
- }
2513
+ const nextPagination = checkScrollPagination(event, {
2514
+ pagination: this.pagination(),
2515
+ total: this.total(),
2516
+ isLoading: this.isLoading(),
2517
+ });
2518
+ if (nextPagination) {
2519
+ this.pagination.set(nextPagination);
2520
+ this.fetchFiles(true);
2467
2521
  }
2468
2522
  }
2469
2523
  toggleSelection(file) {
@@ -2565,8 +2619,8 @@ class FileSelectorDialogComponent {
2565
2619
  this.isLoading.set(false);
2566
2620
  }
2567
2621
  }
2568
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2569
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: FileSelectorDialogComponent, isStandalone: true, selector: "lib-file-selector-dialog", inputs: { loadFiles: { classPropertyName: "loadFiles", publicName: "loadFiles", isSignal: true, isRequired: true, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, acceptTypes: { classPropertyName: "acceptTypes", publicName: "acceptTypes", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visible: "visibleChange", fileSelected: "fileSelected", filesSelected: "filesSelected", closed: "closed", onError: "onError" }, ngImport: i0, template: `
2622
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2623
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: FileSelectorDialogComponent, isStandalone: true, selector: "lib-file-selector-dialog", inputs: { loadFiles: { classPropertyName: "loadFiles", publicName: "loadFiles", isSignal: true, isRequired: true, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, acceptTypes: { classPropertyName: "acceptTypes", publicName: "acceptTypes", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visible: "visibleChange", fileSelected: "fileSelected", filesSelected: "filesSelected", closed: "closed", onError: "onError" }, ngImport: i0, template: `
2570
2624
  <p-dialog
2571
2625
  [header]="header()"
2572
2626
  [(visible)]="visible"
@@ -2574,11 +2628,13 @@ class FileSelectorDialogComponent {
2574
2628
  [closable]="true"
2575
2629
  [draggable]="false"
2576
2630
  [resizable]="false"
2577
- [style]="{ width: '800px', maxHeight: '90vh' }"
2631
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2632
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2633
+ styleClass="file-selector-dialog"
2578
2634
  (onHide)="onDialogHide()"
2579
2635
  >
2580
2636
  <!-- Search Bar -->
2581
- <div class="flex gap-2 mb-3">
2637
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2582
2638
  <span class="p-input-icon-left flex-1">
2583
2639
  <i class="pi pi-search"></i>
2584
2640
  <input
@@ -2591,25 +2647,25 @@ class FileSelectorDialogComponent {
2591
2647
  />
2592
2648
  </span>
2593
2649
  @if (multiple()) {
2594
- <span class="text-sm text-color-secondary align-self-center">
2650
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2595
2651
  {{ selectedFiles().length }} selected
2596
2652
  </span>
2597
2653
  }
2598
2654
  </div>
2599
2655
 
2600
- <!-- File Grid -->
2656
+ <!-- File Grid - Responsive columns -->
2601
2657
  <div
2602
2658
  class="file-grid"
2603
2659
  #scrollContainer
2604
2660
  (scroll)="onScroll($event)"
2605
2661
  >
2606
2662
  @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>
2663
+ <div class="col-span-full flex justify-center p-4">
2664
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2609
2665
  </div>
2610
2666
  } @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>
2667
+ <div class="col-span-full text-center p-4 text-color-secondary">
2668
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2613
2669
  <p>No files found</p>
2614
2670
  </div>
2615
2671
  } @else {
@@ -2623,54 +2679,59 @@ class FileSelectorDialogComponent {
2623
2679
  <!-- File Preview -->
2624
2680
  <div class="file-preview">
2625
2681
  @if (isImage(file) && file.url) {
2626
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2682
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2627
2683
  } @else {
2628
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2684
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2629
2685
  }
2630
2686
  @if (isSelected(file)) {
2631
2687
  <div class="selected-overlay">
2632
- <i class="pi pi-check"></i>
2688
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2633
2689
  </div>
2634
2690
  }
2635
2691
  </div>
2636
2692
 
2637
2693
  <!-- 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>
2694
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2695
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2696
+ {{ file.name }}
2697
+ </span>
2698
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2641
2699
  </div>
2642
2700
  </div>
2643
2701
  }
2644
2702
 
2645
2703
  @if (isLoading()) {
2646
- <div class="flex justify-content-center p-2 w-full">
2647
- <i class="pi pi-spin pi-spinner"></i>
2704
+ <div class="col-span-full flex justify-center p-2">
2705
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2648
2706
  </div>
2649
2707
  }
2650
2708
  }
2651
2709
  </div>
2652
2710
 
2653
2711
  <!-- 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>
2712
+ <ng-template #footer>
2713
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2714
+ <button
2715
+ pButton
2716
+ label="Cancel"
2717
+ class="p-button-text w-full sm:w-auto"
2718
+ (click)="onCancel()"
2719
+ ></button>
2720
+ <button
2721
+ pButton
2722
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2723
+ [disabled]="selectedFiles().length === 0"
2724
+ class="w-full sm:w-auto"
2725
+ (click)="onConfirm()"
2726
+ ></button>
2727
+ </div>
2667
2728
  </ng-template>
2668
2729
  </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:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: 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 });
2730
+ `, 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
2731
  }
2671
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2732
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2672
2733
  type: Component,
2673
- args: [{ selector: 'lib-file-selector-dialog', standalone: true, imports: [AngularModule, PrimeModule], template: `
2734
+ args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
2674
2735
  <p-dialog
2675
2736
  [header]="header()"
2676
2737
  [(visible)]="visible"
@@ -2678,11 +2739,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2678
2739
  [closable]="true"
2679
2740
  [draggable]="false"
2680
2741
  [resizable]="false"
2681
- [style]="{ width: '800px', maxHeight: '90vh' }"
2742
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2743
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2744
+ styleClass="file-selector-dialog"
2682
2745
  (onHide)="onDialogHide()"
2683
2746
  >
2684
2747
  <!-- Search Bar -->
2685
- <div class="flex gap-2 mb-3">
2748
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2686
2749
  <span class="p-input-icon-left flex-1">
2687
2750
  <i class="pi pi-search"></i>
2688
2751
  <input
@@ -2695,25 +2758,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2695
2758
  />
2696
2759
  </span>
2697
2760
  @if (multiple()) {
2698
- <span class="text-sm text-color-secondary align-self-center">
2761
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2699
2762
  {{ selectedFiles().length }} selected
2700
2763
  </span>
2701
2764
  }
2702
2765
  </div>
2703
2766
 
2704
- <!-- File Grid -->
2767
+ <!-- File Grid - Responsive columns -->
2705
2768
  <div
2706
2769
  class="file-grid"
2707
2770
  #scrollContainer
2708
2771
  (scroll)="onScroll($event)"
2709
2772
  >
2710
2773
  @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>
2774
+ <div class="col-span-full flex justify-center p-4">
2775
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2713
2776
  </div>
2714
2777
  } @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>
2778
+ <div class="col-span-full text-center p-4 text-color-secondary">
2779
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2717
2780
  <p>No files found</p>
2718
2781
  </div>
2719
2782
  } @else {
@@ -2727,182 +2790,484 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2727
2790
  <!-- File Preview -->
2728
2791
  <div class="file-preview">
2729
2792
  @if (isImage(file) && file.url) {
2730
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2793
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2731
2794
  } @else {
2732
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2795
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2733
2796
  }
2734
2797
  @if (isSelected(file)) {
2735
2798
  <div class="selected-overlay">
2736
- <i class="pi pi-check"></i>
2799
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2737
2800
  </div>
2738
2801
  }
2739
2802
  </div>
2740
2803
 
2741
2804
  <!-- 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>
2805
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2806
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2807
+ {{ file.name }}
2808
+ </span>
2809
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2745
2810
  </div>
2746
2811
  </div>
2747
2812
  }
2748
2813
 
2749
2814
  @if (isLoading()) {
2750
- <div class="flex justify-content-center p-2 w-full">
2751
- <i class="pi pi-spin pi-spinner"></i>
2815
+ <div class="col-span-full flex justify-center p-2">
2816
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2752
2817
  </div>
2753
2818
  }
2754
2819
  }
2755
2820
  </div>
2756
2821
 
2757
2822
  <!-- 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>
2823
+ <ng-template #footer>
2824
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2825
+ <button
2826
+ pButton
2827
+ label="Cancel"
2828
+ class="p-button-text w-full sm:w-auto"
2829
+ (click)="onCancel()"
2830
+ ></button>
2831
+ <button
2832
+ pButton
2833
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2834
+ [disabled]="selectedFiles().length === 0"
2835
+ class="w-full sm:w-auto"
2836
+ (click)="onConfirm()"
2837
+ ></button>
2838
+ </div>
2771
2839
  </ng-template>
2772
2840
  </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:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"] }]
2841
+ `, 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
2842
  }], 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
2843
 
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 = '/') {
2844
+ function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
2814
2845
  return () => {
2815
2846
  const permissionValidator = inject(PermissionValidatorService);
2816
2847
  const router = inject(Router);
2817
- // Check if permissions are loaded
2818
2848
  if (!permissionValidator.isPermissionsLoaded()) {
2819
- devLog('[permissionGuard] Permissions not loaded, denying access to route');
2849
+ if (isDevMode()) {
2850
+ console.log(`[${guardName}] Permissions not loaded, denying access`);
2851
+ }
2820
2852
  return router.createUrlTree([redirectTo]);
2821
2853
  }
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}`);
2854
+ if (!evaluate(permissionValidator.permissions())) {
2855
+ if (isDevMode()) {
2856
+ console.log(`[${guardName}] ${getDenialMessage()}`);
2857
+ }
2827
2858
  return router.createUrlTree([redirectTo]);
2828
2859
  }
2829
2860
  return true;
2830
2861
  };
2831
2862
  }
2832
2863
  /**
2833
- * Any Permission Guard (OR logic)
2864
+ * Permission Guard - Single permission or ILogicNode check.
2834
2865
  *
2835
- * Allows access if user has ANY of the specified permissions.
2866
+ * @example
2867
+ * ```typescript
2868
+ * { path: 'users', canActivate: [permissionGuard('user.view')] }
2869
+ * { path: 'admin', canActivate: [permissionGuard(logicNode, '/access-denied')] }
2870
+ * ```
2871
+ */
2872
+ function permissionGuard(permission, redirectTo = '/') {
2873
+ const code = typeof permission === 'string' ? permission : 'complex-logic';
2874
+ return createGuard('permissionGuard', redirectTo, (perms) => evaluatePermission(permission, perms), () => `Access denied - missing: ${code}`);
2875
+ }
2876
+ /**
2877
+ * Any Permission Guard (OR logic) - Access if user has ANY permission.
2836
2878
  *
2837
2879
  * @example
2838
2880
  * ```typescript
2839
- * // Allow if user has view OR create permission
2840
2881
  * { path: 'users', canActivate: [anyPermissionGuard(['user.view', 'user.create'])] }
2841
2882
  * ```
2842
2883
  */
2843
2884
  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
- };
2885
+ if (!permissions?.length) {
2886
+ return () => {
2887
+ if (isDevMode()) {
2888
+ console.log('[anyPermissionGuard] Empty permissions array, denying');
2889
+ }
2890
+ return inject(Router).createUrlTree([redirectTo]);
2891
+ };
2892
+ }
2893
+ return createGuard('anyPermissionGuard', redirectTo, (perms) => hasAnyPermission(permissions, perms), () => `Access denied - missing any of: ${permissions.join(', ')}`);
2865
2894
  }
2866
2895
  /**
2867
- * All Permissions Guard (AND logic)
2868
- *
2869
- * Allows access only if user has ALL of the specified permissions.
2896
+ * All Permissions Guard (AND logic) - Access only if user has ALL permissions.
2870
2897
  *
2871
2898
  * @example
2872
2899
  * ```typescript
2873
- * // Allow only if user has BOTH view AND create permissions
2874
2900
  * { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
2875
2901
  * ```
2876
2902
  */
2877
2903
  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]);
2904
+ if (!permissions?.length) {
2905
+ return () => {
2906
+ if (isDevMode()) {
2907
+ console.log('[allPermissionsGuard] Empty permissions array, denying');
2908
+ }
2909
+ return inject(Router).createUrlTree([redirectTo]);
2910
+ };
2911
+ }
2912
+ return createGuard('allPermissionsGuard', redirectTo, (perms) => hasAllPermissions(permissions, perms), () => `Access denied - missing all of: ${permissions.join(', ')}`);
2913
+ }
2914
+
2915
+ /**
2916
+ * Base class for form page components that handle create/edit operations.
2917
+ * Provides common functionality for loading existing items, form submission,
2918
+ * navigation, and toast notifications.
2919
+ *
2920
+ * ## Features
2921
+ * - Automatic route parameter handling (loads item when ID is present)
2922
+ * - Edit mode detection based on existing item
2923
+ * - Unified submit handler for create/update operations
2924
+ * - Cancel navigation
2925
+ * - Toast messages for success/validation errors
2926
+ *
2927
+ * ## Usage
2928
+ *
2929
+ * ```typescript
2930
+ * interface IProductFormModel {
2931
+ * name: string;
2932
+ * price: number;
2933
+ * }
2934
+ *
2935
+ * @Component({
2936
+ * selector: 'app-product-form',
2937
+ * standalone: true,
2938
+ * changeDetection: ChangeDetectionStrategy.OnPush,
2939
+ * template: `...`
2940
+ * })
2941
+ * export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
2942
+ * private readonly productService = inject(ProductApiService);
2943
+ *
2944
+ * // Form model signal (private writable, public readonly)
2945
+ * private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
2946
+ * readonly formModel = this._formModel.asReadonly();
2947
+ *
2948
+ * // Required abstract implementations
2949
+ * getFormModel(): Signal<IProductFormModel> {
2950
+ * return this.formModel;
2951
+ * }
2952
+ *
2953
+ * getResourceRoute(): string {
2954
+ * return '/products';
2955
+ * }
2956
+ *
2957
+ * getResourceName(): string {
2958
+ * return 'Product';
2959
+ * }
2960
+ *
2961
+ * isFormValid(): boolean {
2962
+ * const model = this.formModel();
2963
+ * return model.name.trim().length > 0 && model.price > 0;
2964
+ * }
2965
+ *
2966
+ * loadItem(id: string): void {
2967
+ * this.isLoading.set(true);
2968
+ * this.productService.findById(id)
2969
+ * .pipe(takeUntilDestroyed(this.destroyRef))
2970
+ * .subscribe({
2971
+ * next: (response) => {
2972
+ * if (response.success && response.data) {
2973
+ * this.existingItem.set(response.data);
2974
+ * this._formModel.set({
2975
+ * name: response.data.name,
2976
+ * price: response.data.price,
2977
+ * });
2978
+ * }
2979
+ * this.isLoading.set(false);
2980
+ * },
2981
+ * error: () => {
2982
+ * this.router.navigate([this.getResourceRoute()]);
2983
+ * this.isLoading.set(false);
2984
+ * },
2985
+ * });
2986
+ * }
2987
+ *
2988
+ * createItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
2989
+ * return this.productService.insert(model);
2990
+ * }
2991
+ *
2992
+ * updateItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
2993
+ * return this.productService.update({ id: this.existingItem()!.id, ...model });
2994
+ * }
2995
+ * }
2996
+ * ```
2997
+ *
2998
+ * @template T The entity/interface type being edited
2999
+ * @template TFormModel The form model interface
3000
+ */
3001
+ class BaseFormPage {
3002
+ router = inject(Router);
3003
+ route = inject(ActivatedRoute);
3004
+ messageService = inject(MessageService);
3005
+ destroyRef = inject(DestroyRef);
3006
+ routeParams = toSignal(this.route.paramMap);
3007
+ initialized = false;
3008
+ /** Loading state for async operations */
3009
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3010
+ /** The existing item when in edit mode, null when creating */
3011
+ existingItem = signal(null, ...(ngDevMode ? [{ debugName: "existingItem" }] : []));
3012
+ /** Whether the form is in edit mode (has existing item) */
3013
+ isEditMode = computed(() => !!this.existingItem(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
3014
+ constructor() {
3015
+ effect(() => {
3016
+ const params = this.routeParams();
3017
+ if (!params || this.initialized)
3018
+ return;
3019
+ this.initialized = true;
3020
+ const id = params.get('id');
3021
+ if (id && id !== 'new') {
3022
+ this.loadItem(id);
3023
+ }
3024
+ });
3025
+ }
3026
+ /**
3027
+ * Handle form submission.
3028
+ * Validates the form, then calls createItem or updateItem based on mode.
3029
+ * Shows appropriate toast messages and navigates back on success.
3030
+ */
3031
+ onSubmit() {
3032
+ if (!this.isFormValid()) {
3033
+ this.showValidationError();
3034
+ return;
2896
3035
  }
2897
- return true;
2898
- };
3036
+ this.isLoading.set(true);
3037
+ const model = this.getFormModel()();
3038
+ const operation$ = this.isEditMode()
3039
+ ? this.updateItem(model)
3040
+ : this.createItem(model);
3041
+ operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3042
+ next: () => {
3043
+ const action = this.isEditMode() ? 'updated' : 'created';
3044
+ this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
3045
+ this.router.navigate([this.getResourceRoute()]);
3046
+ },
3047
+ error: () => {
3048
+ this.isLoading.set(false);
3049
+ },
3050
+ complete: () => {
3051
+ this.isLoading.set(false);
3052
+ },
3053
+ });
3054
+ }
3055
+ /**
3056
+ * Handle cancel action.
3057
+ * Navigates back to the resource list.
3058
+ */
3059
+ onCancel() {
3060
+ this.router.navigate([this.getResourceRoute()]);
3061
+ }
3062
+ /**
3063
+ * Show validation error toast.
3064
+ * Override to customize the validation error message.
3065
+ */
3066
+ showValidationError() {
3067
+ this.messageService.add({
3068
+ severity: 'error',
3069
+ summary: 'Validation Error',
3070
+ detail: 'Please fill in all required fields.',
3071
+ });
3072
+ }
3073
+ /**
3074
+ * Show success toast.
3075
+ * @param detail The success message to display
3076
+ */
3077
+ showSuccess(detail) {
3078
+ this.messageService.add({
3079
+ severity: 'success',
3080
+ summary: 'Success',
3081
+ detail,
3082
+ });
3083
+ }
3084
+ /**
3085
+ * Show error toast.
3086
+ * @param detail The error message to display
3087
+ */
3088
+ showError(detail) {
3089
+ this.messageService.add({
3090
+ severity: 'error',
3091
+ summary: 'Error',
3092
+ detail,
3093
+ });
3094
+ }
3095
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3096
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.5", type: BaseFormPage, isStandalone: true, ngImport: i0 });
2899
3097
  }
3098
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormPage, decorators: [{
3099
+ type: Directive
3100
+ }], ctorParameters: () => [] });
3101
+
3102
+ /**
3103
+ * Base List Page
3104
+ * Abstract directive providing common signals, computed values, and utilities
3105
+ * for list page components across all feature packages.
3106
+ *
3107
+ * Features:
3108
+ * - Pagination state management (pageSize, currentPage, total)
3109
+ * - Loading state
3110
+ * - CRUD navigation helpers
3111
+ * - Message display utilities
3112
+ * - Delete confirmation with API integration
3113
+ * - Company feature flag support
3114
+ *
3115
+ * Usage:
3116
+ * ```typescript
3117
+ * @Component({ ... })
3118
+ * export class UserListComponent extends BaseListPage<IUser> {
3119
+ * getResourceRoute(): string { return '/users'; }
3120
+ * getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
3121
+ * async loadData(): Promise<void> { ... }
3122
+ * }
3123
+ * ```
3124
+ */
3125
+ class BaseListPage {
3126
+ router = inject(Router);
3127
+ messageService = inject(MessageService);
3128
+ appConfig = inject(APP_CONFIG);
3129
+ confirmationService = inject(ConfirmationService);
3130
+ destroyRef = inject(DestroyRef);
3131
+ /** Items list */
3132
+ items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3133
+ /** Loading state */
3134
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3135
+ /** Total records for pagination */
3136
+ total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : []));
3137
+ /** Page size */
3138
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
3139
+ /** First record index (for p-table lazy load) */
3140
+ first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
3141
+ /** Current page (0-based for API, derived from first/pageSize) */
3142
+ currentPage = computed(() => Math.floor(this.first() / this.pageSize()), ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
3143
+ /** Show company info if company feature enabled */
3144
+ showCompanyInfo = computed(() => this.appConfig.enableCompanyFeature, ...(ngDevMode ? [{ debugName: "showCompanyInfo" }] : []));
3145
+ /**
3146
+ * Navigate to create page
3147
+ */
3148
+ onCreate() {
3149
+ this.router.navigate([this.getResourceRoute(), 'new']);
3150
+ }
3151
+ /**
3152
+ * Navigate to edit page
3153
+ * @param id The ID of the item to edit
3154
+ */
3155
+ onEdit(id) {
3156
+ this.router.navigate([this.getResourceRoute(), id]);
3157
+ }
3158
+ /**
3159
+ * Handle page change from p-table lazy load
3160
+ */
3161
+ onPageChange(event) {
3162
+ this.first.set(event.first ?? 0);
3163
+ this.pageSize.set(event.rows ?? 10);
3164
+ this.loadData();
3165
+ }
3166
+ /**
3167
+ * Get pagination params for API call
3168
+ * Returns 0-based page for backend API
3169
+ */
3170
+ getPaginationParams() {
3171
+ return {
3172
+ currentPage: this.currentPage(),
3173
+ pageSize: this.pageSize(),
3174
+ };
3175
+ }
3176
+ /**
3177
+ * Show success toast message
3178
+ */
3179
+ showSuccess(detail, summary = 'Success') {
3180
+ this.messageService.add({ severity: 'success', summary, detail });
3181
+ }
3182
+ /**
3183
+ * Show error toast message
3184
+ */
3185
+ showError(detail, summary = 'Error') {
3186
+ this.messageService.add({ severity: 'error', summary, detail });
3187
+ }
3188
+ /**
3189
+ * Show info toast message
3190
+ */
3191
+ showInfo(detail, summary = 'Info') {
3192
+ this.messageService.add({ severity: 'info', summary, detail });
3193
+ }
3194
+ /**
3195
+ * Show warning toast message
3196
+ */
3197
+ showWarn(detail, summary = 'Warning') {
3198
+ this.messageService.add({ severity: 'warn', summary, detail });
3199
+ }
3200
+ /**
3201
+ * Delete an item with confirmation dialog
3202
+ * @param item The item to delete
3203
+ * @param idGetter Function to extract ID from item
3204
+ * @param deleteApiCall Function that returns Observable for delete API call
3205
+ * @param options Optional configuration
3206
+ */
3207
+ onDelete(item, idGetter, deleteApiCall, options) {
3208
+ this.confirmationService.confirm({
3209
+ message: this.getDeleteConfirmMessage(item),
3210
+ header: options?.header ?? 'Confirm Delete',
3211
+ icon: 'pi pi-exclamation-triangle',
3212
+ acceptButtonStyleClass: 'p-button-danger',
3213
+ accept: () => {
3214
+ deleteApiCall(idGetter(item))
3215
+ .pipe(takeUntilDestroyed(this.destroyRef))
3216
+ .subscribe({
3217
+ next: () => {
3218
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3219
+ this.loadData();
3220
+ },
3221
+ error: () => {
3222
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3223
+ },
3224
+ });
3225
+ },
3226
+ });
3227
+ }
3228
+ /**
3229
+ * Delete an item with confirmation dialog using async/await
3230
+ * @param item The item to delete
3231
+ * @param idGetter Function to extract ID from item
3232
+ * @param deleteApiCall Async function for delete API call
3233
+ * @param options Optional configuration
3234
+ */
3235
+ async onDeleteAsync(item, idGetter, deleteApiCall, options) {
3236
+ this.confirmationService.confirm({
3237
+ message: this.getDeleteConfirmMessage(item),
3238
+ header: options?.header ?? 'Confirm Delete',
3239
+ icon: 'pi pi-exclamation-triangle',
3240
+ acceptButtonStyleClass: 'p-button-danger',
3241
+ accept: async () => {
3242
+ try {
3243
+ await deleteApiCall(idGetter(item));
3244
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3245
+ await this.loadData();
3246
+ }
3247
+ catch {
3248
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3249
+ }
3250
+ },
3251
+ });
3252
+ }
3253
+ /**
3254
+ * Navigate to a route
3255
+ */
3256
+ navigateTo(path) {
3257
+ this.router.navigate(path);
3258
+ }
3259
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseListPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3260
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.5", type: BaseListPage, isStandalone: true, ngImport: i0 });
3261
+ }
3262
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseListPage, decorators: [{
3263
+ type: Directive
3264
+ }] });
2900
3265
 
2901
- // Interfaces
3266
+ // Constants
2902
3267
 
2903
3268
  /**
2904
3269
  * Generated bundle index. Do not edit.
2905
3270
  */
2906
3271
 
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 };
3272
+ 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
3273
  //# sourceMappingURL=flusys-ng-shared.mjs.map