@flusys/ng-shared 3.0.0 → 4.0.0-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
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, 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
- import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe } from '@angular/common';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, Pipe, Injector, isDevMode, NgModule, runInInjectionContext, resource, model, untracked, forwardRef, Component, ApplicationRef, viewChild, afterNextRender, ViewEncapsulation, DestroyRef, InjectionToken } from '@angular/core';
3
+ import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe, DOCUMENT as DOCUMENT$1 } from '@angular/common';
5
4
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
6
- import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
5
+ import { APP_CONFIG, getServiceUrl, TRANSLATE_ADAPTER, FALLBACK_MESSAGES_REGISTRY } from '@flusys/ng-core';
7
6
  import { of, firstValueFrom, map as map$1 } from 'rxjs';
8
7
  import { map, tap, catchError } from 'rxjs/operators';
9
8
  import * as i1$1 from '@angular/forms';
@@ -18,12 +17,14 @@ import * as i1 from 'primeng/checkbox';
18
17
  import { CheckboxModule } from 'primeng/checkbox';
19
18
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
20
19
  import { DatePickerModule } from 'primeng/datepicker';
21
- import * as i4 from 'primeng/dialog';
20
+ import * as i5 from 'primeng/dialog';
22
21
  import { DialogModule } from 'primeng/dialog';
23
22
  import { DividerModule } from 'primeng/divider';
24
23
  import { FileUploadModule } from 'primeng/fileupload';
24
+ import * as i6 from 'primeng/iconfield';
25
25
  import { IconFieldModule } from 'primeng/iconfield';
26
26
  import { ImageModule } from 'primeng/image';
27
+ import * as i7 from 'primeng/inputicon';
27
28
  import { InputIconModule } from 'primeng/inputicon';
28
29
  import { InputNumberModule } from 'primeng/inputnumber';
29
30
  import * as i2 from 'primeng/inputtext';
@@ -38,7 +39,7 @@ import * as i2$1 from 'primeng/progressbar';
38
39
  import { ProgressBarModule } from 'primeng/progressbar';
39
40
  import { RadioButtonModule } from 'primeng/radiobutton';
40
41
  import { RippleModule } from 'primeng/ripple';
41
- import * as i3$1 from 'primeng/select';
42
+ import * as i3 from 'primeng/select';
42
43
  import { SelectModule } from 'primeng/select';
43
44
  import { SelectButtonModule } from 'primeng/selectbutton';
44
45
  import { SkeletonModule } from 'primeng/skeleton';
@@ -49,23 +50,16 @@ import { TabsModule } from 'primeng/tabs';
49
50
  import { TagModule } from 'primeng/tag';
50
51
  import { TextareaModule } from 'primeng/textarea';
51
52
  import { ToastModule } from 'primeng/toast';
53
+ import { ToggleButtonModule } from 'primeng/togglebutton';
52
54
  import { ToggleSwitchModule } from 'primeng/toggleswitch';
53
55
  import { TooltipModule } from 'primeng/tooltip';
54
56
  import { TreeTableModule } from 'primeng/treetable';
57
+ import { ProgressSpinnerModule } from 'primeng/progressspinner';
58
+ import { ColorPickerModule } from 'primeng/colorpicker';
59
+ import * as i3$1 from 'primeng/api';
55
60
  import { MessageService, ConfirmationService } from 'primeng/api';
56
61
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
57
62
 
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
63
  const USER_PERMISSIONS = {
70
64
  CREATE: 'user.create',
71
65
  READ: 'user.read',
@@ -84,7 +78,6 @@ const BRANCH_PERMISSIONS = {
84
78
  UPDATE: 'branch.update',
85
79
  DELETE: 'branch.delete',
86
80
  };
87
- // ==================== IAM MODULE ====================
88
81
  const ACTION_PERMISSIONS = {
89
82
  CREATE: 'action.create',
90
83
  READ: 'action.read',
@@ -113,7 +106,6 @@ const COMPANY_ACTION_PERMISSIONS = {
113
106
  READ: 'company-action.read',
114
107
  ASSIGN: 'company-action.assign',
115
108
  };
116
- // ==================== STORAGE MODULE ====================
117
109
  const FILE_PERMISSIONS = {
118
110
  CREATE: 'file.create',
119
111
  READ: 'file.read',
@@ -132,7 +124,6 @@ const STORAGE_CONFIG_PERMISSIONS = {
132
124
  UPDATE: 'storage-config.update',
133
125
  DELETE: 'storage-config.delete',
134
126
  };
135
- // ==================== EMAIL MODULE ====================
136
127
  const EMAIL_CONFIG_PERMISSIONS = {
137
128
  CREATE: 'email-config.create',
138
129
  READ: 'email-config.read',
@@ -145,43 +136,444 @@ const EMAIL_TEMPLATE_PERMISSIONS = {
145
136
  UPDATE: 'email-template.update',
146
137
  DELETE: 'email-template.delete',
147
138
  };
148
- // ==================== FORM BUILDER MODULE ====================
149
139
  const FORM_PERMISSIONS = {
150
140
  CREATE: 'form.create',
151
141
  READ: 'form.read',
152
142
  UPDATE: 'form.update',
153
143
  DELETE: 'form.delete',
154
144
  };
155
- // ==================== AGGREGATED EXPORTS ====================
156
- /**
157
- * All permission codes grouped by module
158
- */
145
+ const EVENT_PERMISSIONS = {
146
+ CREATE: 'event.create',
147
+ READ: 'event.read',
148
+ UPDATE: 'event.update',
149
+ DELETE: 'event.delete',
150
+ };
151
+ const EVENT_PARTICIPANT_PERMISSIONS = {
152
+ CREATE: 'event-participant.create',
153
+ READ: 'event-participant.read',
154
+ UPDATE: 'event-participant.update',
155
+ DELETE: 'event-participant.delete',
156
+ };
157
+ const NOTIFICATION_PERMISSIONS = {
158
+ CREATE: 'notification.create',
159
+ READ: 'notification.read',
160
+ UPDATE: 'notification.update',
161
+ DELETE: 'notification.delete',
162
+ };
163
+ const LANGUAGE_PERMISSIONS = {
164
+ CREATE: 'language.create',
165
+ READ: 'language.read',
166
+ UPDATE: 'language.update',
167
+ DELETE: 'language.delete',
168
+ };
169
+ const TRANSLATION_KEY_PERMISSIONS = {
170
+ CREATE: 'translation-key.create',
171
+ READ: 'translation-key.read',
172
+ UPDATE: 'translation-key.update',
173
+ DELETE: 'translation-key.delete',
174
+ };
175
+ const TRANSLATION_PERMISSIONS = {
176
+ CREATE: 'translation.create',
177
+ READ: 'translation.read',
178
+ UPDATE: 'translation.update',
179
+ DELETE: 'translation.delete',
180
+ };
159
181
  const PERMISSIONS = {
160
- // Auth
161
182
  USER: USER_PERMISSIONS,
162
183
  COMPANY: COMPANY_PERMISSIONS,
163
184
  BRANCH: BRANCH_PERMISSIONS,
164
- // IAM
165
185
  ACTION: ACTION_PERMISSIONS,
166
186
  ROLE: ROLE_PERMISSIONS,
167
187
  ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
168
188
  USER_ROLE: USER_ROLE_PERMISSIONS,
169
189
  USER_ACTION: USER_ACTION_PERMISSIONS,
170
190
  COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
171
- // Storage
172
191
  FILE: FILE_PERMISSIONS,
173
192
  FOLDER: FOLDER_PERMISSIONS,
174
193
  STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
175
- // Email
176
194
  EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
177
195
  EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
178
- // Form Builder
179
196
  FORM: FORM_PERMISSIONS,
197
+ EVENT: EVENT_PERMISSIONS,
198
+ EVENT_PARTICIPANT: EVENT_PARTICIPANT_PERMISSIONS,
199
+ NOTIFICATION: NOTIFICATION_PERMISSIONS,
200
+ LANGUAGE: LANGUAGE_PERMISSIONS,
201
+ TRANSLATION_KEY: TRANSLATION_KEY_PERMISSIONS,
202
+ TRANSLATION: TRANSLATION_PERMISSIONS,
180
203
  };
181
204
 
182
- /**
183
- * Common file type filters
184
- */
205
+ const SHARED_MESSAGES = {
206
+ // Common actions
207
+ 'shared.save': 'Save',
208
+ 'shared.cancel': 'Cancel',
209
+ 'shared.delete': 'Delete',
210
+ 'shared.edit': 'Edit',
211
+ 'shared.create': 'Create',
212
+ 'shared.update': 'Update',
213
+ 'shared.add': 'Add',
214
+ 'shared.remove': 'Remove',
215
+ 'shared.close': 'Close',
216
+ 'shared.confirm': 'Confirm',
217
+ 'shared.back': 'Back',
218
+ 'shared.next': 'Next',
219
+ 'shared.previous': 'Previous',
220
+ 'shared.submit': 'Submit',
221
+ 'shared.reset': 'Reset',
222
+ 'shared.clear': 'Clear',
223
+ 'shared.search': 'Search',
224
+ 'shared.filter': 'Filter',
225
+ 'shared.refresh': 'Refresh',
226
+ 'shared.export': 'Export',
227
+ 'shared.import': 'Import',
228
+ 'shared.download': 'Download',
229
+ 'shared.upload': 'Upload',
230
+ 'shared.view': 'View',
231
+ 'shared.details': 'Details',
232
+ 'shared.actions': 'Actions',
233
+ 'shared.options': 'Options',
234
+ 'shared.settings': 'Settings',
235
+ 'shared.yes': 'Yes',
236
+ 'shared.no': 'No',
237
+ 'shared.continue': 'Continue',
238
+ 'shared.view.details': 'View Details',
239
+ // Common labels
240
+ 'shared.name': 'Name',
241
+ 'shared.description': 'Description',
242
+ 'shared.status': 'Status',
243
+ 'shared.active': 'Active',
244
+ 'shared.inactive': 'Inactive',
245
+ 'shared.enabled': 'Enabled',
246
+ 'shared.disabled': 'Disabled',
247
+ 'shared.verified': 'Verified',
248
+ 'shared.unverified': 'Unverified',
249
+ 'shared.read.only': 'Read Only',
250
+ 'shared.assigned': 'Assigned',
251
+ 'shared.not.assigned': 'Not Assigned',
252
+ 'shared.unknown': 'Unknown',
253
+ 'shared.na': 'N/A',
254
+ 'shared.no.company': 'No Company',
255
+ 'shared.company': 'Company',
256
+ 'shared.no.branch': 'No Branch',
257
+ 'shared.display.order': 'Display Order',
258
+ 'shared.display.order.placeholder': 'Enter display order',
259
+ 'shared.created.at': 'Created At',
260
+ 'shared.updated.at': 'Updated At',
261
+ 'shared.created.by': 'Created By',
262
+ 'shared.updated.by': 'Updated By',
263
+ 'shared.date': 'Date',
264
+ 'shared.time': 'Time',
265
+ 'shared.email': 'Email',
266
+ 'shared.phone': 'Phone',
267
+ 'shared.address': 'Address',
268
+ 'shared.type': 'Type',
269
+ 'shared.category': 'Category',
270
+ 'shared.code': 'Code',
271
+ 'shared.serial': 'Serial',
272
+ 'shared.order': 'Order',
273
+ 'shared.priority': 'Priority',
274
+ 'shared.notes': 'Notes',
275
+ 'shared.comments': 'Comments',
276
+ // Toast summaries
277
+ 'shared.info': 'Info',
278
+ 'shared.warning': 'Warning',
279
+ 'shared.confirm.delete.header': 'Confirm Delete',
280
+ // Status messages
281
+ 'shared.loading': 'Loading...',
282
+ 'shared.saving': 'Saving...',
283
+ 'shared.deleting': 'Deleting...',
284
+ 'shared.processing': 'Processing...',
285
+ 'shared.please.wait': 'Please wait...',
286
+ // Success messages
287
+ 'shared.success': 'Success',
288
+ 'shared.operation.success': 'Operation completed successfully',
289
+ 'shared.save.success': 'Saved successfully',
290
+ 'shared.delete.success': 'Deleted successfully',
291
+ 'shared.update.success': 'Updated successfully',
292
+ 'shared.create.success': 'Created successfully',
293
+ // Error messages
294
+ 'shared.error': 'Error',
295
+ 'shared.operation.failed': 'Operation failed',
296
+ 'shared.save.failed': 'Failed to save',
297
+ 'shared.delete.failed': 'Failed to delete',
298
+ 'shared.update.failed': 'Failed to update',
299
+ 'shared.create.failed': 'Failed to create',
300
+ 'shared.load.failed': 'Failed to load data',
301
+ 'shared.unexpected.error': 'An unexpected error occurred',
302
+ 'shared.network.error': 'Network error. Please check your connection.',
303
+ 'shared.server.error': 'Server error. Please try again later.',
304
+ // Validation messages
305
+ 'shared.required': 'This field is required',
306
+ 'shared.invalid.email': 'Please enter a valid email address',
307
+ 'shared.invalid.phone': 'Please enter a valid phone number',
308
+ 'shared.min.length': 'Minimum {{min}} characters required',
309
+ 'shared.max.length': 'Maximum {{max}} characters allowed',
310
+ 'shared.min.value': 'Minimum value is {{min}}',
311
+ 'shared.max.value': 'Maximum value is {{max}}',
312
+ 'shared.invalid.format': 'Invalid format',
313
+ 'shared.password.mismatch': 'Passwords do not match',
314
+ // Confirmation messages
315
+ 'shared.confirm.delete': 'Are you sure you want to delete this item?',
316
+ 'shared.confirm.delete.item': 'Are you sure you want to delete "{{name}}"?',
317
+ 'shared.confirm.delete.multiple': 'Are you sure you want to delete {{count}} items?',
318
+ 'shared.confirm.cancel': 'Are you sure you want to cancel? Unsaved changes will be lost.',
319
+ 'shared.confirm.action': 'Are you sure you want to proceed?',
320
+ 'shared.unsaved.changes': 'You have unsaved changes.',
321
+ // Empty states
322
+ 'shared.no.data': 'No data available',
323
+ 'shared.no.results': 'No results found',
324
+ 'shared.empty.list': 'The list is empty',
325
+ 'shared.no.items.selected': 'No items selected',
326
+ // Pagination
327
+ 'shared.showing': 'Showing',
328
+ 'shared.of': 'of',
329
+ 'shared.items': 'items',
330
+ 'shared.page': 'Page',
331
+ 'shared.rows.per.page': 'Rows per page',
332
+ 'shared.first': 'First',
333
+ 'shared.last': 'Last',
334
+ // CRUD operation messages (used by createApiController)
335
+ 'item.create.success': 'Item created successfully',
336
+ 'item.create.many.success': '{{count}} items created successfully',
337
+ 'item.get.success': 'Item retrieved successfully',
338
+ 'item.get.all.success': 'Items retrieved successfully',
339
+ 'item.update.success': 'Item updated successfully',
340
+ 'item.update.many.success': '{{count}} items updated successfully',
341
+ 'item.delete.success': '{{count}} item(s) deleted successfully',
342
+ 'item.restore.success': '{{count}} item(s) restored successfully',
343
+ // HTTP error messages (for error interceptor)
344
+ 'shared.error.bad.request': 'Bad Request',
345
+ 'shared.error.not.found': 'Not Found',
346
+ 'shared.error.conflict': 'Conflict',
347
+ 'shared.error.validation.error': 'Validation Error',
348
+ 'shared.error.server.error': 'Server Error',
349
+ 'shared.error.service.unavailable': 'Service Unavailable',
350
+ // Validation
351
+ 'shared.validation.error': 'Validation Error',
352
+ 'shared.fill.all.fields': 'Please fill in all required fields correctly.',
353
+ 'shared.fill.required.fields': 'Please fill in all required fields',
354
+ 'shared.name.required': 'Name is required',
355
+ 'shared.email.required': 'Email is required',
356
+ 'shared.select.all': 'Select All',
357
+ 'shared.deselect.all': 'Deselect All',
358
+ 'shared.save.changes': 'Save Changes',
359
+ 'shared.validation.required': '{{field}} is required',
360
+ 'shared.validation.email': 'Please enter a valid email address',
361
+ 'shared.validation.min.length': '{{field}} must be at least {{min}} characters',
362
+ 'shared.validation.max.length': '{{field}} must be at most {{max}} characters',
363
+ 'shared.validation.min': '{{field}} must be at least {{min}}',
364
+ 'shared.validation.max': '{{field}} must be at most {{max}}',
365
+ 'shared.validation.pattern': '{{field}} format is invalid',
366
+ 'shared.password.required': 'Password is required',
367
+ 'shared.confirm.password.required': 'Please confirm your password',
368
+ 'shared.min.characters': 'Min. {{count}} characters',
369
+ 'shared.select': 'Select...',
370
+ 'shared.select.option': 'Select an option',
371
+ 'shared.placeholder.current.password': 'Enter current password',
372
+ 'shared.placeholder.new.password': 'Enter new password',
373
+ 'shared.placeholder.confirm.password': 'Confirm new password',
374
+ // File uploader
375
+ 'shared.upload.drop.multiple': 'Drop files here or click to upload',
376
+ 'shared.upload.drop.single': 'Drop file here or click to upload',
377
+ 'shared.upload.allowed.types': 'Allowed:',
378
+ 'shared.upload.all.types.allowed': 'All file types allowed',
379
+ 'shared.upload.uploading': 'Uploading {{fileName}}...',
380
+ 'shared.upload.max.size': '(Max {{size}}MB)',
381
+ 'shared.upload.invalid.type': 'Invalid File Type',
382
+ 'shared.upload.file.too.large': 'File Too Large',
383
+ 'shared.upload.complete': 'Upload Complete',
384
+ 'shared.upload.files': 'files',
385
+ 'shared.upload.files.uploaded': '{{count}} file(s) uploaded successfully',
386
+ 'shared.upload.failed': 'Upload failed',
387
+ 'shared.upload.provider.not.configured': 'File upload not configured. Add {{provider}} to your app config.',
388
+ // File type categories
389
+ 'shared.file.type.images': 'Images',
390
+ 'shared.file.type.documents': 'Documents',
391
+ 'shared.file.type.videos': 'Videos',
392
+ 'shared.file.type.audio': 'Audio',
393
+ // Size units
394
+ 'shared.units.kb': 'KB',
395
+ 'shared.units.mb': 'MB',
396
+ 'shared.units.gb': 'GB',
397
+ 'shared.units.tb': 'TB',
398
+ 'shared.units.bytes': 'Bytes',
399
+ // File selector
400
+ 'shared.file.selector.search.placeholder': 'Search files...',
401
+ 'shared.file.selector.no.files': 'No files found',
402
+ 'shared.file.selector.selected': '{{count}} selected',
403
+ 'shared.file.selector.select.multiple': 'Select ({{count}})',
404
+ 'shared.file.selector.select': 'Select',
405
+ 'shared.file.selector.default.header': 'Select File',
406
+ 'shared.file.selector.select.file': 'Select File',
407
+ 'shared.file.selector.select.files': 'Select Files',
408
+ 'shared.file.selector.all.folders': 'All Folders',
409
+ 'shared.file.selector.all.storage': 'All Storage',
410
+ 'shared.file.selector.provider.not.configured': 'File selection not configured.',
411
+ 'shared.file.selector.add.provider': 'Add',
412
+ 'shared.default': 'Default',
413
+ // User/option select
414
+ 'shared.user.select.placeholder': 'Select User',
415
+ 'shared.select.placeholder': 'Select Option',
416
+ 'shared.multi.select.placeholder': 'Select Options',
417
+ 'shared.multi.select.items.selected': '{{count}} Items Selected',
418
+ 'shared.select.deselect.all': 'Select/Deselect All',
419
+ 'shared.loading.actions': 'Loading actions...',
420
+ 'shared.loading.roles': 'Loading roles...',
421
+ 'shared.pending.changes': 'Pending Changes',
422
+ 'shared.to.add': 'To Add',
423
+ 'shared.to.remove': 'To Remove',
424
+ 'shared.to.assign': 'To Assign',
425
+ 'shared.to.whitelist': 'To Whitelist',
426
+ 'shared.description.placeholder': 'Enter description',
427
+ // Backend error keys (used by exception filters)
428
+ 'error.generic': 'An error occurred',
429
+ 'error.not.found': 'Resource not found',
430
+ 'error.validation': 'Validation failed',
431
+ 'error.unauthorized': 'Unauthorized access',
432
+ 'error.forbidden': 'Access forbidden',
433
+ 'error.conflict': 'Resource conflict',
434
+ 'error.internal': 'Internal server error',
435
+ 'error.service.unavailable': 'Service temporarily unavailable',
436
+ 'error.http': 'HTTP error',
437
+ 'error.unknown': 'Unknown error occurred',
438
+ 'error.permission.system.unavailable': 'Permission system temporarily unavailable. Please try again later.',
439
+ 'error.insufficient.permissions': 'Missing required permissions: {{permissions}}',
440
+ 'error.insufficient.permissions.or': 'Requires at least one of: {{permissions}}',
441
+ 'error.no.permissions.found': 'No permissions found. Please contact administrator.',
442
+ // Backend system keys (infrastructure errors)
443
+ 'system.repository.not.available': '{{entity}} repository not available',
444
+ 'system.datasource.not.available': 'Data source not available',
445
+ 'system.database.config.not.available': 'Database configuration not available',
446
+ 'system.service.not.available': 'Service "{{provider}}" not available. Available: {{available}}',
447
+ 'system.config.required': 'Configuration required',
448
+ 'system.internal.error': 'Failed to initialize "{{provider}}": {{error}}',
449
+ 'system.not.found': 'System resource not found',
450
+ 'system.duplicate.request': 'Duplicate request detected',
451
+ 'system.invalid.tenant.id': 'Invalid tenant ID',
452
+ 'system.tenant.not.found': 'Tenant "{{tenantId}}" not found',
453
+ 'system.tenant.header.required': 'Tenant not found. Ensure "{{header}}" header is set.',
454
+ 'system.missing.parameter': 'Missing required parameter: {{key}}',
455
+ 'system.sdk.not.installed': 'Required SDK "{{sdk}}" not installed. Run: npm install {{sdk}}',
456
+ 'system.path.traversal.detected': 'Path traversal detected',
457
+ 'system.invalid.file.key': 'Invalid file key',
458
+ // Aliases for commonly used keys (dot-separated)
459
+ 'shared.all': 'All',
460
+ // PrimeNG component messages (synced to PrimeNGConfig)
461
+ 'primeng.empty.message': 'No results found',
462
+ 'primeng.empty.filter.message': 'No results found',
463
+ 'primeng.empty.search.message': 'No results found',
464
+ 'primeng.selection.message': '{0} items selected',
465
+ 'primeng.empty.selection.message': 'No selected item',
466
+ 'primeng.choose': 'Choose',
467
+ 'primeng.upload': 'Upload',
468
+ 'primeng.cancel': 'Cancel',
469
+ 'primeng.pending': 'Pending',
470
+ 'primeng.choose.date': 'Choose Date',
471
+ 'primeng.today': 'Today',
472
+ 'primeng.clear': 'Clear',
473
+ 'primeng.week.header': 'Wk',
474
+ 'primeng.first.day.of.week': '0',
475
+ 'primeng.accept': 'Yes',
476
+ 'primeng.reject': 'No',
477
+ 'primeng.lt': 'Less than',
478
+ 'primeng.lte': 'Less than or equal to',
479
+ 'primeng.gt': 'Greater than',
480
+ 'primeng.gte': 'Greater than or equal to',
481
+ 'primeng.date.is': 'Date is',
482
+ 'primeng.date.is.not': 'Date is not',
483
+ 'primeng.date.before': 'Date is before',
484
+ 'primeng.date.after': 'Date is after',
485
+ 'primeng.contains': 'Contains',
486
+ 'primeng.not.contains': 'Not contains',
487
+ 'primeng.starts.with': 'Starts with',
488
+ 'primeng.ends.with': 'Ends with',
489
+ 'primeng.equals': 'Equals',
490
+ 'primeng.not.equals': 'Not equals',
491
+ 'primeng.no.filter': 'No Filter',
492
+ 'primeng.match.all': 'Match All',
493
+ 'primeng.match.any': 'Match Any',
494
+ 'primeng.add.rule': 'Add Rule',
495
+ 'primeng.remove.rule': 'Remove Rule',
496
+ 'primeng.apply': 'Apply',
497
+ 'primeng.aria.true.label': 'True',
498
+ 'primeng.aria.false.label': 'False',
499
+ 'primeng.aria.null.label': 'Not Selected',
500
+ 'primeng.aria.star': '1 star',
501
+ 'primeng.aria.stars': '{star} stars',
502
+ 'primeng.aria.select.all': 'All items selected',
503
+ 'primeng.aria.unselect.all': 'All items unselected',
504
+ 'primeng.aria.close': 'Close',
505
+ 'primeng.aria.previous': 'Previous',
506
+ 'primeng.aria.next': 'Next',
507
+ 'primeng.aria.navigation': 'Navigation',
508
+ 'primeng.aria.scroll.top': 'Scroll Top',
509
+ 'primeng.aria.move.top': 'Move Top',
510
+ 'primeng.aria.move.up': 'Move Up',
511
+ 'primeng.aria.move.down': 'Move Down',
512
+ 'primeng.aria.move.bottom': 'Move Bottom',
513
+ 'primeng.aria.move.to.target': 'Move to Target',
514
+ 'primeng.aria.move.to.source': 'Move to Source',
515
+ 'primeng.aria.move.all.to.target': 'Move All to Target',
516
+ 'primeng.aria.move.all.to.source': 'Move All to Source',
517
+ 'primeng.aria.page.label': 'Page {page}',
518
+ 'primeng.aria.first.page.label': 'First Page',
519
+ 'primeng.aria.last.page.label': 'Last Page',
520
+ 'primeng.aria.next.page.label': 'Next Page',
521
+ 'primeng.aria.prev.page.label': 'Previous Page',
522
+ 'primeng.aria.rows.per.page.label': 'Rows per page',
523
+ 'primeng.aria.jump.to.page.dropdown.label': 'Jump to Page Dropdown',
524
+ 'primeng.aria.jump.to.page.input.label': 'Jump to Page Input',
525
+ 'primeng.aria.select.row': 'Row Selected',
526
+ 'primeng.aria.unselect.row': 'Row Unselected',
527
+ 'primeng.aria.expand.row': 'Row Expanded',
528
+ 'primeng.aria.collapse.row': 'Row Collapsed',
529
+ 'primeng.aria.show.filter.menu': 'Show Filter Menu',
530
+ 'primeng.aria.hide.filter.menu': 'Hide Filter Menu',
531
+ 'primeng.aria.filter.operator': 'Filter Operator',
532
+ 'primeng.aria.filter.constraint': 'Filter Constraint',
533
+ 'primeng.aria.edit.row': 'Edit Row',
534
+ 'primeng.aria.save.edit': 'Save Edit',
535
+ 'primeng.aria.cancel.edit': 'Cancel Edit',
536
+ 'primeng.aria.list.view': 'List View',
537
+ 'primeng.aria.grid.view': 'Grid View',
538
+ 'primeng.aria.slide': 'Slide',
539
+ 'primeng.aria.slide.number': '{slideNumber}',
540
+ 'primeng.aria.zoom.image': 'Zoom Image',
541
+ 'primeng.aria.zoom.in': 'Zoom In',
542
+ 'primeng.aria.zoom.out': 'Zoom Out',
543
+ 'primeng.aria.rotate.right': 'Rotate Right',
544
+ 'primeng.aria.rotate.left': 'Rotate Left',
545
+ // Common aliases (backward compatibility for modules using common.* prefix)
546
+ 'common.save': 'Save',
547
+ 'common.cancel': 'Cancel',
548
+ 'common.delete': 'Delete',
549
+ 'common.edit': 'Edit',
550
+ 'common.create': 'Create',
551
+ 'common.update': 'Update',
552
+ 'common.add': 'Add',
553
+ 'common.remove': 'Remove',
554
+ 'common.close': 'Close',
555
+ 'common.confirm': 'Confirm',
556
+ 'common.test': 'Test',
557
+ 'common.name': 'Name',
558
+ 'common.description': 'Description',
559
+ 'common.status': 'Status',
560
+ 'common.active': 'Active',
561
+ 'common.inactive': 'Inactive',
562
+ 'common.default': 'Default',
563
+ 'common.type': 'Type',
564
+ 'common.created': 'Created',
565
+ 'common.updated': 'Updated',
566
+ 'common.actions': 'Actions',
567
+ 'common.company': 'Company',
568
+ 'common.success': 'Success',
569
+ 'common.error': 'Error',
570
+ 'common.validation': 'Validation',
571
+ 'common.fill.required.fields': 'Please fill in all required fields',
572
+ // File uploader
573
+ 'shared.file.uploader.no.upload.function': 'No upload function available. Configure FILE_PROVIDER or provide uploadFile input.',
574
+ };
575
+
576
+ // ─── Constants ────────────────────────────────────────────────────────────────
185
577
  const FILE_TYPE_FILTERS = {
186
578
  IMAGES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
187
579
  DOCUMENTS: [
@@ -195,30 +587,15 @@ const FILE_TYPE_FILTERS = {
195
587
  AUDIO: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm'],
196
588
  ALL: [],
197
589
  };
198
- /**
199
- * Get accept string for file input from content types
200
- */
590
+ // ─── Utility Functions ────────────────────────────────────────────────────────
201
591
  function getAcceptString(contentTypes) {
202
- if (!contentTypes.length)
203
- return '*/*';
204
- return contentTypes.join(',');
592
+ return contentTypes.length ? contentTypes.join(',') : '*/*';
205
593
  }
206
- /**
207
- * Check if file matches allowed content types
208
- */
209
594
  function isFileTypeAllowed(file, allowedTypes) {
210
595
  if (!allowedTypes.length)
211
596
  return true;
212
- return allowedTypes.some((type) => {
213
- if (type.endsWith('/*')) {
214
- return file.type.startsWith(type.replace('/*', '/'));
215
- }
216
- return file.type === type;
217
- });
597
+ return allowedTypes.some((type) => type.endsWith('/*') ? file.type.startsWith(type.replace('/*', '/')) : file.type === type);
218
598
  }
219
- /**
220
- * Get file icon class based on content type
221
- */
222
599
  function getFileIconClass(contentType) {
223
600
  if (contentType.startsWith('image/'))
224
601
  return 'pi pi-image';
@@ -234,9 +611,6 @@ function getFileIconClass(contentType) {
234
611
  return 'pi pi-file-excel';
235
612
  return 'pi pi-file';
236
613
  }
237
- /**
238
- * Format file size for display
239
- */
240
614
  function formatFileSize(sizeInKb) {
241
615
  const kb = typeof sizeInKb === 'string' ? parseFloat(sizeInKb) : sizeInKb;
242
616
  if (kb < 1024)
@@ -244,8 +618,7 @@ function formatFileSize(sizeInKb) {
244
618
  const mb = kb / 1024;
245
619
  if (mb < 1024)
246
620
  return `${mb.toFixed(1)} MB`;
247
- const gb = mb / 1024;
248
- return `${gb.toFixed(2)} GB`;
621
+ return `${(mb / 1024).toFixed(2)} GB`;
249
622
  }
250
623
 
251
624
  var ContactTypeEnum;
@@ -720,6 +1093,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
720
1093
  }]
721
1094
  }], 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"] }] } });
722
1095
 
1096
+ class TranslatePipe {
1097
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1098
+ fallbackMessages = inject(FALLBACK_MESSAGES_REGISTRY, { optional: true });
1099
+ transform(key, params) {
1100
+ if (!this.translateAdapter) {
1101
+ return this.getFromFallback(key, params);
1102
+ }
1103
+ // Read languageCode signal to create reactive dependency for zoneless mode
1104
+ // This ensures the pipe re-evaluates when language changes
1105
+ this.translateAdapter.languageCode();
1106
+ return this.translateAdapter.translate(key, params);
1107
+ }
1108
+ getFromFallback(template, params) {
1109
+ // Try to get from fallback messages registry first
1110
+ if (this.fallbackMessages) {
1111
+ const value = this.fallbackMessages[template];
1112
+ if (value) {
1113
+ return this.interpolate(value, params);
1114
+ }
1115
+ }
1116
+ // Fall back to interpolating the key itself
1117
+ return this.interpolate(template, params);
1118
+ }
1119
+ interpolate(template, params) {
1120
+ if (!params)
1121
+ return template;
1122
+ return template.replace(/\{\{(\w+)\}\}/g, (match, paramKey) => {
1123
+ const value = params[paramKey];
1124
+ return value !== undefined ? String(value) : match;
1125
+ });
1126
+ }
1127
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1128
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: TranslatePipe, isStandalone: true, name: "translate", pure: false });
1129
+ }
1130
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TranslatePipe, decorators: [{
1131
+ type: Pipe,
1132
+ args: [{
1133
+ name: 'translate',
1134
+ pure: false,
1135
+ }]
1136
+ }] });
1137
+
1138
+ async function tryLoadLocalizationServices(injector) {
1139
+ try {
1140
+ // @ts-ignore - ng-localization is optional and loaded dynamically at runtime
1141
+ const locModule = await import('@flusys/ng-localization');
1142
+ const config = injector.get(locModule.LOCALIZATION_CONFIG, null);
1143
+ if (!config)
1144
+ return null;
1145
+ return {
1146
+ stateService: injector.get(locModule.LocalizationStateService),
1147
+ apiService: injector.get(locModule.LocalizationApiService),
1148
+ };
1149
+ }
1150
+ catch {
1151
+ return null;
1152
+ }
1153
+ }
1154
+ function resolveTranslationModule(config) {
1155
+ return async () => {
1156
+ const injector = inject(Injector);
1157
+ const { modules, fallbackMessages } = config;
1158
+ // Try to load localization services if available
1159
+ const locServices = await tryLoadLocalizationServices(injector);
1160
+ // Register fallback messages
1161
+ if (fallbackMessages) {
1162
+ if (locServices) {
1163
+ modules.forEach((module) => {
1164
+ if (!locServices.stateService.hasModuleFallbacks(module)) {
1165
+ locServices.stateService.registerModuleFallbacks(module, fallbackMessages);
1166
+ }
1167
+ });
1168
+ }
1169
+ else {
1170
+ const registry = injector.get(FALLBACK_MESSAGES_REGISTRY, null);
1171
+ if (registry) {
1172
+ Object.assign(registry, fallbackMessages);
1173
+ }
1174
+ }
1175
+ }
1176
+ if (!locServices)
1177
+ return true;
1178
+ // Load translations from API
1179
+ const languageCode = locServices.stateService.currentLanguageCode();
1180
+ const modulesToLoad = modules.filter((m) => !locServices.stateService.isModuleLoaded(m));
1181
+ if (modulesToLoad.length === 0)
1182
+ return true;
1183
+ try {
1184
+ const response = (await firstValueFrom(locServices.apiService.getTranslationsByLanguage(languageCode, modulesToLoad)));
1185
+ if (response?.success && response?.data) {
1186
+ locServices.stateService.mergeTranslations(response.data);
1187
+ modulesToLoad.forEach((m) => locServices.stateService.markModuleLoaded(m));
1188
+ }
1189
+ else if (modulesToLoad.length > 0 && isDevMode()) {
1190
+ console.warn(`No translation data received for modules: ${modulesToLoad.join(', ')}`);
1191
+ }
1192
+ }
1193
+ catch (error) {
1194
+ if (isDevMode()) {
1195
+ console.error(`Failed to load translations for modules: ${modules.join(', ')}`, error);
1196
+ }
1197
+ }
1198
+ return true;
1199
+ };
1200
+ }
1201
+
1202
+ /** Fallback translate adapter for when localization provider is not used */
1203
+ const createFallbackTranslateAdapter = (fallbackRegistry) => ({
1204
+ languageCode: signal('en'),
1205
+ translate: (key, params) => {
1206
+ // Check fallback messages registry first
1207
+ const value = fallbackRegistry[key];
1208
+ if (value) {
1209
+ // Simple parameter interpolation
1210
+ if (params) {
1211
+ return value.replace(/\{\{(\w+)\}\}/g, (match, paramKey) => {
1212
+ const paramValue = params[paramKey];
1213
+ return paramValue !== undefined ? String(paramValue) : match;
1214
+ });
1215
+ }
1216
+ return value;
1217
+ }
1218
+ // Fall back to returning the key
1219
+ return key;
1220
+ },
1221
+ });
1222
+ /**
1223
+ * Provide fallback localization when @flusys/ng-localization is not used.
1224
+ * Uses hardcoded fallback messages registered by route resolvers.
1225
+ *
1226
+ * @example
1227
+ * ```typescript
1228
+ * providers: [
1229
+ * ...provideFallbackLocalization(),
1230
+ * ]
1231
+ * ```
1232
+ */
1233
+ function provideFallbackLocalization() {
1234
+ return [
1235
+ // Fallback messages registry (populated by route resolvers)
1236
+ { provide: FALLBACK_MESSAGES_REGISTRY, useValue: {} },
1237
+ // Fallback translate adapter (reads from registry)
1238
+ {
1239
+ provide: TRANSLATE_ADAPTER,
1240
+ useFactory: createFallbackTranslateAdapter,
1241
+ deps: [FALLBACK_MESSAGES_REGISTRY],
1242
+ },
1243
+ ];
1244
+ }
1245
+
723
1246
  class AngularModule {
724
1247
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
725
1248
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, imports: [CommonModule,
@@ -814,9 +1337,12 @@ class PrimeModule {
814
1337
  TagModule,
815
1338
  TextareaModule,
816
1339
  ToastModule,
1340
+ ToggleButtonModule,
817
1341
  ToggleSwitchModule,
818
1342
  TooltipModule,
819
- TreeTableModule] });
1343
+ TreeTableModule,
1344
+ ProgressSpinnerModule,
1345
+ ColorPickerModule] });
820
1346
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
821
1347
  AvatarModule,
822
1348
  ButtonModule,
@@ -851,9 +1377,12 @@ class PrimeModule {
851
1377
  TagModule,
852
1378
  TextareaModule,
853
1379
  ToastModule,
1380
+ ToggleButtonModule,
854
1381
  ToggleSwitchModule,
855
1382
  TooltipModule,
856
- TreeTableModule] });
1383
+ TreeTableModule,
1384
+ ProgressSpinnerModule,
1385
+ ColorPickerModule] });
857
1386
  }
858
1387
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: PrimeModule, decorators: [{
859
1388
  type: NgModule,
@@ -893,64 +1422,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
893
1422
  TagModule,
894
1423
  TextareaModule,
895
1424
  ToastModule,
1425
+ ToggleButtonModule,
896
1426
  ToggleSwitchModule,
897
1427
  TooltipModule,
898
1428
  TreeTableModule,
1429
+ ProgressSpinnerModule,
1430
+ ColorPickerModule,
899
1431
  ],
900
1432
  }]
901
1433
  }] });
902
1434
 
903
- // =============================================================================
904
- // API Resource Service - Signal-based CRUD with Angular Resource API
905
- // =============================================================================
906
1435
  /**
907
1436
  * Abstract base class for API services using Angular 21 resource() API.
908
1437
  * Provides signal-based reactive data fetching with automatic loading states.
909
- * Response types match FLUSYS_NEST backend DTOs.
910
- *
911
- * ## Endpoint Mapping
912
- *
913
- * All endpoints use POST method (RPC-style API):
914
- * - `POST /{resource}/insert` - Create single item
915
- * - `POST /{resource}/insert-many` - Create multiple items
916
- * - `POST /{resource}/get/:id` - Get single item by ID
917
- * - `POST /{resource}/get-all?q=` - List with pagination/filter
918
- * - `POST /{resource}/update` - Update single item
919
- * - `POST /{resource}/update-many` - Update multiple items
920
- * - `POST /{resource}/delete` - Delete/restore/permanent delete
921
1438
  *
922
1439
  * @example
923
1440
  * ```typescript
924
- * // Define service with global apiBaseUrl
925
1441
  * @Injectable({ providedIn: 'root' })
926
1442
  * export class UserService extends ApiResourceService<UserDto, User> {
927
1443
  * constructor() {
928
1444
  * super('auth/users', inject(HttpClient));
929
1445
  * }
930
1446
  * }
931
- *
932
- * // Define service with feature-specific baseUrl
933
- * @Injectable({ providedIn: 'root' })
934
- * export class FormService extends ApiResourceService<FormDto, Form> {
935
- * constructor() {
936
- * super('form', inject(HttpClient), 'formBuilder');
937
- * // URL: services.formBuilder.baseUrl + '/form'
938
- * }
939
- * }
940
- *
941
- * // In component - use signals
942
- * userService = inject(UserService);
943
- * users = this.userService.data; // Signal<User[]>
944
- * isLoading = this.userService.isLoading; // Signal<boolean>
945
- * total = this.userService.total; // Signal<number>
946
- *
947
- * // Trigger fetch
948
- * this.userService.fetchList('search', { pagination: { currentPage: 0, pageSize: 10 } });
949
- *
950
- * // CRUD operations
951
- * await this.userService.insertAsync({ name: 'John', email: 'john@example.com' });
952
- * await this.userService.updateAsync({ id: '123', name: 'John Updated' });
953
- * await this.userService.deleteAsync({ id: '123', type: 'delete' });
954
1447
  * ```
955
1448
  */
956
1449
  class ApiResourceService {
@@ -958,43 +1451,22 @@ class ApiResourceService {
958
1451
  injector = inject(Injector);
959
1452
  http;
960
1453
  moduleApiName;
961
- // ==========================================================================
962
- // State Signals for List Queries
963
- // ==========================================================================
964
- /** Current search term */
965
1454
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
966
- /** Current filter and pagination state */
967
1455
  filterData = signal({
968
1456
  pagination: { currentPage: 0, pageSize: 10 },
969
1457
  filter: {},
970
1458
  select: [],
971
1459
  sort: {},
972
1460
  }, ...(ngDevMode ? [{ debugName: "filterData" }] : []));
973
- /**
974
- * Resource for list data - lazy initialized to prevent auto-fetch on service injection.
975
- * Call initListResource() or any list method (fetchList, reload, etc.) to initialize.
976
- */
977
1461
  _listResource = null;
978
- /** Whether the list resource has been initialized */
979
1462
  _resourceInitialized = false;
980
- /**
981
- * Signal to track resource initialization for computed signals.
982
- * This allows computed signals to re-evaluate when the resource is created.
983
- * Without this, computed signals would not detect when _listResource changes from null.
984
- */
985
1463
  _resourceInitSignal = signal(false, ...(ngDevMode ? [{ debugName: "_resourceInitSignal" }] : []));
986
- /** Get or create the list resource (lazy initialization) */
987
1464
  get listResource() {
988
1465
  if (!this._listResource) {
989
1466
  this.initListResource();
990
1467
  }
991
1468
  return this._listResource;
992
1469
  }
993
- /**
994
- * Initialize the list resource. Called automatically when accessing listResource
995
- * or any list-related computed signals/methods.
996
- * Uses runInInjectionContext to support lazy initialization outside constructor.
997
- */
998
1470
  initListResource() {
999
1471
  if (this._resourceInitialized)
1000
1472
  return;
@@ -1004,88 +1476,49 @@ class ApiResourceService {
1004
1476
  search: this.searchTerm(),
1005
1477
  filter: this.filterData(),
1006
1478
  }),
1007
- loader: async ({ params }) => {
1008
- const { search, filter } = params;
1009
- return this.fetchAllAsync(search, filter);
1010
- } });
1479
+ loader: async ({ params }) => this.getAllAsync(params.filter, params.search) });
1011
1480
  });
1012
- // Signal that resource is now initialized - triggers computed re-evaluation
1013
1481
  this._resourceInitSignal.set(true);
1014
1482
  }
1015
- // ==========================================================================
1016
- // Computed State Accessors
1017
- // ==========================================================================
1018
- /**
1019
- * Whether data is currently loading.
1020
- * Tracks _resourceInitSignal to re-evaluate when resource is created.
1021
- */
1022
1483
  isLoading = computed(() => {
1023
- this._resourceInitSignal(); // Track initialization
1484
+ this._resourceInitSignal();
1024
1485
  return this._listResource?.isLoading() ?? false;
1025
1486
  }, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1026
- /**
1027
- * List data array.
1028
- * Tracks _resourceInitSignal to re-evaluate when resource is created.
1029
- */
1030
1487
  data = computed(() => {
1031
- this._resourceInitSignal(); // Track initialization
1488
+ this._resourceInitSignal();
1032
1489
  return this._listResource?.value()?.data ?? [];
1033
1490
  }, ...(ngDevMode ? [{ debugName: "data" }] : []));
1034
- /**
1035
- * Total count of items.
1036
- * Tracks _resourceInitSignal to re-evaluate when resource is created.
1037
- */
1038
1491
  total = computed(() => {
1039
- this._resourceInitSignal(); // Track initialization
1492
+ this._resourceInitSignal();
1040
1493
  return this._listResource?.value()?.meta?.total ?? 0;
1041
1494
  }, ...(ngDevMode ? [{ debugName: "total" }] : []));
1042
- /**
1043
- * Pagination metadata.
1044
- * Tracks _resourceInitSignal to re-evaluate when resource is created.
1045
- */
1046
1495
  pageInfo = computed(() => {
1047
- this._resourceInitSignal(); // Track initialization
1496
+ this._resourceInitSignal();
1048
1497
  return this._listResource?.value()?.meta;
1049
1498
  }, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
1050
- /**
1051
- * Whether there are more pages.
1052
- * Tracks _resourceInitSignal to re-evaluate when resource is created.
1053
- */
1054
1499
  hasMore = computed(() => {
1055
- this._resourceInitSignal(); // Track initialization
1500
+ this._resourceInitSignal();
1056
1501
  const meta = this._listResource?.value()?.meta;
1057
1502
  if (!meta)
1058
1503
  return false;
1059
1504
  return meta.hasMore ?? (meta.page + 1) * meta.pageSize < meta.total;
1060
1505
  }, ...(ngDevMode ? [{ debugName: "hasMore" }] : []));
1061
- /**
1062
- * @param moduleApiName - The API resource path (e.g., 'form' for /form-builder/form)
1063
- * @param http - HttpClient instance
1064
- * @param serviceName - Optional service name for feature-specific base URL (e.g., 'formBuilder')
1065
- */
1066
1506
  constructor(moduleApiName, http, serviceName) {
1067
- this.moduleApiName = moduleApiName;
1507
+ this.moduleApiName = moduleApiName || serviceName || 'api';
1068
1508
  const config = inject(APP_CONFIG);
1069
- // Use service-specific URL if provided, otherwise fallback to global apiBaseUrl
1070
1509
  const serviceBaseUrl = serviceName ? getServiceUrl(config, serviceName) : config.apiBaseUrl;
1071
- this.baseUrl = `${serviceBaseUrl}/${moduleApiName}`;
1510
+ this.baseUrl = moduleApiName ? `${serviceBaseUrl}/${moduleApiName}` : serviceBaseUrl;
1072
1511
  this.http = http;
1073
- // Resource is now lazy-initialized, not created in constructor
1074
1512
  }
1075
1513
  getHttpOptions(endpoint, params) {
1076
1514
  return {
1077
- headers: new HttpHeaders({
1078
- 'x-loader-tag': `${this.moduleApiName}/${endpoint}`,
1079
- }),
1515
+ headers: new HttpHeaders({ 'x-loader-tag': `${this.moduleApiName}/${endpoint}` }),
1080
1516
  ...(params ? { params } : {}),
1081
1517
  };
1082
1518
  }
1083
1519
  // ==========================================================================
1084
- // List Management Methods
1520
+ // List Management
1085
1521
  // ==========================================================================
1086
- /**
1087
- * Fetch list data (triggers resource initialization and reload)
1088
- */
1089
1522
  fetchList(search = '', filter) {
1090
1523
  this.initListResource();
1091
1524
  this.searchTerm.set(search);
@@ -1093,16 +1526,10 @@ class ApiResourceService {
1093
1526
  this.filterData.update((prev) => ({ ...prev, ...filter }));
1094
1527
  }
1095
1528
  }
1096
- /**
1097
- * Update pagination
1098
- */
1099
1529
  setPagination(pagination) {
1100
1530
  this.initListResource();
1101
1531
  this.filterData.update((prev) => ({ ...prev, pagination }));
1102
1532
  }
1103
- /**
1104
- * Go to next page
1105
- */
1106
1533
  nextPage() {
1107
1534
  this.initListResource();
1108
1535
  this.filterData.update((prev) => ({
@@ -1113,9 +1540,6 @@ class ApiResourceService {
1113
1540
  },
1114
1541
  }));
1115
1542
  }
1116
- /**
1117
- * Reset to first page
1118
- */
1119
1543
  resetPagination() {
1120
1544
  this.initListResource();
1121
1545
  this.filterData.update((prev) => ({
@@ -1123,42 +1547,21 @@ class ApiResourceService {
1123
1547
  pagination: { currentPage: 0, pageSize: prev.pagination?.pageSize ?? 10 },
1124
1548
  }));
1125
1549
  }
1126
- /**
1127
- * Reload current data
1128
- */
1129
1550
  reload() {
1130
- if (this._listResource) {
1131
- this._listResource.reload();
1132
- }
1551
+ this._listResource?.reload();
1133
1552
  }
1134
1553
  // ==========================================================================
1135
- // Observable-based API Methods (IApiService interface)
1554
+ // Observable API (IApiService interface)
1136
1555
  // ==========================================================================
1137
- /**
1138
- * Insert single item (Observable)
1139
- * POST /{resource}/insert
1140
- */
1141
1556
  insert(dto) {
1142
1557
  return this.http.post(`${this.baseUrl}/insert`, dto, this.getHttpOptions('insert'));
1143
1558
  }
1144
- /**
1145
- * Insert multiple items (Observable)
1146
- * POST /{resource}/insert-many
1147
- */
1148
1559
  insertMany(dtos) {
1149
1560
  return this.http.post(`${this.baseUrl}/insert-many`, dtos, this.getHttpOptions('insert-many'));
1150
1561
  }
1151
- /**
1152
- * Find single item by ID (Observable)
1153
- * POST /{resource}/get/:id
1154
- */
1155
1562
  findById(id, select) {
1156
1563
  return this.http.post(`${this.baseUrl}/get/${id}`, { select }, this.getHttpOptions(`get/${id}`));
1157
1564
  }
1158
- /**
1159
- * Get all items with pagination (Observable)
1160
- * POST /{resource}/get-all?q=search
1161
- */
1162
1565
  getAll(search, filter) {
1163
1566
  let params = new HttpParams();
1164
1567
  if (search) {
@@ -1166,70 +1569,36 @@ class ApiResourceService {
1166
1569
  }
1167
1570
  return this.http.post(`${this.baseUrl}/get-all`, filter, this.getHttpOptions('get-all', params));
1168
1571
  }
1169
- /**
1170
- * Update single item (Observable)
1171
- * POST /{resource}/update
1172
- */
1173
1572
  update(dto) {
1174
1573
  return this.http.post(`${this.baseUrl}/update`, dto, this.getHttpOptions('update'));
1175
1574
  }
1176
- /**
1177
- * Update multiple items (Observable)
1178
- * POST /{resource}/update-many
1179
- */
1180
1575
  updateMany(dtos) {
1181
1576
  return this.http.post(`${this.baseUrl}/update-many`, dtos, this.getHttpOptions('update-many'));
1182
1577
  }
1183
- /**
1184
- * Delete items (Observable)
1185
- * POST /{resource}/delete
1186
- * @param deleteDto - { id: string | string[], type: 'delete' | 'restore' | 'permanent' }
1187
- */
1188
1578
  delete(deleteDto) {
1189
1579
  return this.http.post(`${this.baseUrl}/delete`, deleteDto, this.getHttpOptions('delete'));
1190
1580
  }
1191
1581
  // ==========================================================================
1192
- // Promise-based API Methods (Async)
1582
+ // Async API
1193
1583
  // ==========================================================================
1194
- /**
1195
- * Fetch paginated list (async)
1196
- */
1197
- async fetchAllAsync(search, filter) {
1584
+ async getAllAsync(filter, search = '') {
1198
1585
  return firstValueFrom(this.getAll(search, filter));
1199
1586
  }
1200
- /**
1201
- * Find single item by ID (async)
1202
- */
1203
1587
  async findByIdAsync(id, select) {
1204
1588
  return firstValueFrom(this.findById(id, select));
1205
1589
  }
1206
- /**
1207
- * Insert single item (async)
1208
- */
1209
1590
  async insertAsync(dto) {
1210
1591
  return firstValueFrom(this.insert(dto));
1211
1592
  }
1212
- /**
1213
- * Insert multiple items (async)
1214
- */
1215
1593
  async insertManyAsync(dtos) {
1216
1594
  return firstValueFrom(this.insertMany(dtos));
1217
1595
  }
1218
- /**
1219
- * Update single item (async)
1220
- */
1221
1596
  async updateAsync(dto) {
1222
1597
  return firstValueFrom(this.update(dto));
1223
1598
  }
1224
- /**
1225
- * Update multiple items (async)
1226
- */
1227
1599
  async updateManyAsync(dtos) {
1228
1600
  return firstValueFrom(this.updateMany(dtos));
1229
1601
  }
1230
- /**
1231
- * Delete items (async)
1232
- */
1233
1602
  async deleteAsync(deleteDto) {
1234
1603
  return firstValueFrom(this.delete(deleteDto));
1235
1604
  }
@@ -1373,14 +1742,13 @@ class IconComponent {
1373
1742
  <i class="pi pi-question"></i>
1374
1743
  }
1375
1744
  }
1376
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1745
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }] });
1377
1746
  }
1378
1747
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IconComponent, decorators: [{
1379
1748
  type: Component,
1380
1749
  args: [{
1381
1750
  selector: 'lib-icon',
1382
1751
  imports: [AngularModule],
1383
- changeDetection: ChangeDetectionStrategy.OnPush,
1384
1752
  template: `
1385
1753
  @if (icon()) {
1386
1754
  @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
@@ -1438,13 +1806,24 @@ function checkScrollPagination(event, config) {
1438
1806
  * - Signal forms: `[formField]="formTree.field"`
1439
1807
  */
1440
1808
  class LazyMultiSelectComponent extends BaseFormControl {
1441
- destroyRef = inject(DestroyRef);
1809
+ document = inject(DOCUMENT$1);
1810
+ appRef = inject(ApplicationRef);
1811
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1442
1812
  onDocumentClickBound = this.handleDocumentClick.bind(this);
1443
- isDestroyed = false;
1444
1813
  // View references
1445
1814
  pSelectRef = viewChild.required('pSelect');
1815
+ overlayTemplate = viewChild.required('overlayTpl');
1816
+ // Portal state
1817
+ overlayViewRef = null;
1446
1818
  // Inputs
1447
- placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1819
+ placeHolder = input('', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1820
+ // Computed placeholder with translation fallback
1821
+ displayPlaceholder = computed(() => {
1822
+ const customPlaceholder = this.placeHolder();
1823
+ if (customPlaceholder)
1824
+ return customPlaceholder;
1825
+ return this.t('shared.multi.select.placeholder');
1826
+ }, ...(ngDevMode ? [{ debugName: "displayPlaceholder" }] : []));
1448
1827
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1449
1828
  isLoading = input.required(...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1450
1829
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
@@ -1464,7 +1843,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1464
1843
  if (selectedValues.length === 0)
1465
1844
  return '';
1466
1845
  if (selectedValues.length > 3) {
1467
- return `${selectedValues.length} Items Selected`;
1846
+ return this.t('shared.multi.select.items.selected', { count: selectedValues.length });
1468
1847
  }
1469
1848
  return this.selectDataList()
1470
1849
  .filter((item) => selectedValues.includes(item.value))
@@ -1503,16 +1882,16 @@ class LazyMultiSelectComponent extends BaseFormControl {
1503
1882
  });
1504
1883
  });
1505
1884
  // Document click listener for closing dropdown
1506
- this.destroyRef.onDestroy(() => {
1507
- this.isDestroyed = true;
1508
- document.removeEventListener('click', this.onDocumentClickBound);
1509
- });
1510
- afterNextRender(() => {
1511
- if (!this.isDestroyed) {
1512
- document.addEventListener('click', this.onDocumentClickBound);
1513
- }
1885
+ afterNextRender({
1886
+ write: () => {
1887
+ this.document.addEventListener('click', this.onDocumentClickBound);
1888
+ },
1514
1889
  });
1515
1890
  }
1891
+ ngOnDestroy() {
1892
+ this.document.removeEventListener('click', this.onDocumentClickBound);
1893
+ this.closeOverlay();
1894
+ }
1516
1895
  onScroll(event) {
1517
1896
  const nextPagination = checkScrollPagination(event, {
1518
1897
  pagination: this.pagination(),
@@ -1523,29 +1902,62 @@ class LazyMultiSelectComponent extends BaseFormControl {
1523
1902
  this.onPagination.emit(nextPagination);
1524
1903
  }
1525
1904
  }
1526
- onSelectClick(event) {
1527
- if (this.disabled())
1905
+ onSelectClick() {
1906
+ if (this.disabled() || !this.isEditMode())
1528
1907
  return;
1908
+ if (this.openOptions()) {
1909
+ this.closeOverlay();
1910
+ }
1911
+ else {
1912
+ this.openOverlay();
1913
+ }
1914
+ }
1915
+ openOverlay() {
1529
1916
  this.pSelectRef()?.nativeElement.classList.add('p-focus');
1530
- this.openOptions.update((isOpen) => !isOpen);
1917
+ this.openOptions.set(true);
1918
+ // Create embedded view and append to body (portal pattern)
1919
+ this.overlayViewRef = this.overlayTemplate().createEmbeddedView({});
1920
+ this.appRef.attachView(this.overlayViewRef);
1921
+ const overlayEl = this.overlayViewRef.rootNodes[0];
1922
+ this.document.body.appendChild(overlayEl);
1923
+ // Override positioning for portal (CSS class handles visual styles)
1924
+ const rect = this.pSelectRef()?.nativeElement.getBoundingClientRect();
1925
+ if (rect) {
1926
+ overlayEl.style.cssText = `
1927
+ position: fixed !important;
1928
+ top: ${rect.bottom + 2}px !important;
1929
+ left: ${rect.left}px !important;
1930
+ width: ${rect.width}px !important;
1931
+ max-width: ${rect.width}px !important;
1932
+ min-width: ${rect.width}px !important;
1933
+ z-index: 999999999999 !important;
1934
+ `;
1935
+ }
1936
+ }
1937
+ closeOverlay() {
1938
+ this.openOptions.set(false);
1939
+ this.pSelectRef()?.nativeElement.classList.remove('p-focus');
1940
+ if (this.overlayViewRef) {
1941
+ this.appRef.detachView(this.overlayViewRef);
1942
+ this.overlayViewRef.destroy();
1943
+ this.overlayViewRef = null;
1944
+ }
1531
1945
  }
1532
1946
  onOverlayClick(event) {
1533
1947
  event.stopPropagation();
1534
1948
  }
1535
1949
  handleDocumentClick(event) {
1536
- const clickedInside = this.pSelectRef()?.nativeElement.contains(event.target);
1537
- if (!clickedInside) {
1538
- this.openOptions.set(false);
1539
- this.pSelectRef()?.nativeElement.classList.remove('p-focus');
1950
+ const target = event.target;
1951
+ const clickedInSelect = this.pSelectRef()?.nativeElement.contains(target);
1952
+ const clickedInOverlay = this.overlayViewRef?.rootNodes[0]?.contains(target);
1953
+ if (!clickedInSelect && !clickedInOverlay) {
1954
+ this.closeOverlay();
1540
1955
  this.markAsTouched();
1541
1956
  }
1542
1957
  }
1543
1958
  isSelected(data) {
1544
1959
  return this.value()?.includes(data.value) ?? false;
1545
1960
  }
1546
- key(option) {
1547
- return option.value;
1548
- }
1549
1961
  selectValue(event, option) {
1550
1962
  const previousValue = this.value() ?? [];
1551
1963
  if (event.checked) {
@@ -1569,13 +1981,19 @@ class LazyMultiSelectComponent extends BaseFormControl {
1569
1981
  event.stopPropagation();
1570
1982
  this.value.set([]);
1571
1983
  }
1984
+ t(key, variables) {
1985
+ if (this.translateAdapter) {
1986
+ return this.translateAdapter.translate(key, variables);
1987
+ }
1988
+ return key;
1989
+ }
1572
1990
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1573
- 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 });
1991
+ 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 }, { propertyName: "overlayTemplate", first: true, predicate: ["overlayTpl"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"p-select w-full\"\n #pSelect\n (click)=\"onSelectClick()\"\n [class.p-disabled]=\"disabled()\"\n [class.edit-mode-element-css]=\"!isEditMode()\"\n [class.overflow-hidden]=\"!isEditMode()\"\n>\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\" [class.edit-mode-element-css]=\"!isEditMode()\">\n {{ selectedValueDisplay() }}\n </span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ displayPlaceholder() }}</span>\n }\n\n @if (isEditMode()) {\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\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 14 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\"\n aria-hidden=\"true\"\n >\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\"\n />\n </svg>\n </span>\n </div>\n }\n</div>\n\n<ng-template #overlayTpl>\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]=\"'shared.search' | translate\"\n [ngModel]=\"searchTerm()\"\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 data.value) {\n <li\n class=\"p-select-option flex flex-row gap-2 items-center\"\n [class.p-select-option-selected]=\"isSelected(data)\"\n >\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</ng-template>\n", styles: [".p-select-overlay{display:flex;flex-direction:column;max-height:250px;overflow:hidden}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-border);background:var(--p-content-hover-background);flex-shrink:0;width:100%;box-sizing:border-box}.p-select-header input{background:var(--p-form-field-background);border-color:var(--p-form-field-border-color);color:var(--p-text-color)}.p-select-header input::placeholder{color:var(--p-text-muted-color)}.p-select-list-container{flex:1;overflow-y:auto;min-height:0;width:100%}.p-select-list{margin:0;padding:.25rem 0;list-style:none;background:var(--p-surface-overlay);color:var(--p-text-color);width:100%}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease;color:var(--p-text-color)}.p-select-option:hover{background:var(--p-content-hover-background)}.p-select-option.p-select-option-selected{background:var(--p-highlight-background);color:var(--p-highlight-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-highlight-focus-background);color:var(--p-highlight-focus-color)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-muted-color);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: 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: "pipe", type: TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
1574
1992
  }
1575
1993
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1576
1994
  type: Component,
1577
- 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"] }]
1578
- }], 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"] }] } });
1995
+ args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule, TranslatePipe], encapsulation: ViewEncapsulation.None, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div\n class=\"p-select w-full\"\n #pSelect\n (click)=\"onSelectClick()\"\n [class.p-disabled]=\"disabled()\"\n [class.edit-mode-element-css]=\"!isEditMode()\"\n [class.overflow-hidden]=\"!isEditMode()\"\n>\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\" [class.edit-mode-element-css]=\"!isEditMode()\">\n {{ selectedValueDisplay() }}\n </span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ displayPlaceholder() }}</span>\n }\n\n @if (isEditMode()) {\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\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 14 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\"\n aria-hidden=\"true\"\n >\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\"\n />\n </svg>\n </span>\n </div>\n }\n</div>\n\n<ng-template #overlayTpl>\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]=\"'shared.search' | translate\"\n [ngModel]=\"searchTerm()\"\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 data.value) {\n <li\n class=\"p-select-option flex flex-row gap-2 items-center\"\n [class.p-select-option-selected]=\"isSelected(data)\"\n >\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</ng-template>\n", styles: [".p-select-overlay{display:flex;flex-direction:column;max-height:250px;overflow:hidden}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-border);background:var(--p-content-hover-background);flex-shrink:0;width:100%;box-sizing:border-box}.p-select-header input{background:var(--p-form-field-background);border-color:var(--p-form-field-border-color);color:var(--p-text-color)}.p-select-header input::placeholder{color:var(--p-text-muted-color)}.p-select-list-container{flex:1;overflow-y:auto;min-height:0;width:100%}.p-select-list{margin:0;padding:.25rem 0;list-style:none;background:var(--p-surface-overlay);color:var(--p-text-color);width:100%}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease;color:var(--p-text-color)}.p-select-option:hover{background:var(--p-content-hover-background)}.p-select-option.p-select-option-selected{background:var(--p-highlight-background);color:var(--p-highlight-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-highlight-focus-background);color:var(--p-highlight-focus-color)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-muted-color);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"] }]
1996
+ }], ctorParameters: () => [], propDecorators: { pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], overlayTemplate: [{ type: i0.ViewChild, args: ['overlayTpl', { 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"] }] } });
1579
1997
 
1580
1998
  /**
1581
1999
  * Lazy-loading single select component with search and pagination.
@@ -1587,13 +2005,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1587
2005
  */
1588
2006
  class LazySelectComponent extends BaseFormControl {
1589
2007
  destroyRef = inject(DestroyRef);
2008
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1590
2009
  onScrollBound = this.onScroll.bind(this);
1591
2010
  scrollTargetEl = null;
1592
2011
  isDestroyed = false;
1593
2012
  // View references
1594
2013
  scrollContainer = viewChild.required('scrollContainer');
1595
2014
  // Inputs
1596
- placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
2015
+ placeHolder = input('', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
2016
+ // Computed placeholder with translation fallback
2017
+ displayPlaceholder = computed(() => {
2018
+ const customPlaceholder = this.placeHolder();
2019
+ if (customPlaceholder)
2020
+ return customPlaceholder;
2021
+ return this.t('shared.select.placeholder');
2022
+ }, ...(ngDevMode ? [{ debugName: "displayPlaceholder" }] : []));
1597
2023
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
1598
2024
  optionValue = input.required(...(ngDevMode ? [{ debugName: "optionValue" }] : []));
1599
2025
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1676,12 +2102,18 @@ class LazySelectComponent extends BaseFormControl {
1676
2102
  onBlur() {
1677
2103
  this.markAsTouched();
1678
2104
  }
2105
+ t(key, variables) {
2106
+ if (this.translateAdapter) {
2107
+ return this.translateAdapter.translate(key, variables);
2108
+ }
2109
+ return key;
2110
+ }
1679
2111
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1680
- 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 });
2112
+ 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]=\"displayPlaceholder()\"\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.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"] }] });
1681
2113
  }
1682
2114
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, decorators: [{
1683
2115
  type: Component,
1684
- 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" }]
2116
+ args: [{ selector: 'lib-lazy-select', imports: [AngularModule, PrimeModule, EditModeElementChangerDirective], 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]=\"displayPlaceholder()\"\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" }]
1685
2117
  }], 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"] }] } });
1686
2118
 
1687
2119
  /**
@@ -1757,14 +2189,6 @@ const AUTH_STATE_PROVIDER = new InjectionToken('AUTH_STATE_PROVIDER', {
1757
2189
  * Use with `inject(PROFILE_PERMISSION_PROVIDER, { optional: true })`.
1758
2190
  */
1759
2191
  const PROFILE_PERMISSION_PROVIDER = new InjectionToken('PROFILE_PERMISSION_PROVIDER');
1760
- /**
1761
- * Profile Upload Provider Token
1762
- *
1763
- * Provides file upload functionality for profile pictures.
1764
- * Optional - if not configured or storage not enabled, upload section is hidden.
1765
- * Use with `inject(PROFILE_UPLOAD_PROVIDER, { optional: true })`.
1766
- */
1767
- const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1768
2192
  /**
1769
2193
  * User List Provider Token
1770
2194
  *
@@ -1779,6 +2203,20 @@ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1779
2203
  * ]
1780
2204
  */
1781
2205
  const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
2206
+ /**
2207
+ * File Provider Token
2208
+ *
2209
+ * Provides file loading and upload functionality for file selectors.
2210
+ * Optional - if not configured, file selector requires loadFiles/uploadFile inputs.
2211
+ * Use with `inject(FILE_PROVIDER, { optional: true })`.
2212
+ *
2213
+ * @example
2214
+ * // In app.config.ts
2215
+ * providers: [
2216
+ * { provide: FILE_PROVIDER, useClass: StorageFileProvider },
2217
+ * ]
2218
+ */
2219
+ const FILE_PROVIDER = new InjectionToken('FILE_PROVIDER');
1782
2220
 
1783
2221
  const DEFAULT_PAGE_SIZE$1 = 20;
1784
2222
  /**
@@ -1792,10 +2230,11 @@ class BaseUserSelectComponent {
1792
2230
  destroyRef = inject(DestroyRef);
1793
2231
  injector = inject(Injector);
1794
2232
  userProvider = inject(USER_PROVIDER);
2233
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1795
2234
  abortController = null;
1796
2235
  // Inputs
1797
2236
  loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1798
- placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
2237
+ placeHolder = input('', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1799
2238
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1800
2239
  filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1801
2240
  additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
@@ -1813,6 +2252,13 @@ class BaseUserSelectComponent {
1813
2252
  label: user.name || user.email,
1814
2253
  value: user.id,
1815
2254
  })), ...(ngDevMode ? [{ debugName: "dropdownUsers" }] : []));
2255
+ // Computed placeholder with translation fallback
2256
+ displayPlaceholder = computed(() => {
2257
+ const customPlaceholder = this.placeHolder();
2258
+ if (customPlaceholder)
2259
+ return customPlaceholder;
2260
+ return this.t('shared.user.select.placeholder');
2261
+ }, ...(ngDevMode ? [{ debugName: "displayPlaceholder" }] : []));
1816
2262
  constructor() {
1817
2263
  // Cleanup on destroy
1818
2264
  this.destroyRef.onDestroy(() => {
@@ -1905,6 +2351,12 @@ class BaseUserSelectComponent {
1905
2351
  })),
1906
2352
  })));
1907
2353
  }
2354
+ t(key, variables) {
2355
+ if (this.translateAdapter) {
2356
+ return this.translateAdapter.translate(key, variables);
2357
+ }
2358
+ return key;
2359
+ }
1908
2360
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseUserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1909
2361
  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 });
1910
2362
  }
@@ -1964,7 +2416,7 @@ class UserSelectComponent extends BaseUserSelectComponent {
1964
2416
  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: `
1965
2417
  <lib-lazy-select
1966
2418
  [(value)]="value"
1967
- [placeHolder]="placeHolder()"
2419
+ [placeHolder]="displayPlaceholder()"
1968
2420
  [optionLabel]="'label'"
1969
2421
  [optionValue]="'value'"
1970
2422
  [isEditMode]="isEditMode()"
@@ -1975,7 +2427,7 @@ class UserSelectComponent extends BaseUserSelectComponent {
1975
2427
  (onSearch)="handleSearch($event)"
1976
2428
  (onPagination)="handlePagination($event)"
1977
2429
  />
1978
- `, 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 });
2430
+ `, 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"] }] });
1979
2431
  }
1980
2432
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserSelectComponent, decorators: [{
1981
2433
  type: Component,
@@ -1985,7 +2437,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1985
2437
  template: `
1986
2438
  <lib-lazy-select
1987
2439
  [(value)]="value"
1988
- [placeHolder]="placeHolder()"
2440
+ [placeHolder]="displayPlaceholder()"
1989
2441
  [optionLabel]="'label'"
1990
2442
  [optionValue]="'value'"
1991
2443
  [isEditMode]="isEditMode()"
@@ -1997,7 +2449,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1997
2449
  (onPagination)="handlePagination($event)"
1998
2450
  />
1999
2451
  `,
2000
- changeDetection: ChangeDetectionStrategy.OnPush,
2001
2452
  }]
2002
2453
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
2003
2454
 
@@ -2049,7 +2500,7 @@ class UserMultiSelectComponent extends BaseUserSelectComponent {
2049
2500
  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: `
2050
2501
  <lib-lazy-multi-select
2051
2502
  [(value)]="value"
2052
- [placeHolder]="placeHolder()"
2503
+ [placeHolder]="displayPlaceholder()"
2053
2504
  [isEditMode]="isEditMode()"
2054
2505
  [isLoading]="isLoading()"
2055
2506
  [total]="total()"
@@ -2058,7 +2509,7 @@ class UserMultiSelectComponent extends BaseUserSelectComponent {
2058
2509
  (onSearch)="handleSearch($event)"
2059
2510
  (onPagination)="handlePagination($event)"
2060
2511
  />
2061
- `, 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 });
2512
+ `, 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"] }] });
2062
2513
  }
2063
2514
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserMultiSelectComponent, decorators: [{
2064
2515
  type: Component,
@@ -2068,7 +2519,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2068
2519
  template: `
2069
2520
  <lib-lazy-multi-select
2070
2521
  [(value)]="value"
2071
- [placeHolder]="placeHolder()"
2522
+ [placeHolder]="displayPlaceholder()"
2072
2523
  [isEditMode]="isEditMode()"
2073
2524
  [isLoading]="isLoading()"
2074
2525
  [total]="total()"
@@ -2078,19 +2529,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2078
2529
  (onPagination)="handlePagination($event)"
2079
2530
  />
2080
2531
  `,
2081
- changeDetection: ChangeDetectionStrategy.OnPush,
2082
2532
  }]
2083
2533
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
2084
2534
 
2085
2535
  /**
2086
2536
  * File Uploader Component - Drag & drop file upload with type filtering.
2087
2537
  *
2088
- * Pass your own `uploadFile` function - works with any storage API.
2538
+ * Uses FILE_PROVIDER when available (no inputs needed).
2539
+ * Optionally pass `uploadFile` to override the provider.
2540
+ *
2541
+ * @example
2542
+ * ```html
2543
+ * <!-- Using FILE_PROVIDER (recommended) -->
2544
+ * <lib-file-uploader
2545
+ * [acceptTypes]="['image/*']"
2546
+ * (fileUploaded)="onFileUploaded($event)"
2547
+ * />
2548
+ *
2549
+ * <!-- Custom upload function -->
2550
+ * <lib-file-uploader
2551
+ * [uploadFile]="customUploadFn"
2552
+ * (fileUploaded)="onFileUploaded($event)"
2553
+ * />
2554
+ * ```
2089
2555
  */
2090
2556
  class FileUploaderComponent {
2091
2557
  messageService = inject(MessageService);
2092
- // Required: function to upload file
2093
- uploadFile = input.required(...(ngDevMode ? [{ debugName: "uploadFile" }] : []));
2558
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
2559
+ fileProvider = inject(FILE_PROVIDER, { optional: true });
2560
+ // Optional: custom upload function (overrides FILE_PROVIDER)
2561
+ uploadFile = input(...(ngDevMode ? [undefined, { debugName: "uploadFile" }] : []));
2562
+ // Optional: function to upload multiple files at once (more efficient)
2563
+ uploadMultipleFiles = input(...(ngDevMode ? [undefined, { debugName: "uploadMultipleFiles" }] : []));
2564
+ // Check if upload capability is available
2565
+ hasUploadCapability = computed(() => !!this.uploadFile() || !!this.fileProvider, ...(ngDevMode ? [{ debugName: "hasUploadCapability" }] : []));
2566
+ // Get effective upload function (input or provider)
2567
+ getUploadFn() {
2568
+ const inputFn = this.uploadFile();
2569
+ if (inputFn)
2570
+ return inputFn;
2571
+ if (this.fileProvider)
2572
+ return (file, opts) => this.fileProvider.uploadFile(file, opts);
2573
+ throw new Error('shared.file.uploader.no.upload.function');
2574
+ }
2575
+ // Get effective multiple upload function
2576
+ getUploadMultipleFn() {
2577
+ const inputFn = this.uploadMultipleFiles();
2578
+ if (inputFn)
2579
+ return inputFn;
2580
+ if (this.fileProvider?.uploadMultipleFiles) {
2581
+ return (files, opts) => this.fileProvider.uploadMultipleFiles(files, opts);
2582
+ }
2583
+ return undefined;
2584
+ }
2094
2585
  // Inputs
2095
2586
  acceptTypes = input([], ...(ngDevMode ? [{ debugName: "acceptTypes" }] : []));
2096
2587
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
@@ -2120,14 +2611,14 @@ class FileUploaderComponent {
2120
2611
  // Check if types match predefined filters
2121
2612
  const typesStr = JSON.stringify(types);
2122
2613
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.IMAGES))
2123
- return 'Images';
2614
+ return this.t('shared.file.type.images');
2124
2615
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.DOCUMENTS))
2125
- return 'Documents';
2616
+ return this.t('shared.file.type.documents');
2126
2617
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.VIDEOS))
2127
- return 'Videos';
2618
+ return this.t('shared.file.type.videos');
2128
2619
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.AUDIO))
2129
- return 'Audio';
2130
- return types.map(t => t.split('/')[1] || t).join(', ');
2620
+ return this.t('shared.file.type.audio');
2621
+ return types.map((t) => t.split('/')[1] || t).join(', ');
2131
2622
  }, ...(ngDevMode ? [{ debugName: "acceptTypesDisplay" }] : []));
2132
2623
  onDragOver(event) {
2133
2624
  event.preventDefault();
@@ -2164,12 +2655,12 @@ class FileUploaderComponent {
2164
2655
  handleFiles(files) {
2165
2656
  // Filter by type
2166
2657
  const allowedTypes = this.acceptTypes();
2167
- const validFiles = files.filter(file => {
2658
+ const validFiles = files.filter((file) => {
2168
2659
  if (!isFileTypeAllowed(file, allowedTypes)) {
2169
2660
  this.messageService.add({
2170
2661
  severity: 'warn',
2171
- summary: 'Invalid File Type',
2172
- detail: `File type not allowed: ${file.name}`,
2662
+ summary: this.t('shared.upload.invalid.type'),
2663
+ detail: `${file.name}`,
2173
2664
  });
2174
2665
  return false;
2175
2666
  }
@@ -2177,12 +2668,12 @@ class FileUploaderComponent {
2177
2668
  });
2178
2669
  // Filter by size
2179
2670
  const maxSize = this.maxSizeMb() * 1024 * 1024;
2180
- const sizeValidFiles = validFiles.filter(file => {
2671
+ const sizeValidFiles = validFiles.filter((file) => {
2181
2672
  if (file.size > maxSize) {
2182
2673
  this.messageService.add({
2183
2674
  severity: 'warn',
2184
- summary: 'File Too Large',
2185
- detail: `${file.name} exceeds ${this.maxSizeMb()}MB limit`,
2675
+ summary: this.t('shared.upload.file.too.large'),
2676
+ detail: `${file.name} (${this.maxSizeMb()}${this.t('shared.units.mb')})`,
2186
2677
  });
2187
2678
  return false;
2188
2679
  }
@@ -2201,37 +2692,24 @@ class FileUploaderComponent {
2201
2692
  }
2202
2693
  }
2203
2694
  removeFile(file) {
2204
- this.selectedFiles.update(files => files.filter(f => f !== file));
2695
+ this.selectedFiles.update((files) => files.filter((f) => f !== file));
2205
2696
  }
2206
2697
  async uploadFiles(files) {
2207
2698
  const filesToUpload = files ?? this.selectedFiles();
2208
2699
  if (!filesToUpload.length || this.isUploading())
2209
2700
  return;
2210
2701
  this.isUploading.set(true);
2211
- const uploadedFiles = [];
2212
2702
  try {
2213
- for (const file of filesToUpload) {
2214
- this.uploadingFileName.set(file.name);
2215
- this.uploadProgress.set(0);
2216
- const response = await firstValueFrom(this.uploadFile()(file, this.uploadOptions()));
2217
- if (response.success && response.data) {
2218
- uploadedFiles.push(response.data);
2219
- this.fileUploaded.emit(response.data);
2220
- }
2221
- else {
2222
- throw new Error(response.message || 'Upload failed');
2223
- }
2703
+ const batchUploadFn = this.getUploadMultipleFn();
2704
+ // Use batch upload if available and multiple files
2705
+ if (batchUploadFn && filesToUpload.length > 1) {
2706
+ await this.uploadBatch(filesToUpload, batchUploadFn);
2707
+ }
2708
+ else {
2709
+ await this.uploadSequential(filesToUpload);
2224
2710
  }
2225
- this.filesUploaded.emit(uploadedFiles);
2226
- this.selectedFiles.set([]);
2227
- this.messageService.add({
2228
- severity: 'success',
2229
- summary: 'Upload Complete',
2230
- detail: `${uploadedFiles.length} file(s) uploaded successfully`,
2231
- });
2232
2711
  }
2233
2712
  catch (error) {
2234
- // Error toast handled by global interceptor
2235
2713
  this.onError.emit(error);
2236
2714
  }
2237
2715
  finally {
@@ -2240,54 +2718,113 @@ class FileUploaderComponent {
2240
2718
  this.uploadProgress.set(0);
2241
2719
  }
2242
2720
  }
2721
+ async uploadBatch(files, uploadFn) {
2722
+ this.uploadingFileName.set(`${files.length} ${this.t('shared.upload.files')}`);
2723
+ this.uploadProgress.set(0);
2724
+ const response = await firstValueFrom(uploadFn(files, this.uploadOptions()));
2725
+ if (response.success && response.data) {
2726
+ response.data.forEach((file) => this.fileUploaded.emit(file));
2727
+ this.filesUploaded.emit(response.data);
2728
+ this.selectedFiles.set([]);
2729
+ this.messageService.add({
2730
+ severity: 'success',
2731
+ summary: this.t('shared.upload.complete'),
2732
+ detail: this.t('shared.upload.files.uploaded', { count: response.data.length }),
2733
+ });
2734
+ }
2735
+ else {
2736
+ throw new Error(response.message || this.t('shared.upload.failed'));
2737
+ }
2738
+ }
2739
+ async uploadSequential(files) {
2740
+ const uploadedFiles = [];
2741
+ const uploadFn = this.getUploadFn();
2742
+ for (const file of files) {
2743
+ this.uploadingFileName.set(file.name);
2744
+ this.uploadProgress.set(0);
2745
+ const response = await firstValueFrom(uploadFn(file, this.uploadOptions()));
2746
+ if (response.success && response.data) {
2747
+ uploadedFiles.push(response.data);
2748
+ this.fileUploaded.emit(response.data);
2749
+ }
2750
+ else {
2751
+ throw new Error(response.message || this.t('shared.upload.failed'));
2752
+ }
2753
+ }
2754
+ this.filesUploaded.emit(uploadedFiles);
2755
+ this.selectedFiles.set([]);
2756
+ this.messageService.add({
2757
+ severity: 'success',
2758
+ summary: this.t('shared.upload.complete'),
2759
+ detail: this.t('shared.upload.files.uploaded', { count: uploadedFiles.length }),
2760
+ });
2761
+ }
2243
2762
  getFileIcon(file) {
2244
2763
  return getFileIconClass(file.type);
2245
2764
  }
2246
2765
  formatSize(bytes) {
2247
2766
  const kb = bytes / 1024;
2248
2767
  if (kb < 1024)
2249
- return `${kb.toFixed(1)} KB`;
2768
+ return `${kb.toFixed(1)} ${this.t('shared.units.kb')}`;
2250
2769
  const mb = kb / 1024;
2251
- return `${mb.toFixed(1)} MB`;
2770
+ return `${mb.toFixed(1)} ${this.t('shared.units.mb')}`;
2771
+ }
2772
+ t(key, variables) {
2773
+ if (this.translateAdapter) {
2774
+ return this.translateAdapter.translate(key, variables);
2775
+ }
2776
+ return key;
2252
2777
  }
2253
2778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2254
- 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: `
2255
- <div
2256
- class="w-full"
2257
- [class.opacity-60]="disabled()"
2258
- (dragover)="onDragOver($event)"
2259
- (dragleave)="onDragLeave($event)"
2260
- (drop)="onDrop($event)"
2261
- >
2262
- <!-- Upload Area - Responsive padding -->
2779
+ 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: false, transformFunction: null }, uploadMultipleFiles: { classPropertyName: "uploadMultipleFiles", publicName: "uploadMultipleFiles", 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 }, 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: `
2780
+ @if (!hasUploadCapability()) {
2781
+ <div class="p-4 border border-dashed border-orange-300 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
2782
+ <i class="pi pi-exclamation-triangle text-2xl text-orange-500 mb-2"></i>
2783
+ <p class="text-sm text-orange-700 dark:text-orange-300">
2784
+ {{ 'shared.upload.provider.not.configured' | translate: { provider: 'provideStorageProviders()' } }}
2785
+ </p>
2786
+ </div>
2787
+ } @else {
2263
2788
  <div
2264
- 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"
2265
- [class.drag-over]="isDragOver()"
2266
- [class.cursor-not-allowed]="disabled()"
2267
- (click)="fileInput.click()"
2789
+ class="w-full"
2790
+ [class.opacity-60]="disabled()"
2791
+ (dragover)="onDragOver($event)"
2792
+ (dragleave)="onDragLeave($event)"
2793
+ (drop)="onDrop($event)"
2268
2794
  >
2269
- @if (isUploading()) {
2795
+ <!-- Upload Area - Responsive padding -->
2796
+ <div
2797
+ 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"
2798
+ [class.drag-over]="isDragOver()"
2799
+ [class.cursor-not-allowed]="disabled()"
2800
+ (click)="fileInput.click()"
2801
+ >
2802
+ @if (isUploading()) {
2270
2803
  <div class="flex flex-col items-center">
2271
2804
  <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2272
- <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2805
+ <p class="mt-2 text-sm sm:text-base break-all px-2">{{ 'shared.upload.uploading' | translate: { fileName: uploadingFileName() } }}</p>
2273
2806
  @if (uploadProgress() > 0) {
2274
- <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2807
+ <p-progressBar
2808
+ [value]="uploadProgress()"
2809
+ [showValue]="true"
2810
+ class="w-full mt-2 max-w-xs"
2811
+ />
2275
2812
  }
2276
2813
  </div>
2277
2814
  } @else {
2278
2815
  <div class="flex flex-col items-center">
2279
2816
  <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2280
2817
  <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2281
- {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2818
+ {{ multiple() ? ('shared.upload.drop.multiple' | translate) : ('shared.upload.drop.single' | translate) }}
2282
2819
  </p>
2283
2820
  <p class="text-xs sm:text-sm text-color-secondary px-2">
2284
2821
  @if (acceptTypesDisplay()) {
2285
- Allowed: {{ acceptTypesDisplay() }}
2822
+ {{ 'shared.upload.allowed.types' | translate }} {{ acceptTypesDisplay() }}
2286
2823
  } @else {
2287
- All file types allowed
2824
+ {{ 'shared.upload.all.types.allowed' | translate }}
2288
2825
  }
2289
2826
  @if (maxSizeMb()) {
2290
- <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2827
+ <span class="whitespace-nowrap">{{ 'shared.upload.max.size' | translate: { size: maxSizeMb() } }}</span>
2291
2828
  }
2292
2829
  </p>
2293
2830
  </div>
@@ -2309,10 +2846,20 @@ class FileUploaderComponent {
2309
2846
  @if (selectedFiles().length > 0 && showPreview()) {
2310
2847
  <div class="mt-3 space-y-2">
2311
2848
  @for (file of selectedFiles(); track file.name) {
2312
- <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2313
- <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2314
- <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2315
- <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2849
+ <div
2850
+ class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg"
2851
+ >
2852
+ <i
2853
+ [class]="getFileIcon(file)"
2854
+ class="text-lg sm:text-xl flex-shrink-0"
2855
+ ></i>
2856
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{
2857
+ file.name
2858
+ }}</span>
2859
+ <span
2860
+ class="text-xs sm:text-sm text-color-secondary whitespace-nowrap"
2861
+ >{{ formatSize(file.size) }}</span
2862
+ >
2316
2863
  <p-button
2317
2864
  icon="pi pi-times"
2318
2865
  [text]="true"
@@ -2326,48 +2873,61 @@ class FileUploaderComponent {
2326
2873
  }
2327
2874
  </div>
2328
2875
  }
2329
- </div>
2330
- `, 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 });
2876
+ </div>
2877
+ }
2878
+ `, 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"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2331
2879
  }
2332
2880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, decorators: [{
2333
2881
  type: Component,
2334
- args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
2335
- <div
2336
- class="w-full"
2337
- [class.opacity-60]="disabled()"
2338
- (dragover)="onDragOver($event)"
2339
- (dragleave)="onDragLeave($event)"
2340
- (drop)="onDrop($event)"
2341
- >
2342
- <!-- Upload Area - Responsive padding -->
2882
+ args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule, TranslatePipe], template: `
2883
+ @if (!hasUploadCapability()) {
2884
+ <div class="p-4 border border-dashed border-orange-300 bg-orange-50 dark:bg-orange-900/20 rounded-lg text-center">
2885
+ <i class="pi pi-exclamation-triangle text-2xl text-orange-500 mb-2"></i>
2886
+ <p class="text-sm text-orange-700 dark:text-orange-300">
2887
+ {{ 'shared.upload.provider.not.configured' | translate: { provider: 'provideStorageProviders()' } }}
2888
+ </p>
2889
+ </div>
2890
+ } @else {
2343
2891
  <div
2344
- 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"
2345
- [class.drag-over]="isDragOver()"
2346
- [class.cursor-not-allowed]="disabled()"
2347
- (click)="fileInput.click()"
2892
+ class="w-full"
2893
+ [class.opacity-60]="disabled()"
2894
+ (dragover)="onDragOver($event)"
2895
+ (dragleave)="onDragLeave($event)"
2896
+ (drop)="onDrop($event)"
2348
2897
  >
2349
- @if (isUploading()) {
2898
+ <!-- Upload Area - Responsive padding -->
2899
+ <div
2900
+ 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"
2901
+ [class.drag-over]="isDragOver()"
2902
+ [class.cursor-not-allowed]="disabled()"
2903
+ (click)="fileInput.click()"
2904
+ >
2905
+ @if (isUploading()) {
2350
2906
  <div class="flex flex-col items-center">
2351
2907
  <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2352
- <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2908
+ <p class="mt-2 text-sm sm:text-base break-all px-2">{{ 'shared.upload.uploading' | translate: { fileName: uploadingFileName() } }}</p>
2353
2909
  @if (uploadProgress() > 0) {
2354
- <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2910
+ <p-progressBar
2911
+ [value]="uploadProgress()"
2912
+ [showValue]="true"
2913
+ class="w-full mt-2 max-w-xs"
2914
+ />
2355
2915
  }
2356
2916
  </div>
2357
2917
  } @else {
2358
2918
  <div class="flex flex-col items-center">
2359
2919
  <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2360
2920
  <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2361
- {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2921
+ {{ multiple() ? ('shared.upload.drop.multiple' | translate) : ('shared.upload.drop.single' | translate) }}
2362
2922
  </p>
2363
2923
  <p class="text-xs sm:text-sm text-color-secondary px-2">
2364
2924
  @if (acceptTypesDisplay()) {
2365
- Allowed: {{ acceptTypesDisplay() }}
2925
+ {{ 'shared.upload.allowed.types' | translate }} {{ acceptTypesDisplay() }}
2366
2926
  } @else {
2367
- All file types allowed
2927
+ {{ 'shared.upload.all.types.allowed' | translate }}
2368
2928
  }
2369
2929
  @if (maxSizeMb()) {
2370
- <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2930
+ <span class="whitespace-nowrap">{{ 'shared.upload.max.size' | translate: { size: maxSizeMb() } }}</span>
2371
2931
  }
2372
2932
  </p>
2373
2933
  </div>
@@ -2389,10 +2949,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2389
2949
  @if (selectedFiles().length > 0 && showPreview()) {
2390
2950
  <div class="mt-3 space-y-2">
2391
2951
  @for (file of selectedFiles(); track file.name) {
2392
- <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2393
- <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2394
- <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2395
- <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2952
+ <div
2953
+ class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg"
2954
+ >
2955
+ <i
2956
+ [class]="getFileIcon(file)"
2957
+ class="text-lg sm:text-xl flex-shrink-0"
2958
+ ></i>
2959
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{
2960
+ file.name
2961
+ }}</span>
2962
+ <span
2963
+ class="text-xs sm:text-sm text-color-secondary whitespace-nowrap"
2964
+ >{{ formatSize(file.size) }}</span
2965
+ >
2396
2966
  <p-button
2397
2967
  icon="pi pi-times"
2398
2968
  [text]="true"
@@ -2406,71 +2976,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2406
2976
  }
2407
2977
  </div>
2408
2978
  }
2409
- </div>
2410
- `, 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"] }]
2411
- }], 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"] }] } });
2979
+ </div>
2980
+ }
2981
+ `, 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"] }]
2982
+ }], propDecorators: { uploadFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFile", required: false }] }], uploadMultipleFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadMultipleFiles", required: false }] }], 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"] }] } });
2412
2983
 
2413
2984
  const DEFAULT_PAGE_SIZE = 20;
2985
+ const DEFAULT_SELECTOR_PAGE_SIZE = 50;
2414
2986
  /**
2415
- * File Selector Dialog - Browse and select existing files with filtering.
2987
+ * File Selector Dialog - Self-contained file browser with upload support.
2416
2988
  *
2417
- * Pass your own `loadFiles` function - works with any storage API.
2989
+ * Uses FILE_PROVIDER internally - no external functions needed.
2990
+ * Just configure with inputs and handle selection events.
2418
2991
  *
2419
2992
  * Features:
2420
2993
  * - Search with debouncing
2421
- * - File type filtering
2994
+ * - File type filtering (acceptTypes)
2995
+ * - Folder filtering
2422
2996
  * - Infinite scroll pagination
2423
2997
  * - Single or multiple selection
2998
+ * - Built-in file upload (withUploader)
2424
2999
  * - File preview with icons
2425
3000
  *
2426
3001
  * @example
2427
- * ```typescript
2428
- * // In component
2429
- * readonly fileService = inject(FileManagerApiService);
2430
- *
2431
- * readonly loadFiles: LoadFilesFn = (filter) =>
2432
- * this.fileService.getAll(filter.search, {
2433
- * pagination: { currentPage: filter.page, pageSize: filter.pageSize },
2434
- * filter: { contentTypes: filter.contentTypes },
2435
- * }).pipe(
2436
- * map(res => ({
2437
- * ...res,
2438
- * data: res.data?.map(f => ({
2439
- * id: f.id,
2440
- * name: f.name,
2441
- * contentType: f.contentType,
2442
- * size: f.size,
2443
- * url: f.url
2444
- * }))
2445
- * }))
2446
- * );
2447
- * ```
2448
- *
2449
3002
  * ```html
2450
3003
  * <lib-file-selector-dialog
2451
- * [(visible)]="showFileSelector"
2452
- * [loadFiles]="loadFiles"
3004
+ * [(visible)]="showSelector"
2453
3005
  * [acceptTypes]="['image/*']"
2454
- * [multiple]="false"
2455
- * (fileSelected)="onFileSelected($event)"
3006
+ * [multiple]="true"
3007
+ * [withUploader]="true"
3008
+ * (filesSelected)="onFilesSelected($event)"
2456
3009
  * />
2457
3010
  * ```
2458
3011
  */
2459
3012
  class FileSelectorDialogComponent {
2460
3013
  destroyRef = inject(DestroyRef);
3014
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
3015
+ fileProvider = inject(FILE_PROVIDER, { optional: true });
2461
3016
  abortController = null;
2462
3017
  searchDebounceTimer = null;
2463
- // Required: function to load files
2464
- loadFiles = input.required(...(ngDevMode ? [{ debugName: "loadFiles" }] : []));
2465
- // Inputs
2466
- header = input('Select File', ...(ngDevMode ? [{ debugName: "header" }] : []));
3018
+ // Configuration inputs
3019
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : []));
2467
3020
  acceptTypes = input([], ...(ngDevMode ? [{ debugName: "acceptTypes" }] : []));
2468
3021
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
3022
+ // Computed header - shows "Select Files" for multiple, allows custom override
3023
+ dialogHeader = computed(() => {
3024
+ const customHeader = this.header();
3025
+ if (customHeader)
3026
+ return customHeader;
3027
+ return this.t(this.multiple() ? 'shared.file.selector.select.files' : 'shared.file.selector.select.file');
3028
+ }, ...(ngDevMode ? [{ debugName: "dialogHeader" }] : []));
2469
3029
  maxSelection = input(10, ...(ngDevMode ? [{ debugName: "maxSelection" }] : []));
2470
3030
  pageSize = input(DEFAULT_PAGE_SIZE, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
3031
+ folderId = input(...(ngDevMode ? [undefined, { debugName: "folderId" }] : [])); // Filter by folder
3032
+ withUploader = input(false, ...(ngDevMode ? [{ debugName: "withUploader" }] : []));
2471
3033
  // Two-way visibility binding
2472
3034
  visible = model(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
2473
- // Outputs
3035
+ // Outputs - return file IDs
2474
3036
  fileSelected = output();
2475
3037
  filesSelected = output();
2476
3038
  closed = output();
@@ -2480,10 +3042,48 @@ class FileSelectorDialogComponent {
2480
3042
  files = signal([], ...(ngDevMode ? [{ debugName: "files" }] : []));
2481
3043
  selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : []));
2482
3044
  total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
2483
- pagination = signal({ pageSize: DEFAULT_PAGE_SIZE, currentPage: 0 }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
3045
+ pagination = signal({
3046
+ pageSize: DEFAULT_PAGE_SIZE,
3047
+ currentPage: 0,
3048
+ }, ...(ngDevMode ? [{ debugName: "pagination" }] : []));
2484
3049
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
2485
- // Computed
2486
- acceptString = computed(() => getAcceptString(this.acceptTypes()), ...(ngDevMode ? [{ debugName: "acceptString" }] : []));
3050
+ // Folder selector state
3051
+ folders = signal([], ...(ngDevMode ? [{ debugName: "folders" }] : []));
3052
+ selectedFolderId = signal(null, ...(ngDevMode ? [{ debugName: "selectedFolderId" }] : []));
3053
+ foldersLoading = signal(false, ...(ngDevMode ? [{ debugName: "foldersLoading" }] : []));
3054
+ foldersPagination = signal({
3055
+ pageSize: DEFAULT_SELECTOR_PAGE_SIZE,
3056
+ currentPage: 0,
3057
+ }, ...(ngDevMode ? [{ debugName: "foldersPagination" }] : []));
3058
+ foldersTotal = signal(undefined, ...(ngDevMode ? [{ debugName: "foldersTotal" }] : []));
3059
+ // Storage config selector state
3060
+ storageConfigs = signal([], ...(ngDevMode ? [{ debugName: "storageConfigs" }] : []));
3061
+ selectedStorageConfigId = signal(null, ...(ngDevMode ? [{ debugName: "selectedStorageConfigId" }] : []));
3062
+ storageConfigsLoading = signal(false, ...(ngDevMode ? [{ debugName: "storageConfigsLoading" }] : []));
3063
+ storageConfigsPagination = signal({
3064
+ pageSize: DEFAULT_SELECTOR_PAGE_SIZE,
3065
+ currentPage: 0,
3066
+ }, ...(ngDevMode ? [{ debugName: "storageConfigsPagination" }] : []));
3067
+ storageConfigsTotal = signal(undefined, ...(ngDevMode ? [{ debugName: "storageConfigsTotal" }] : []));
3068
+ // Get effective storage config ID (selected or default)
3069
+ effectiveStorageConfigId = computed(() => {
3070
+ const selected = this.selectedStorageConfigId();
3071
+ if (selected)
3072
+ return selected;
3073
+ // Find default config
3074
+ const defaultConfig = this.storageConfigs().find((c) => c.isDefault);
3075
+ return defaultConfig?.id;
3076
+ }, ...(ngDevMode ? [{ debugName: "effectiveStorageConfigId" }] : []));
3077
+ // Upload function bound to provider
3078
+ uploadFileFn = (file, options) => {
3079
+ if (!this.fileProvider)
3080
+ throw new Error('shared.file.selector.provider.not.configured');
3081
+ return this.fileProvider.uploadFile(file, options);
3082
+ };
3083
+ // Multiple upload function bound to provider (optional)
3084
+ uploadMultipleFilesFn = this.fileProvider?.uploadMultipleFiles
3085
+ ? (files, options) => this.fileProvider.uploadMultipleFiles(files, options)
3086
+ : undefined;
2487
3087
  constructor() {
2488
3088
  this.destroyRef.onDestroy(() => {
2489
3089
  this.abortController?.abort();
@@ -2491,13 +3091,17 @@ class FileSelectorDialogComponent {
2491
3091
  clearTimeout(this.searchDebounceTimer);
2492
3092
  }
2493
3093
  });
2494
- // Load files when dialog becomes visible
3094
+ // Load files and storage configs when dialog becomes visible
2495
3095
  effect(() => {
2496
3096
  const isVisible = this.visible();
2497
3097
  if (isVisible) {
2498
3098
  untracked(() => {
2499
3099
  this.resetState();
2500
3100
  this.fetchFiles();
3101
+ // Preload storage configs for upload default
3102
+ if (this.fileProvider?.loadStorageConfigs) {
3103
+ this.loadStorageConfigs();
3104
+ }
2501
3105
  });
2502
3106
  }
2503
3107
  });
@@ -2510,7 +3114,6 @@ class FileSelectorDialogComponent {
2510
3114
  });
2511
3115
  }
2512
3116
  onSearchChange(value) {
2513
- // Debounce search
2514
3117
  if (this.searchDebounceTimer) {
2515
3118
  clearTimeout(this.searchDebounceTimer);
2516
3119
  }
@@ -2590,15 +3193,131 @@ class FileSelectorDialogComponent {
2590
3193
  onDialogHide() {
2591
3194
  this.closed.emit();
2592
3195
  }
3196
+ // Folder selector methods
3197
+ onFolderChange(folderId) {
3198
+ this.selectedFolderId.set(folderId);
3199
+ this.pagination.update((p) => ({ ...p, currentPage: 0 }));
3200
+ this.files.set([]);
3201
+ this.fetchFiles();
3202
+ }
3203
+ onFolderSearch(event) {
3204
+ this.foldersPagination.update((p) => ({ ...p, currentPage: 0 }));
3205
+ this.loadFolders(event.filter);
3206
+ }
3207
+ async loadFolders(search = '') {
3208
+ if (!this.fileProvider?.loadFolders || this.foldersLoading())
3209
+ return;
3210
+ this.foldersLoading.set(true);
3211
+ try {
3212
+ const pag = this.foldersPagination();
3213
+ const filter = {
3214
+ page: pag.currentPage,
3215
+ pageSize: pag.pageSize,
3216
+ search,
3217
+ };
3218
+ const response = await firstValueFrom(this.fileProvider.loadFolders(filter));
3219
+ if (response.success && response.data) {
3220
+ this.folders.set(response.data);
3221
+ this.foldersTotal.set(response.meta?.total);
3222
+ }
3223
+ }
3224
+ catch (error) {
3225
+ this.onError.emit(error);
3226
+ }
3227
+ finally {
3228
+ this.foldersLoading.set(false);
3229
+ }
3230
+ }
3231
+ // Storage config selector methods
3232
+ onStorageConfigChange(configId) {
3233
+ this.selectedStorageConfigId.set(configId);
3234
+ this.pagination.update((p) => ({ ...p, currentPage: 0 }));
3235
+ this.files.set([]);
3236
+ this.fetchFiles();
3237
+ }
3238
+ onStorageConfigSearch(event) {
3239
+ this.storageConfigsPagination.update((p) => ({ ...p, currentPage: 0 }));
3240
+ this.loadStorageConfigs(event.filter);
3241
+ }
3242
+ async loadStorageConfigs(search = '') {
3243
+ if (!this.fileProvider?.loadStorageConfigs || this.storageConfigsLoading())
3244
+ return;
3245
+ this.storageConfigsLoading.set(true);
3246
+ try {
3247
+ const pag = this.storageConfigsPagination();
3248
+ const filter = {
3249
+ page: pag.currentPage,
3250
+ pageSize: pag.pageSize,
3251
+ search,
3252
+ };
3253
+ const response = await firstValueFrom(this.fileProvider.loadStorageConfigs(filter));
3254
+ if (response.success && response.data) {
3255
+ this.storageConfigs.set(response.data);
3256
+ this.storageConfigsTotal.set(response.meta?.total);
3257
+ }
3258
+ }
3259
+ catch (error) {
3260
+ this.onError.emit(error);
3261
+ }
3262
+ finally {
3263
+ this.storageConfigsLoading.set(false);
3264
+ }
3265
+ }
3266
+ onFileUploaded(uploadedFile) {
3267
+ if (!uploadedFile.id)
3268
+ return;
3269
+ // Convert uploaded file to IFileBasicInfo
3270
+ const fileInfo = {
3271
+ id: uploadedFile.id,
3272
+ name: uploadedFile.name,
3273
+ contentType: uploadedFile.contentType,
3274
+ size: String(uploadedFile.size / 1024),
3275
+ url: null,
3276
+ };
3277
+ // Add to the beginning of files list
3278
+ this.files.update((current) => [fileInfo, ...current]);
3279
+ // Auto-select the uploaded file
3280
+ if (this.multiple()) {
3281
+ const selected = this.selectedFiles();
3282
+ if (selected.length < this.maxSelection()) {
3283
+ this.selectedFiles.update((files) => [...files, fileInfo]);
3284
+ }
3285
+ }
3286
+ else {
3287
+ this.selectedFiles.set([fileInfo]);
3288
+ }
3289
+ // Refresh files to get proper URLs
3290
+ this.fetchFiles();
3291
+ }
2593
3292
  resetState() {
3293
+ // Cancel any pending request and reset loading state
3294
+ this.abortController?.abort();
3295
+ this.abortController = null;
3296
+ this.isLoading.set(false);
2594
3297
  this.files.set([]);
2595
3298
  this.selectedFiles.set([]);
2596
3299
  this.searchTerm.set('');
2597
3300
  this.pagination.set({ pageSize: this.pageSize(), currentPage: 0 });
2598
3301
  this.total.set(undefined);
3302
+ // Reset folder selector
3303
+ this.selectedFolderId.set(null);
3304
+ this.folders.set([]);
3305
+ this.foldersPagination.set({
3306
+ pageSize: DEFAULT_SELECTOR_PAGE_SIZE,
3307
+ currentPage: 0,
3308
+ });
3309
+ this.foldersTotal.set(undefined);
3310
+ // Reset storage config selector
3311
+ this.selectedStorageConfigId.set(null);
3312
+ this.storageConfigs.set([]);
3313
+ this.storageConfigsPagination.set({
3314
+ pageSize: DEFAULT_SELECTOR_PAGE_SIZE,
3315
+ currentPage: 0,
3316
+ });
3317
+ this.storageConfigsTotal.set(undefined);
2599
3318
  }
2600
3319
  async fetchFiles(append = false) {
2601
- if (this.isLoading())
3320
+ if (!this.fileProvider || this.isLoading())
2602
3321
  return;
2603
3322
  this.abortController?.abort();
2604
3323
  this.abortController = new AbortController();
@@ -2609,15 +3328,33 @@ class FileSelectorDialogComponent {
2609
3328
  page: pag.currentPage,
2610
3329
  pageSize: pag.pageSize,
2611
3330
  search: this.searchTerm(),
2612
- contentTypes: this.acceptTypes().length ? this.acceptTypes() : undefined,
3331
+ contentTypes: this.acceptTypes().length
3332
+ ? this.acceptTypes()
3333
+ : undefined,
3334
+ folderId: this.selectedFolderId() || this.folderId(),
3335
+ storageConfigId: this.selectedStorageConfigId() || undefined,
2613
3336
  };
2614
- const response = await firstValueFrom(this.loadFiles()(filter));
3337
+ const response = await firstValueFrom(this.fileProvider.loadFiles(filter));
2615
3338
  if (response.success && response.data) {
3339
+ let files = response.data;
3340
+ // Always fetch URLs for all files to get fresh presigned URLs
3341
+ if (this.fileProvider.getFileUrls && files.length > 0) {
3342
+ const fileIds = files.map((f) => f.id);
3343
+ const urlResponse = await firstValueFrom(this.fileProvider.getFileUrls(fileIds));
3344
+ if (urlResponse.success && urlResponse.data) {
3345
+ // Merge URLs into files
3346
+ const urlMap = new Map(urlResponse.data.map((f) => [f.id, f.url]));
3347
+ files = files.map((f) => ({
3348
+ ...f,
3349
+ url: urlMap.get(f.id) ?? f.url,
3350
+ }));
3351
+ }
3352
+ }
2616
3353
  if (append) {
2617
- this.files.update((current) => [...current, ...response.data]);
3354
+ this.files.update((current) => [...current, ...files]);
2618
3355
  }
2619
3356
  else {
2620
- this.files.set(response.data);
3357
+ this.files.set(files);
2621
3358
  }
2622
3359
  this.total.set(response.meta?.total);
2623
3360
  }
@@ -2631,10 +3368,16 @@ class FileSelectorDialogComponent {
2631
3368
  this.isLoading.set(false);
2632
3369
  }
2633
3370
  }
3371
+ t(key, variables) {
3372
+ if (this.translateAdapter) {
3373
+ return this.translateAdapter.translate(key, variables);
3374
+ }
3375
+ return key;
3376
+ }
2634
3377
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2635
- 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: `
3378
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: FileSelectorDialogComponent, isStandalone: true, selector: "lib-file-selector-dialog", inputs: { 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 }, folderId: { classPropertyName: "folderId", publicName: "folderId", isSignal: true, isRequired: false, transformFunction: null }, withUploader: { classPropertyName: "withUploader", publicName: "withUploader", 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: `
2636
3379
  <p-dialog
2637
- [header]="header()"
3380
+ [header]="dialogHeader()"
2638
3381
  [(visible)]="visible"
2639
3382
  [modal]="true"
2640
3383
  [closable]="true"
@@ -2645,107 +3388,246 @@ class FileSelectorDialogComponent {
2645
3388
  styleClass="file-selector-dialog"
2646
3389
  (onHide)="onDialogHide()"
2647
3390
  >
2648
- <!-- Search Bar -->
2649
- <div class="flex flex-col sm:flex-row gap-2 mb-3">
2650
- <span class="p-input-icon-left flex-1">
2651
- <i class="pi pi-search"></i>
2652
- <input
2653
- pInputText
2654
- type="text"
2655
- [ngModel]="searchTerm()"
2656
- (ngModelChange)="onSearchChange($event)"
2657
- placeholder="Search files..."
2658
- class="w-full"
2659
- />
2660
- </span>
2661
- @if (multiple()) {
2662
- <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2663
- {{ selectedFiles().length }} selected
2664
- </span>
3391
+ @if (!fileProvider) {
3392
+ <div class="p-6 text-center">
3393
+ <i
3394
+ class="pi pi-exclamation-triangle text-4xl text-orange-500 mb-3"
3395
+ ></i>
3396
+ <p class="text-lg font-medium text-color mb-2">
3397
+ {{ 'shared.file.selector.provider.not.configured' | translate }}
3398
+ </p>
3399
+ <p class="text-sm text-color-secondary mb-4">
3400
+ {{ 'shared.file.selector.add.provider' | translate }}
3401
+ <code class="inline-block bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded font-mono text-xs">provideStorageProviders()</code>
3402
+ </p>
3403
+ <button
3404
+ pButton
3405
+ [label]="'shared.close' | translate"
3406
+ class="p-button-text"
3407
+ (click)="onCancel()"
3408
+ ></button>
3409
+ </div>
3410
+ } @else {
3411
+ <!-- Upload Section (when withUploader is enabled) -->
3412
+ @if (withUploader()) {
3413
+ <div class="mb-4">
3414
+ <lib-file-uploader
3415
+ [uploadFile]="uploadFileFn"
3416
+ [uploadMultipleFiles]="uploadMultipleFilesFn"
3417
+ [uploadOptions]="{ storageConfigId: effectiveStorageConfigId() }"
3418
+ [acceptTypes]="acceptTypes()"
3419
+ [multiple]="multiple()"
3420
+ [maxFiles]="maxSelection()"
3421
+ [maxSizeMb]="10"
3422
+ [showPreview]="false"
3423
+ (fileUploaded)="onFileUploaded($event)"
3424
+ />
3425
+ </div>
2665
3426
  }
2666
- </div>
2667
3427
 
2668
- <!-- File Grid - Responsive columns -->
2669
- <div
2670
- class="file-grid"
2671
- #scrollContainer
2672
- (scroll)="onScroll($event)"
2673
- >
2674
- @if (isLoading() && files().length === 0) {
2675
- <div class="col-span-full flex justify-center p-4">
2676
- <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2677
- </div>
2678
- } @else if (files().length === 0) {
2679
- <div class="col-span-full text-center p-4 text-color-secondary">
2680
- <i class="pi pi-inbox text-4xl mb-2 block"></i>
2681
- <p>No files found</p>
2682
- </div>
2683
- } @else {
2684
- @for (file of files(); track file.id) {
2685
- <div
2686
- class="file-card"
2687
- [class.selected]="isSelected(file)"
2688
- [class.disabled]="!isFileAllowed(file)"
2689
- (click)="toggleSelection(file)"
3428
+ <!-- Filters Row - All on same line on desktop -->
3429
+ <div
3430
+ class="flex flex-col sm:flex-row gap-2 mb-3 items-stretch sm:items-center"
3431
+ >
3432
+ <!-- Search -->
3433
+ <p-iconfield class="flex-1">
3434
+ <p-inputicon class="pi pi-search" />
3435
+ <input
3436
+ pInputText
3437
+ type="text"
3438
+ [ngModel]="searchTerm()"
3439
+ (ngModelChange)="onSearchChange($event)"
3440
+ [placeholder]="'shared.file.selector.search.placeholder' | translate"
3441
+ class="w-full"
3442
+ />
3443
+ </p-iconfield>
3444
+
3445
+ <!-- Folder Selector -->
3446
+ @if (fileProvider.loadFolders) {
3447
+ <p-select
3448
+ [options]="folders()"
3449
+ [ngModel]="selectedFolderId()"
3450
+ (ngModelChange)="onFolderChange($event)"
3451
+ optionLabel="name"
3452
+ optionValue="id"
3453
+ [placeholder]="'shared.file.selector.all.folders' | translate"
3454
+ [showClear]="true"
3455
+ [filter]="true"
3456
+ filterBy="name"
3457
+ (onFilter)="onFolderSearch($event)"
3458
+ [loading]="foldersLoading()"
3459
+ class="w-full sm:w-40"
3460
+ (onShow)="loadFolders()"
2690
3461
  >
2691
- <!-- File Preview -->
2692
- <div class="file-preview">
2693
- @if (isImage(file) && file.url) {
2694
- <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2695
- } @else {
2696
- <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2697
- }
2698
- @if (isSelected(file)) {
2699
- <div class="selected-overlay">
2700
- <i class="pi pi-check text-xl sm:text-2xl"></i>
2701
- </div>
2702
- }
2703
- </div>
3462
+ <ng-template pTemplate="selectedItem" let-folder>
3463
+ <div class="flex items-center gap-2">
3464
+ <i class="pi pi-folder text-color-secondary"></i>
3465
+ <span>{{ folder?.name }}</span>
3466
+ </div>
3467
+ </ng-template>
3468
+ <ng-template pTemplate="item" let-folder>
3469
+ <div class="flex items-center gap-2">
3470
+ <i class="pi pi-folder text-color-secondary"></i>
3471
+ <span>{{ folder.name }}</span>
3472
+ </div>
3473
+ </ng-template>
3474
+ </p-select>
3475
+ }
2704
3476
 
2705
- <!-- File Info -->
2706
- <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2707
- <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2708
- {{ file.name }}
2709
- </span>
2710
- <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2711
- </div>
2712
- </div>
3477
+ <!-- Storage Config Selector -->
3478
+ @if (fileProvider.loadStorageConfigs) {
3479
+ <p-select
3480
+ [options]="storageConfigs()"
3481
+ [ngModel]="selectedStorageConfigId()"
3482
+ (ngModelChange)="onStorageConfigChange($event)"
3483
+ optionLabel="name"
3484
+ optionValue="id"
3485
+ [placeholder]="'shared.file.selector.all.storage' | translate"
3486
+ [showClear]="true"
3487
+ [filter]="true"
3488
+ filterBy="name"
3489
+ (onFilter)="onStorageConfigSearch($event)"
3490
+ [loading]="storageConfigsLoading()"
3491
+ class="w-full sm:w-40"
3492
+ (onShow)="loadStorageConfigs()"
3493
+ >
3494
+ <ng-template pTemplate="selectedItem" let-config>
3495
+ <div class="flex items-center gap-2">
3496
+ <i class="pi pi-database text-color-secondary"></i>
3497
+ <span>{{ config?.name }}</span>
3498
+ @if (config?.isDefault) {
3499
+ <span
3500
+ class="text-xs bg-primary text-primary-contrast px-1 rounded"
3501
+ >{{ 'shared.default' | translate }}</span
3502
+ >
3503
+ }
3504
+ </div>
3505
+ </ng-template>
3506
+ <ng-template pTemplate="item" let-config>
3507
+ <div class="flex items-center gap-2">
3508
+ <i class="pi pi-database text-color-secondary"></i>
3509
+ <span>{{ config.name }}</span>
3510
+ @if (config.isDefault) {
3511
+ <span
3512
+ class="text-xs bg-primary text-primary-contrast px-1 rounded"
3513
+ >{{ 'shared.default' | translate }}</span
3514
+ >
3515
+ }
3516
+ </div>
3517
+ </ng-template>
3518
+ </p-select>
2713
3519
  }
2714
3520
 
2715
- @if (isLoading()) {
2716
- <div class="col-span-full flex justify-center p-2">
2717
- <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2718
- </div>
3521
+ <!-- Selected count -->
3522
+ @if (multiple()) {
3523
+ <span
3524
+ class="text-sm text-color-secondary self-center whitespace-nowrap"
3525
+ >
3526
+ {{ 'shared.file.selector.selected' | translate: { count: selectedFiles().length } }}
3527
+ </span>
2719
3528
  }
2720
- }
2721
- </div>
3529
+ </div>
2722
3530
 
2723
- <!-- Footer -->
2724
- <ng-template #footer>
2725
- <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2726
- <button
2727
- pButton
2728
- label="Cancel"
2729
- class="p-button-text w-full sm:w-auto"
2730
- (click)="onCancel()"
2731
- ></button>
2732
- <button
2733
- pButton
2734
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2735
- [disabled]="selectedFiles().length === 0"
2736
- class="w-full sm:w-auto"
2737
- (click)="onConfirm()"
2738
- ></button>
3531
+ <!-- File Grid - Responsive columns -->
3532
+ <div class="file-grid" #scrollContainer (scroll)="onScroll($event)">
3533
+ @if (isLoading() && files().length === 0) {
3534
+ <div class="col-span-full flex justify-center p-4">
3535
+ <i
3536
+ class="pi pi-spin pi-spinner text-4xl text-color-secondary"
3537
+ ></i>
3538
+ </div>
3539
+ } @else if (files().length === 0) {
3540
+ <div class="col-span-full text-center p-4 text-color-secondary">
3541
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
3542
+ <p>{{ 'shared.file.selector.no.files' | translate }}</p>
3543
+ </div>
3544
+ } @else {
3545
+ @for (file of files(); track file.id) {
3546
+ <div
3547
+ class="file-card"
3548
+ [class.selected]="isSelected(file)"
3549
+ [class.disabled]="!isFileAllowed(file)"
3550
+ (click)="toggleSelection(file)"
3551
+ >
3552
+ <!-- File Preview -->
3553
+ <div class="file-preview">
3554
+ @if (isImage(file) && file.url) {
3555
+ <img
3556
+ [src]="file.url"
3557
+ [alt]="file.name"
3558
+ class="w-full h-full object-cover"
3559
+ />
3560
+ } @else {
3561
+ <i
3562
+ [class]="getFileIcon(file)"
3563
+ class="text-4xl sm:text-5xl text-color-secondary"
3564
+ ></i>
3565
+ }
3566
+ @if (isSelected(file)) {
3567
+ <div class="selected-overlay">
3568
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
3569
+ </div>
3570
+ }
3571
+ </div>
3572
+
3573
+ <!-- File Info -->
3574
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
3575
+ <span
3576
+ class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis"
3577
+ [title]="file.name"
3578
+ >
3579
+ {{ file.name }}
3580
+ </span>
3581
+ <span class="block text-xs text-color-secondary">{{
3582
+ formatSize(file.size)
3583
+ }}</span>
3584
+ </div>
3585
+ </div>
3586
+ }
3587
+
3588
+ @if (isLoading()) {
3589
+ <div class="col-span-full flex justify-center p-2">
3590
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
3591
+ </div>
3592
+ }
3593
+ }
2739
3594
  </div>
2740
- </ng-template>
3595
+
3596
+ <!-- Footer -->
3597
+ <ng-template pTemplate="footer">
3598
+ <div
3599
+ class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end"
3600
+ >
3601
+ <button
3602
+ pButton
3603
+ type="button"
3604
+ [label]="'shared.cancel' | translate"
3605
+ class="p-button-text w-full sm:w-auto"
3606
+ (click)="onCancel()"
3607
+ ></button>
3608
+ <button
3609
+ pButton
3610
+ type="button"
3611
+ [label]="
3612
+ multiple()
3613
+ ? ('shared.file.selector.select.multiple' | translate: { count: selectedFiles().length })
3614
+ : ('shared.file.selector.select' | translate)
3615
+ "
3616
+ [disabled]="selectedFiles().length === 0"
3617
+ class="w-full sm:w-auto"
3618
+ (click)="onConfirm()"
3619
+ ></button>
3620
+ </div>
3621
+ </ng-template>
3622
+ }
2741
3623
  </p-dialog>
2742
- `, 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 });
3624
+ `, 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: i3$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: 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: "component", type: i6.IconField, selector: "p-iconfield, p-iconField, p-icon-field", inputs: ["hostName", "iconPosition", "styleClass"] }, { kind: "component", type: i7.InputIcon, selector: "p-inputicon, p-inputIcon", inputs: ["hostName", "styleClass"] }, { 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: "component", type: FileUploaderComponent, selector: "lib-file-uploader", inputs: ["uploadFile", "uploadMultipleFiles", "acceptTypes", "multiple", "maxFiles", "maxSizeMb", "uploadOptions", "disabled", "showPreview", "autoUpload"], outputs: ["fileUploaded", "filesUploaded", "onError", "fileSelected"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2743
3625
  }
2744
3626
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2745
3627
  type: Component,
2746
- args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
3628
+ args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule, FileUploaderComponent, TranslatePipe], template: `
2747
3629
  <p-dialog
2748
- [header]="header()"
3630
+ [header]="dialogHeader()"
2749
3631
  [(visible)]="visible"
2750
3632
  [modal]="true"
2751
3633
  [closable]="true"
@@ -2756,102 +3638,241 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2756
3638
  styleClass="file-selector-dialog"
2757
3639
  (onHide)="onDialogHide()"
2758
3640
  >
2759
- <!-- Search Bar -->
2760
- <div class="flex flex-col sm:flex-row gap-2 mb-3">
2761
- <span class="p-input-icon-left flex-1">
2762
- <i class="pi pi-search"></i>
2763
- <input
2764
- pInputText
2765
- type="text"
2766
- [ngModel]="searchTerm()"
2767
- (ngModelChange)="onSearchChange($event)"
2768
- placeholder="Search files..."
2769
- class="w-full"
2770
- />
2771
- </span>
2772
- @if (multiple()) {
2773
- <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2774
- {{ selectedFiles().length }} selected
2775
- </span>
3641
+ @if (!fileProvider) {
3642
+ <div class="p-6 text-center">
3643
+ <i
3644
+ class="pi pi-exclamation-triangle text-4xl text-orange-500 mb-3"
3645
+ ></i>
3646
+ <p class="text-lg font-medium text-color mb-2">
3647
+ {{ 'shared.file.selector.provider.not.configured' | translate }}
3648
+ </p>
3649
+ <p class="text-sm text-color-secondary mb-4">
3650
+ {{ 'shared.file.selector.add.provider' | translate }}
3651
+ <code class="inline-block bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded font-mono text-xs">provideStorageProviders()</code>
3652
+ </p>
3653
+ <button
3654
+ pButton
3655
+ [label]="'shared.close' | translate"
3656
+ class="p-button-text"
3657
+ (click)="onCancel()"
3658
+ ></button>
3659
+ </div>
3660
+ } @else {
3661
+ <!-- Upload Section (when withUploader is enabled) -->
3662
+ @if (withUploader()) {
3663
+ <div class="mb-4">
3664
+ <lib-file-uploader
3665
+ [uploadFile]="uploadFileFn"
3666
+ [uploadMultipleFiles]="uploadMultipleFilesFn"
3667
+ [uploadOptions]="{ storageConfigId: effectiveStorageConfigId() }"
3668
+ [acceptTypes]="acceptTypes()"
3669
+ [multiple]="multiple()"
3670
+ [maxFiles]="maxSelection()"
3671
+ [maxSizeMb]="10"
3672
+ [showPreview]="false"
3673
+ (fileUploaded)="onFileUploaded($event)"
3674
+ />
3675
+ </div>
2776
3676
  }
2777
- </div>
2778
3677
 
2779
- <!-- File Grid - Responsive columns -->
2780
- <div
2781
- class="file-grid"
2782
- #scrollContainer
2783
- (scroll)="onScroll($event)"
2784
- >
2785
- @if (isLoading() && files().length === 0) {
2786
- <div class="col-span-full flex justify-center p-4">
2787
- <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2788
- </div>
2789
- } @else if (files().length === 0) {
2790
- <div class="col-span-full text-center p-4 text-color-secondary">
2791
- <i class="pi pi-inbox text-4xl mb-2 block"></i>
2792
- <p>No files found</p>
2793
- </div>
2794
- } @else {
2795
- @for (file of files(); track file.id) {
2796
- <div
2797
- class="file-card"
2798
- [class.selected]="isSelected(file)"
2799
- [class.disabled]="!isFileAllowed(file)"
2800
- (click)="toggleSelection(file)"
3678
+ <!-- Filters Row - All on same line on desktop -->
3679
+ <div
3680
+ class="flex flex-col sm:flex-row gap-2 mb-3 items-stretch sm:items-center"
3681
+ >
3682
+ <!-- Search -->
3683
+ <p-iconfield class="flex-1">
3684
+ <p-inputicon class="pi pi-search" />
3685
+ <input
3686
+ pInputText
3687
+ type="text"
3688
+ [ngModel]="searchTerm()"
3689
+ (ngModelChange)="onSearchChange($event)"
3690
+ [placeholder]="'shared.file.selector.search.placeholder' | translate"
3691
+ class="w-full"
3692
+ />
3693
+ </p-iconfield>
3694
+
3695
+ <!-- Folder Selector -->
3696
+ @if (fileProvider.loadFolders) {
3697
+ <p-select
3698
+ [options]="folders()"
3699
+ [ngModel]="selectedFolderId()"
3700
+ (ngModelChange)="onFolderChange($event)"
3701
+ optionLabel="name"
3702
+ optionValue="id"
3703
+ [placeholder]="'shared.file.selector.all.folders' | translate"
3704
+ [showClear]="true"
3705
+ [filter]="true"
3706
+ filterBy="name"
3707
+ (onFilter)="onFolderSearch($event)"
3708
+ [loading]="foldersLoading()"
3709
+ class="w-full sm:w-40"
3710
+ (onShow)="loadFolders()"
2801
3711
  >
2802
- <!-- File Preview -->
2803
- <div class="file-preview">
2804
- @if (isImage(file) && file.url) {
2805
- <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2806
- } @else {
2807
- <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2808
- }
2809
- @if (isSelected(file)) {
2810
- <div class="selected-overlay">
2811
- <i class="pi pi-check text-xl sm:text-2xl"></i>
2812
- </div>
2813
- }
2814
- </div>
3712
+ <ng-template pTemplate="selectedItem" let-folder>
3713
+ <div class="flex items-center gap-2">
3714
+ <i class="pi pi-folder text-color-secondary"></i>
3715
+ <span>{{ folder?.name }}</span>
3716
+ </div>
3717
+ </ng-template>
3718
+ <ng-template pTemplate="item" let-folder>
3719
+ <div class="flex items-center gap-2">
3720
+ <i class="pi pi-folder text-color-secondary"></i>
3721
+ <span>{{ folder.name }}</span>
3722
+ </div>
3723
+ </ng-template>
3724
+ </p-select>
3725
+ }
2815
3726
 
2816
- <!-- File Info -->
2817
- <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2818
- <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2819
- {{ file.name }}
2820
- </span>
2821
- <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2822
- </div>
2823
- </div>
3727
+ <!-- Storage Config Selector -->
3728
+ @if (fileProvider.loadStorageConfigs) {
3729
+ <p-select
3730
+ [options]="storageConfigs()"
3731
+ [ngModel]="selectedStorageConfigId()"
3732
+ (ngModelChange)="onStorageConfigChange($event)"
3733
+ optionLabel="name"
3734
+ optionValue="id"
3735
+ [placeholder]="'shared.file.selector.all.storage' | translate"
3736
+ [showClear]="true"
3737
+ [filter]="true"
3738
+ filterBy="name"
3739
+ (onFilter)="onStorageConfigSearch($event)"
3740
+ [loading]="storageConfigsLoading()"
3741
+ class="w-full sm:w-40"
3742
+ (onShow)="loadStorageConfigs()"
3743
+ >
3744
+ <ng-template pTemplate="selectedItem" let-config>
3745
+ <div class="flex items-center gap-2">
3746
+ <i class="pi pi-database text-color-secondary"></i>
3747
+ <span>{{ config?.name }}</span>
3748
+ @if (config?.isDefault) {
3749
+ <span
3750
+ class="text-xs bg-primary text-primary-contrast px-1 rounded"
3751
+ >{{ 'shared.default' | translate }}</span
3752
+ >
3753
+ }
3754
+ </div>
3755
+ </ng-template>
3756
+ <ng-template pTemplate="item" let-config>
3757
+ <div class="flex items-center gap-2">
3758
+ <i class="pi pi-database text-color-secondary"></i>
3759
+ <span>{{ config.name }}</span>
3760
+ @if (config.isDefault) {
3761
+ <span
3762
+ class="text-xs bg-primary text-primary-contrast px-1 rounded"
3763
+ >{{ 'shared.default' | translate }}</span
3764
+ >
3765
+ }
3766
+ </div>
3767
+ </ng-template>
3768
+ </p-select>
2824
3769
  }
2825
3770
 
2826
- @if (isLoading()) {
2827
- <div class="col-span-full flex justify-center p-2">
2828
- <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2829
- </div>
3771
+ <!-- Selected count -->
3772
+ @if (multiple()) {
3773
+ <span
3774
+ class="text-sm text-color-secondary self-center whitespace-nowrap"
3775
+ >
3776
+ {{ 'shared.file.selector.selected' | translate: { count: selectedFiles().length } }}
3777
+ </span>
2830
3778
  }
2831
- }
2832
- </div>
3779
+ </div>
2833
3780
 
2834
- <!-- Footer -->
2835
- <ng-template #footer>
2836
- <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2837
- <button
2838
- pButton
2839
- label="Cancel"
2840
- class="p-button-text w-full sm:w-auto"
2841
- (click)="onCancel()"
2842
- ></button>
2843
- <button
2844
- pButton
2845
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2846
- [disabled]="selectedFiles().length === 0"
2847
- class="w-full sm:w-auto"
2848
- (click)="onConfirm()"
2849
- ></button>
3781
+ <!-- File Grid - Responsive columns -->
3782
+ <div class="file-grid" #scrollContainer (scroll)="onScroll($event)">
3783
+ @if (isLoading() && files().length === 0) {
3784
+ <div class="col-span-full flex justify-center p-4">
3785
+ <i
3786
+ class="pi pi-spin pi-spinner text-4xl text-color-secondary"
3787
+ ></i>
3788
+ </div>
3789
+ } @else if (files().length === 0) {
3790
+ <div class="col-span-full text-center p-4 text-color-secondary">
3791
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
3792
+ <p>{{ 'shared.file.selector.no.files' | translate }}</p>
3793
+ </div>
3794
+ } @else {
3795
+ @for (file of files(); track file.id) {
3796
+ <div
3797
+ class="file-card"
3798
+ [class.selected]="isSelected(file)"
3799
+ [class.disabled]="!isFileAllowed(file)"
3800
+ (click)="toggleSelection(file)"
3801
+ >
3802
+ <!-- File Preview -->
3803
+ <div class="file-preview">
3804
+ @if (isImage(file) && file.url) {
3805
+ <img
3806
+ [src]="file.url"
3807
+ [alt]="file.name"
3808
+ class="w-full h-full object-cover"
3809
+ />
3810
+ } @else {
3811
+ <i
3812
+ [class]="getFileIcon(file)"
3813
+ class="text-4xl sm:text-5xl text-color-secondary"
3814
+ ></i>
3815
+ }
3816
+ @if (isSelected(file)) {
3817
+ <div class="selected-overlay">
3818
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
3819
+ </div>
3820
+ }
3821
+ </div>
3822
+
3823
+ <!-- File Info -->
3824
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
3825
+ <span
3826
+ class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis"
3827
+ [title]="file.name"
3828
+ >
3829
+ {{ file.name }}
3830
+ </span>
3831
+ <span class="block text-xs text-color-secondary">{{
3832
+ formatSize(file.size)
3833
+ }}</span>
3834
+ </div>
3835
+ </div>
3836
+ }
3837
+
3838
+ @if (isLoading()) {
3839
+ <div class="col-span-full flex justify-center p-2">
3840
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
3841
+ </div>
3842
+ }
3843
+ }
2850
3844
  </div>
2851
- </ng-template>
3845
+
3846
+ <!-- Footer -->
3847
+ <ng-template pTemplate="footer">
3848
+ <div
3849
+ class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end"
3850
+ >
3851
+ <button
3852
+ pButton
3853
+ type="button"
3854
+ [label]="'shared.cancel' | translate"
3855
+ class="p-button-text w-full sm:w-auto"
3856
+ (click)="onCancel()"
3857
+ ></button>
3858
+ <button
3859
+ pButton
3860
+ type="button"
3861
+ [label]="
3862
+ multiple()
3863
+ ? ('shared.file.selector.select.multiple' | translate: { count: selectedFiles().length })
3864
+ : ('shared.file.selector.select' | translate)
3865
+ "
3866
+ [disabled]="selectedFiles().length === 0"
3867
+ class="w-full sm:w-auto"
3868
+ (click)="onConfirm()"
3869
+ ></button>
3870
+ </div>
3871
+ </ng-template>
3872
+ }
2852
3873
  </p-dialog>
2853
- `, 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"] }]
2854
- }], 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"] }] } });
3874
+ `, 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"] }]
3875
+ }], ctorParameters: () => [], propDecorators: { 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 }] }], folderId: [{ type: i0.Input, args: [{ isSignal: true, alias: "folderId", required: false }] }], withUploader: [{ type: i0.Input, args: [{ isSignal: true, alias: "withUploader", 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"] }] } });
2855
3876
 
2856
3877
  function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
2857
3878
  return () => {
@@ -2947,8 +3968,7 @@ function allPermissionsGuard(permissions, redirectTo = '/') {
2947
3968
  * @Component({
2948
3969
  * selector: 'app-product-form',
2949
3970
  * standalone: true,
2950
- * changeDetection: ChangeDetectionStrategy.OnPush,
2951
- * template: `...`
3971
+ * * template: `...`
2952
3972
  * })
2953
3973
  * export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
2954
3974
  * private readonly productService = inject(ProductApiService);
@@ -3015,6 +4035,7 @@ class BaseFormPage {
3015
4035
  route = inject(ActivatedRoute);
3016
4036
  messageService = inject(MessageService);
3017
4037
  destroyRef = inject(DestroyRef);
4038
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
3018
4039
  routeParams = toSignal(this.route.paramMap);
3019
4040
  initialized = false;
3020
4041
  /** Loading state for async operations */
@@ -3052,8 +4073,8 @@ class BaseFormPage {
3052
4073
  : this.createItem(model);
3053
4074
  operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3054
4075
  next: () => {
3055
- const action = this.isEditMode() ? 'updated' : 'created';
3056
- this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
4076
+ const messageKey = this.isEditMode() ? 'shared.update.success' : 'shared.create.success';
4077
+ this.showSuccess(this.t(messageKey));
3057
4078
  this.router.navigate([this.getResourceRoute()]);
3058
4079
  },
3059
4080
  error: () => {
@@ -3078,8 +4099,8 @@ class BaseFormPage {
3078
4099
  showValidationError() {
3079
4100
  this.messageService.add({
3080
4101
  severity: 'error',
3081
- summary: 'Validation Error',
3082
- detail: 'Please fill in all required fields.',
4102
+ summary: this.t('shared.validation.error'),
4103
+ detail: this.t('shared.fill.all.fields'),
3083
4104
  });
3084
4105
  }
3085
4106
  /**
@@ -3089,7 +4110,7 @@ class BaseFormPage {
3089
4110
  showSuccess(detail) {
3090
4111
  this.messageService.add({
3091
4112
  severity: 'success',
3092
- summary: 'Success',
4113
+ summary: this.t('shared.success'),
3093
4114
  detail,
3094
4115
  });
3095
4116
  }
@@ -3100,10 +4121,16 @@ class BaseFormPage {
3100
4121
  showError(detail) {
3101
4122
  this.messageService.add({
3102
4123
  severity: 'error',
3103
- summary: 'Error',
4124
+ summary: this.t('shared.error'),
3104
4125
  detail,
3105
4126
  });
3106
4127
  }
4128
+ /**
4129
+ * Translate a key using the adapter, fallback to key if not available.
4130
+ */
4131
+ t(key, variables) {
4132
+ return this.translateAdapter?.translate(key, variables) ?? key;
4133
+ }
3107
4134
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3108
4135
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.5", type: BaseFormPage, isStandalone: true, ngImport: i0 });
3109
4136
  }
@@ -3140,6 +4167,7 @@ class BaseListPage {
3140
4167
  appConfig = inject(APP_CONFIG);
3141
4168
  confirmationService = inject(ConfirmationService);
3142
4169
  destroyRef = inject(DestroyRef);
4170
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
3143
4171
  /** Items list */
3144
4172
  items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3145
4173
  /** Loading state */
@@ -3188,26 +4216,32 @@ class BaseListPage {
3188
4216
  /**
3189
4217
  * Show success toast message
3190
4218
  */
3191
- showSuccess(detail, summary = 'Success') {
3192
- this.messageService.add({ severity: 'success', summary, detail });
4219
+ showSuccess(detail, summary) {
4220
+ this.messageService.add({ severity: 'success', summary: summary ?? this.t('shared.success'), detail });
3193
4221
  }
3194
4222
  /**
3195
4223
  * Show error toast message
3196
4224
  */
3197
- showError(detail, summary = 'Error') {
3198
- this.messageService.add({ severity: 'error', summary, detail });
4225
+ showError(detail, summary) {
4226
+ this.messageService.add({ severity: 'error', summary: summary ?? this.t('shared.error'), detail });
3199
4227
  }
3200
4228
  /**
3201
4229
  * Show info toast message
3202
4230
  */
3203
- showInfo(detail, summary = 'Info') {
3204
- this.messageService.add({ severity: 'info', summary, detail });
4231
+ showInfo(detail, summary) {
4232
+ this.messageService.add({ severity: 'info', summary: summary ?? this.t('shared.info'), detail });
3205
4233
  }
3206
4234
  /**
3207
4235
  * Show warning toast message
3208
4236
  */
3209
- showWarn(detail, summary = 'Warning') {
3210
- this.messageService.add({ severity: 'warn', summary, detail });
4237
+ showWarn(detail, summary) {
4238
+ this.messageService.add({ severity: 'warn', summary: summary ?? this.t('shared.warning'), detail });
4239
+ }
4240
+ /**
4241
+ * Translate a key using the adapter, fallback to key if not available.
4242
+ */
4243
+ t(key, variables) {
4244
+ return this.translateAdapter?.translate(key, variables) ?? key;
3211
4245
  }
3212
4246
  /**
3213
4247
  * Delete an item with confirmation dialog
@@ -3219,7 +4253,7 @@ class BaseListPage {
3219
4253
  onDelete(item, idGetter, deleteApiCall, options) {
3220
4254
  this.confirmationService.confirm({
3221
4255
  message: this.getDeleteConfirmMessage(item),
3222
- header: options?.header ?? 'Confirm Delete',
4256
+ header: options?.header ?? this.t('shared.confirm.delete.header'),
3223
4257
  icon: 'pi pi-exclamation-triangle',
3224
4258
  acceptButtonStyleClass: 'p-button-danger',
3225
4259
  accept: () => {
@@ -3227,11 +4261,11 @@ class BaseListPage {
3227
4261
  .pipe(takeUntilDestroyed(this.destroyRef))
3228
4262
  .subscribe({
3229
4263
  next: () => {
3230
- this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
4264
+ this.showSuccess(options?.successMessage ?? this.t('shared.delete.success'));
3231
4265
  this.loadData();
3232
4266
  },
3233
4267
  error: () => {
3234
- this.showError(options?.errorMessage ?? 'Failed to delete item.');
4268
+ this.showError(options?.errorMessage ?? this.t('shared.delete.failed'));
3235
4269
  },
3236
4270
  });
3237
4271
  },
@@ -3247,17 +4281,17 @@ class BaseListPage {
3247
4281
  async onDeleteAsync(item, idGetter, deleteApiCall, options) {
3248
4282
  this.confirmationService.confirm({
3249
4283
  message: this.getDeleteConfirmMessage(item),
3250
- header: options?.header ?? 'Confirm Delete',
4284
+ header: options?.header ?? this.t('shared.confirm.delete.header'),
3251
4285
  icon: 'pi pi-exclamation-triangle',
3252
4286
  acceptButtonStyleClass: 'p-button-danger',
3253
4287
  accept: async () => {
3254
4288
  try {
3255
4289
  await deleteApiCall(idGetter(item));
3256
- this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
4290
+ this.showSuccess(options?.successMessage ?? this.t('shared.delete.success'));
3257
4291
  await this.loadData();
3258
4292
  }
3259
4293
  catch {
3260
- this.showError(options?.errorMessage ?? 'Failed to delete item.');
4294
+ this.showError(options?.errorMessage ?? this.t('shared.delete.failed'));
3261
4295
  }
3262
4296
  },
3263
4297
  });
@@ -3281,5 +4315,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
3281
4315
  * Generated bundle index. Do not edit.
3282
4316
  */
3283
4317
 
3284
- 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 };
4318
+ 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, EVENT_PARTICIPANT_PERMISSIONS, EVENT_PERMISSIONS, EditModeElementChangerDirective, FILE_PERMISSIONS, FILE_PROVIDER, FILE_TYPE_FILTERS, FOLDER_PERMISSIONS, FORM_PERMISSIONS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LANGUAGE_PERMISSIONS, LazyMultiSelectComponent, LazySelectComponent, NOTIFICATION_PERMISSIONS, PERMISSIONS, PROFILE_PERMISSION_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, ROLE_ACTION_PERMISSIONS, ROLE_PERMISSIONS, SHARED_MESSAGES, STORAGE_CONFIG_PERMISSIONS, TRANSLATION_KEY_PERMISSIONS, TRANSLATION_PERMISSIONS, TranslatePipe, 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, provideFallbackLocalization, provideValueAccessor, resolveTranslationModule };
3285
4319
  //# sourceMappingURL=flusys-ng-shared.mjs.map