@flusys/ng-shared 3.0.1 → 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,8 +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, ApplicationRef, viewChild, afterNextRender, ViewEncapsulation, DestroyRef, InjectionToken, isDevMode } from '@angular/core';
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
3
  import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe, DOCUMENT as DOCUMENT$1 } from '@angular/common';
4
4
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
5
- import { APP_CONFIG, getServiceUrl } from '@flusys/ng-core';
5
+ import { APP_CONFIG, getServiceUrl, TRANSLATE_ADAPTER, FALLBACK_MESSAGES_REGISTRY } from '@flusys/ng-core';
6
6
  import { of, firstValueFrom, map as map$1 } from 'rxjs';
7
7
  import { map, tap, catchError } from 'rxjs/operators';
8
8
  import * as i1$1 from '@angular/forms';
@@ -17,12 +17,14 @@ import * as i1 from 'primeng/checkbox';
17
17
  import { CheckboxModule } from 'primeng/checkbox';
18
18
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
19
19
  import { DatePickerModule } from 'primeng/datepicker';
20
- import * as i4 from 'primeng/dialog';
20
+ import * as i5 from 'primeng/dialog';
21
21
  import { DialogModule } from 'primeng/dialog';
22
22
  import { DividerModule } from 'primeng/divider';
23
23
  import { FileUploadModule } from 'primeng/fileupload';
24
+ import * as i6 from 'primeng/iconfield';
24
25
  import { IconFieldModule } from 'primeng/iconfield';
25
26
  import { ImageModule } from 'primeng/image';
27
+ import * as i7 from 'primeng/inputicon';
26
28
  import { InputIconModule } from 'primeng/inputicon';
27
29
  import { InputNumberModule } from 'primeng/inputnumber';
28
30
  import * as i2 from 'primeng/inputtext';
@@ -54,6 +56,7 @@ import { TooltipModule } from 'primeng/tooltip';
54
56
  import { TreeTableModule } from 'primeng/treetable';
55
57
  import { ProgressSpinnerModule } from 'primeng/progressspinner';
56
58
  import { ColorPickerModule } from 'primeng/colorpicker';
59
+ import * as i3$1 from 'primeng/api';
57
60
  import { MessageService, ConfirmationService } from 'primeng/api';
58
61
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
59
62
 
@@ -157,6 +160,24 @@ const NOTIFICATION_PERMISSIONS = {
157
160
  UPDATE: 'notification.update',
158
161
  DELETE: 'notification.delete',
159
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
+ };
160
181
  const PERMISSIONS = {
161
182
  USER: USER_PERMISSIONS,
162
183
  COMPANY: COMPANY_PERMISSIONS,
@@ -176,11 +197,383 @@ const PERMISSIONS = {
176
197
  EVENT: EVENT_PERMISSIONS,
177
198
  EVENT_PARTICIPANT: EVENT_PARTICIPANT_PERMISSIONS,
178
199
  NOTIFICATION: NOTIFICATION_PERMISSIONS,
200
+ LANGUAGE: LANGUAGE_PERMISSIONS,
201
+ TRANSLATION_KEY: TRANSLATION_KEY_PERMISSIONS,
202
+ TRANSLATION: TRANSLATION_PERMISSIONS,
179
203
  };
180
204
 
181
- /**
182
- * Common file type filters
183
- */
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 ────────────────────────────────────────────────────────────────
184
577
  const FILE_TYPE_FILTERS = {
185
578
  IMAGES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'],
186
579
  DOCUMENTS: [
@@ -194,30 +587,15 @@ const FILE_TYPE_FILTERS = {
194
587
  AUDIO: ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm'],
195
588
  ALL: [],
196
589
  };
197
- /**
198
- * Get accept string for file input from content types
199
- */
590
+ // ─── Utility Functions ────────────────────────────────────────────────────────
200
591
  function getAcceptString(contentTypes) {
201
- if (!contentTypes.length)
202
- return '*/*';
203
- return contentTypes.join(',');
592
+ return contentTypes.length ? contentTypes.join(',') : '*/*';
204
593
  }
205
- /**
206
- * Check if file matches allowed content types
207
- */
208
594
  function isFileTypeAllowed(file, allowedTypes) {
209
595
  if (!allowedTypes.length)
210
596
  return true;
211
- return allowedTypes.some((type) => {
212
- if (type.endsWith('/*')) {
213
- return file.type.startsWith(type.replace('/*', '/'));
214
- }
215
- return file.type === type;
216
- });
597
+ return allowedTypes.some((type) => type.endsWith('/*') ? file.type.startsWith(type.replace('/*', '/')) : file.type === type);
217
598
  }
218
- /**
219
- * Get file icon class based on content type
220
- */
221
599
  function getFileIconClass(contentType) {
222
600
  if (contentType.startsWith('image/'))
223
601
  return 'pi pi-image';
@@ -233,9 +611,6 @@ function getFileIconClass(contentType) {
233
611
  return 'pi pi-file-excel';
234
612
  return 'pi pi-file';
235
613
  }
236
- /**
237
- * Format file size for display
238
- */
239
614
  function formatFileSize(sizeInKb) {
240
615
  const kb = typeof sizeInKb === 'string' ? parseFloat(sizeInKb) : sizeInKb;
241
616
  if (kb < 1024)
@@ -243,8 +618,7 @@ function formatFileSize(sizeInKb) {
243
618
  const mb = kb / 1024;
244
619
  if (mb < 1024)
245
620
  return `${mb.toFixed(1)} MB`;
246
- const gb = mb / 1024;
247
- return `${gb.toFixed(2)} GB`;
621
+ return `${(mb / 1024).toFixed(2)} GB`;
248
622
  }
249
623
 
250
624
  var ContactTypeEnum;
@@ -719,6 +1093,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
719
1093
  }]
720
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"] }] } });
721
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
+
722
1246
  class AngularModule {
723
1247
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
724
1248
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.5", ngImport: i0, type: AngularModule, imports: [CommonModule,
@@ -1218,14 +1742,13 @@ class IconComponent {
1218
1742
  <i class="pi pi-question"></i>
1219
1743
  }
1220
1744
  }
1221
- `, 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"] }] });
1222
1746
  }
1223
1747
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: IconComponent, decorators: [{
1224
1748
  type: Component,
1225
1749
  args: [{
1226
1750
  selector: 'lib-icon',
1227
1751
  imports: [AngularModule],
1228
- changeDetection: ChangeDetectionStrategy.OnPush,
1229
1752
  template: `
1230
1753
  @if (icon()) {
1231
1754
  @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
@@ -1285,6 +1808,7 @@ function checkScrollPagination(event, config) {
1285
1808
  class LazyMultiSelectComponent extends BaseFormControl {
1286
1809
  document = inject(DOCUMENT$1);
1287
1810
  appRef = inject(ApplicationRef);
1811
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1288
1812
  onDocumentClickBound = this.handleDocumentClick.bind(this);
1289
1813
  // View references
1290
1814
  pSelectRef = viewChild.required('pSelect');
@@ -1292,7 +1816,14 @@ class LazyMultiSelectComponent extends BaseFormControl {
1292
1816
  // Portal state
1293
1817
  overlayViewRef = null;
1294
1818
  // Inputs
1295
- 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" }] : []));
1296
1827
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1297
1828
  isLoading = input.required(...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1298
1829
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
@@ -1312,7 +1843,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1312
1843
  if (selectedValues.length === 0)
1313
1844
  return '';
1314
1845
  if (selectedValues.length > 3) {
1315
- return `${selectedValues.length} Items Selected`;
1846
+ return this.t('shared.multi.select.items.selected', { count: selectedValues.length });
1316
1847
  }
1317
1848
  return this.selectDataList()
1318
1849
  .filter((item) => selectedValues.includes(item.value))
@@ -1450,12 +1981,18 @@ class LazyMultiSelectComponent extends BaseFormControl {
1450
1981
  event.stopPropagation();
1451
1982
  this.value.set([]);
1452
1983
  }
1984
+ t(key, variables) {
1985
+ if (this.translateAdapter) {
1986
+ return this.translateAdapter.translate(key, variables);
1987
+ }
1988
+ return key;
1989
+ }
1453
1990
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1454
- 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\">{{ placeHolder() }}</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=\"Search...\"\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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
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 });
1455
1992
  }
1456
1993
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1457
1994
  type: Component,
1458
- args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, 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\">{{ placeHolder() }}</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=\"Search...\"\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"] }]
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"] }]
1459
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"] }] } });
1460
1997
 
1461
1998
  /**
@@ -1468,13 +2005,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1468
2005
  */
1469
2006
  class LazySelectComponent extends BaseFormControl {
1470
2007
  destroyRef = inject(DestroyRef);
2008
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1471
2009
  onScrollBound = this.onScroll.bind(this);
1472
2010
  scrollTargetEl = null;
1473
2011
  isDestroyed = false;
1474
2012
  // View references
1475
2013
  scrollContainer = viewChild.required('scrollContainer');
1476
2014
  // Inputs
1477
- 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" }] : []));
1478
2023
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
1479
2024
  optionValue = input.required(...(ngDevMode ? [{ debugName: "optionValue" }] : []));
1480
2025
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1557,12 +2102,18 @@ class LazySelectComponent extends BaseFormControl {
1557
2102
  onBlur() {
1558
2103
  this.markAsTouched();
1559
2104
  }
2105
+ t(key, variables) {
2106
+ if (this.translateAdapter) {
2107
+ return this.translateAdapter.translate(key, variables);
2108
+ }
2109
+ return key;
2110
+ }
1560
2111
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1561
- 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.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"] }] });
1562
2113
  }
1563
2114
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: LazySelectComponent, decorators: [{
1564
2115
  type: Component,
1565
- 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" }]
1566
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"] }] } });
1567
2118
 
1568
2119
  /**
@@ -1638,14 +2189,6 @@ const AUTH_STATE_PROVIDER = new InjectionToken('AUTH_STATE_PROVIDER', {
1638
2189
  * Use with `inject(PROFILE_PERMISSION_PROVIDER, { optional: true })`.
1639
2190
  */
1640
2191
  const PROFILE_PERMISSION_PROVIDER = new InjectionToken('PROFILE_PERMISSION_PROVIDER');
1641
- /**
1642
- * Profile Upload Provider Token
1643
- *
1644
- * Provides file upload functionality for profile pictures.
1645
- * Optional - if not configured or storage not enabled, upload section is hidden.
1646
- * Use with `inject(PROFILE_UPLOAD_PROVIDER, { optional: true })`.
1647
- */
1648
- const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1649
2192
  /**
1650
2193
  * User List Provider Token
1651
2194
  *
@@ -1660,6 +2203,20 @@ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1660
2203
  * ]
1661
2204
  */
1662
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');
1663
2220
 
1664
2221
  const DEFAULT_PAGE_SIZE$1 = 20;
1665
2222
  /**
@@ -1673,10 +2230,11 @@ class BaseUserSelectComponent {
1673
2230
  destroyRef = inject(DestroyRef);
1674
2231
  injector = inject(Injector);
1675
2232
  userProvider = inject(USER_PROVIDER);
2233
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
1676
2234
  abortController = null;
1677
2235
  // Inputs
1678
2236
  loadUsers = input(...(ngDevMode ? [undefined, { debugName: "loadUsers" }] : []));
1679
- placeHolder = input('Select User', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
2237
+ placeHolder = input('', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1680
2238
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1681
2239
  filterActive = input(true, ...(ngDevMode ? [{ debugName: "filterActive" }] : []));
1682
2240
  additionalFilters = input({}, ...(ngDevMode ? [{ debugName: "additionalFilters" }] : []));
@@ -1694,6 +2252,13 @@ class BaseUserSelectComponent {
1694
2252
  label: user.name || user.email,
1695
2253
  value: user.id,
1696
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" }] : []));
1697
2262
  constructor() {
1698
2263
  // Cleanup on destroy
1699
2264
  this.destroyRef.onDestroy(() => {
@@ -1786,6 +2351,12 @@ class BaseUserSelectComponent {
1786
2351
  })),
1787
2352
  })));
1788
2353
  }
2354
+ t(key, variables) {
2355
+ if (this.translateAdapter) {
2356
+ return this.translateAdapter.translate(key, variables);
2357
+ }
2358
+ return key;
2359
+ }
1789
2360
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseUserSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1790
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 });
1791
2362
  }
@@ -1845,7 +2416,7 @@ class UserSelectComponent extends BaseUserSelectComponent {
1845
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: `
1846
2417
  <lib-lazy-select
1847
2418
  [(value)]="value"
1848
- [placeHolder]="placeHolder()"
2419
+ [placeHolder]="displayPlaceholder()"
1849
2420
  [optionLabel]="'label'"
1850
2421
  [optionValue]="'value'"
1851
2422
  [isEditMode]="isEditMode()"
@@ -1856,7 +2427,7 @@ class UserSelectComponent extends BaseUserSelectComponent {
1856
2427
  (onSearch)="handleSearch($event)"
1857
2428
  (onPagination)="handlePagination($event)"
1858
2429
  />
1859
- `, 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"] }] });
1860
2431
  }
1861
2432
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserSelectComponent, decorators: [{
1862
2433
  type: Component,
@@ -1866,7 +2437,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1866
2437
  template: `
1867
2438
  <lib-lazy-select
1868
2439
  [(value)]="value"
1869
- [placeHolder]="placeHolder()"
2440
+ [placeHolder]="displayPlaceholder()"
1870
2441
  [optionLabel]="'label'"
1871
2442
  [optionValue]="'value'"
1872
2443
  [isEditMode]="isEditMode()"
@@ -1878,7 +2449,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1878
2449
  (onPagination)="handlePagination($event)"
1879
2450
  />
1880
2451
  `,
1881
- changeDetection: ChangeDetectionStrategy.OnPush,
1882
2452
  }]
1883
2453
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], userSelected: [{ type: i0.Output, args: ["userSelected"] }] } });
1884
2454
 
@@ -1930,7 +2500,7 @@ class UserMultiSelectComponent extends BaseUserSelectComponent {
1930
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: `
1931
2501
  <lib-lazy-multi-select
1932
2502
  [(value)]="value"
1933
- [placeHolder]="placeHolder()"
2503
+ [placeHolder]="displayPlaceholder()"
1934
2504
  [isEditMode]="isEditMode()"
1935
2505
  [isLoading]="isLoading()"
1936
2506
  [total]="total()"
@@ -1939,7 +2509,7 @@ class UserMultiSelectComponent extends BaseUserSelectComponent {
1939
2509
  (onSearch)="handleSearch($event)"
1940
2510
  (onPagination)="handlePagination($event)"
1941
2511
  />
1942
- `, 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"] }] });
1943
2513
  }
1944
2514
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: UserMultiSelectComponent, decorators: [{
1945
2515
  type: Component,
@@ -1949,7 +2519,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1949
2519
  template: `
1950
2520
  <lib-lazy-multi-select
1951
2521
  [(value)]="value"
1952
- [placeHolder]="placeHolder()"
2522
+ [placeHolder]="displayPlaceholder()"
1953
2523
  [isEditMode]="isEditMode()"
1954
2524
  [isLoading]="isLoading()"
1955
2525
  [total]="total()"
@@ -1959,19 +2529,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
1959
2529
  (onPagination)="handlePagination($event)"
1960
2530
  />
1961
2531
  `,
1962
- changeDetection: ChangeDetectionStrategy.OnPush,
1963
2532
  }]
1964
2533
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], usersSelected: [{ type: i0.Output, args: ["usersSelected"] }] } });
1965
2534
 
1966
2535
  /**
1967
2536
  * File Uploader Component - Drag & drop file upload with type filtering.
1968
2537
  *
1969
- * 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
+ * ```
1970
2555
  */
1971
2556
  class FileUploaderComponent {
1972
2557
  messageService = inject(MessageService);
1973
- // Required: function to upload file
1974
- 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
+ }
1975
2585
  // Inputs
1976
2586
  acceptTypes = input([], ...(ngDevMode ? [{ debugName: "acceptTypes" }] : []));
1977
2587
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
@@ -2001,14 +2611,14 @@ class FileUploaderComponent {
2001
2611
  // Check if types match predefined filters
2002
2612
  const typesStr = JSON.stringify(types);
2003
2613
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.IMAGES))
2004
- return 'Images';
2614
+ return this.t('shared.file.type.images');
2005
2615
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.DOCUMENTS))
2006
- return 'Documents';
2616
+ return this.t('shared.file.type.documents');
2007
2617
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.VIDEOS))
2008
- return 'Videos';
2618
+ return this.t('shared.file.type.videos');
2009
2619
  if (typesStr === JSON.stringify(FILE_TYPE_FILTERS.AUDIO))
2010
- return 'Audio';
2011
- 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(', ');
2012
2622
  }, ...(ngDevMode ? [{ debugName: "acceptTypesDisplay" }] : []));
2013
2623
  onDragOver(event) {
2014
2624
  event.preventDefault();
@@ -2045,12 +2655,12 @@ class FileUploaderComponent {
2045
2655
  handleFiles(files) {
2046
2656
  // Filter by type
2047
2657
  const allowedTypes = this.acceptTypes();
2048
- const validFiles = files.filter(file => {
2658
+ const validFiles = files.filter((file) => {
2049
2659
  if (!isFileTypeAllowed(file, allowedTypes)) {
2050
2660
  this.messageService.add({
2051
2661
  severity: 'warn',
2052
- summary: 'Invalid File Type',
2053
- detail: `File type not allowed: ${file.name}`,
2662
+ summary: this.t('shared.upload.invalid.type'),
2663
+ detail: `${file.name}`,
2054
2664
  });
2055
2665
  return false;
2056
2666
  }
@@ -2058,12 +2668,12 @@ class FileUploaderComponent {
2058
2668
  });
2059
2669
  // Filter by size
2060
2670
  const maxSize = this.maxSizeMb() * 1024 * 1024;
2061
- const sizeValidFiles = validFiles.filter(file => {
2671
+ const sizeValidFiles = validFiles.filter((file) => {
2062
2672
  if (file.size > maxSize) {
2063
2673
  this.messageService.add({
2064
2674
  severity: 'warn',
2065
- summary: 'File Too Large',
2066
- 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')})`,
2067
2677
  });
2068
2678
  return false;
2069
2679
  }
@@ -2082,37 +2692,24 @@ class FileUploaderComponent {
2082
2692
  }
2083
2693
  }
2084
2694
  removeFile(file) {
2085
- this.selectedFiles.update(files => files.filter(f => f !== file));
2695
+ this.selectedFiles.update((files) => files.filter((f) => f !== file));
2086
2696
  }
2087
2697
  async uploadFiles(files) {
2088
2698
  const filesToUpload = files ?? this.selectedFiles();
2089
2699
  if (!filesToUpload.length || this.isUploading())
2090
2700
  return;
2091
2701
  this.isUploading.set(true);
2092
- const uploadedFiles = [];
2093
2702
  try {
2094
- for (const file of filesToUpload) {
2095
- this.uploadingFileName.set(file.name);
2096
- this.uploadProgress.set(0);
2097
- const response = await firstValueFrom(this.uploadFile()(file, this.uploadOptions()));
2098
- if (response.success && response.data) {
2099
- uploadedFiles.push(response.data);
2100
- this.fileUploaded.emit(response.data);
2101
- }
2102
- else {
2103
- throw new Error(response.message || 'Upload failed');
2104
- }
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);
2105
2710
  }
2106
- this.filesUploaded.emit(uploadedFiles);
2107
- this.selectedFiles.set([]);
2108
- this.messageService.add({
2109
- severity: 'success',
2110
- summary: 'Upload Complete',
2111
- detail: `${uploadedFiles.length} file(s) uploaded successfully`,
2112
- });
2113
2711
  }
2114
2712
  catch (error) {
2115
- // Error toast handled by global interceptor
2116
2713
  this.onError.emit(error);
2117
2714
  }
2118
2715
  finally {
@@ -2121,54 +2718,113 @@ class FileUploaderComponent {
2121
2718
  this.uploadProgress.set(0);
2122
2719
  }
2123
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
+ }
2124
2762
  getFileIcon(file) {
2125
2763
  return getFileIconClass(file.type);
2126
2764
  }
2127
2765
  formatSize(bytes) {
2128
2766
  const kb = bytes / 1024;
2129
2767
  if (kb < 1024)
2130
- return `${kb.toFixed(1)} KB`;
2768
+ return `${kb.toFixed(1)} ${this.t('shared.units.kb')}`;
2131
2769
  const mb = kb / 1024;
2132
- 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;
2133
2777
  }
2134
2778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2135
- 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: `
2136
- <div
2137
- class="w-full"
2138
- [class.opacity-60]="disabled()"
2139
- (dragover)="onDragOver($event)"
2140
- (dragleave)="onDragLeave($event)"
2141
- (drop)="onDrop($event)"
2142
- >
2143
- <!-- 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 {
2144
2788
  <div
2145
- 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"
2146
- [class.drag-over]="isDragOver()"
2147
- [class.cursor-not-allowed]="disabled()"
2148
- (click)="fileInput.click()"
2789
+ class="w-full"
2790
+ [class.opacity-60]="disabled()"
2791
+ (dragover)="onDragOver($event)"
2792
+ (dragleave)="onDragLeave($event)"
2793
+ (drop)="onDrop($event)"
2149
2794
  >
2150
- @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()) {
2151
2803
  <div class="flex flex-col items-center">
2152
2804
  <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2153
- <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>
2154
2806
  @if (uploadProgress() > 0) {
2155
- <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
+ />
2156
2812
  }
2157
2813
  </div>
2158
2814
  } @else {
2159
2815
  <div class="flex flex-col items-center">
2160
2816
  <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2161
2817
  <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2162
- {{ 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) }}
2163
2819
  </p>
2164
2820
  <p class="text-xs sm:text-sm text-color-secondary px-2">
2165
2821
  @if (acceptTypesDisplay()) {
2166
- Allowed: {{ acceptTypesDisplay() }}
2822
+ {{ 'shared.upload.allowed.types' | translate }} {{ acceptTypesDisplay() }}
2167
2823
  } @else {
2168
- All file types allowed
2824
+ {{ 'shared.upload.all.types.allowed' | translate }}
2169
2825
  }
2170
2826
  @if (maxSizeMb()) {
2171
- <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2827
+ <span class="whitespace-nowrap">{{ 'shared.upload.max.size' | translate: { size: maxSizeMb() } }}</span>
2172
2828
  }
2173
2829
  </p>
2174
2830
  </div>
@@ -2190,10 +2846,20 @@ class FileUploaderComponent {
2190
2846
  @if (selectedFiles().length > 0 && showPreview()) {
2191
2847
  <div class="mt-3 space-y-2">
2192
2848
  @for (file of selectedFiles(); track file.name) {
2193
- <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2194
- <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2195
- <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2196
- <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
+ >
2197
2863
  <p-button
2198
2864
  icon="pi pi-times"
2199
2865
  [text]="true"
@@ -2207,48 +2873,61 @@ class FileUploaderComponent {
2207
2873
  }
2208
2874
  </div>
2209
2875
  }
2210
- </div>
2211
- `, 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" }] });
2212
2879
  }
2213
2880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileUploaderComponent, decorators: [{
2214
2881
  type: Component,
2215
- args: [{ selector: 'lib-file-uploader', imports: [AngularModule, PrimeModule], template: `
2216
- <div
2217
- class="w-full"
2218
- [class.opacity-60]="disabled()"
2219
- (dragover)="onDragOver($event)"
2220
- (dragleave)="onDragLeave($event)"
2221
- (drop)="onDrop($event)"
2222
- >
2223
- <!-- 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 {
2224
2891
  <div
2225
- 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"
2226
- [class.drag-over]="isDragOver()"
2227
- [class.cursor-not-allowed]="disabled()"
2228
- (click)="fileInput.click()"
2892
+ class="w-full"
2893
+ [class.opacity-60]="disabled()"
2894
+ (dragover)="onDragOver($event)"
2895
+ (dragleave)="onDragLeave($event)"
2896
+ (drop)="onDrop($event)"
2229
2897
  >
2230
- @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()) {
2231
2906
  <div class="flex flex-col items-center">
2232
2907
  <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2233
- <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>
2234
2909
  @if (uploadProgress() > 0) {
2235
- <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
+ />
2236
2915
  }
2237
2916
  </div>
2238
2917
  } @else {
2239
2918
  <div class="flex flex-col items-center">
2240
2919
  <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2241
2920
  <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2242
- {{ 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) }}
2243
2922
  </p>
2244
2923
  <p class="text-xs sm:text-sm text-color-secondary px-2">
2245
2924
  @if (acceptTypesDisplay()) {
2246
- Allowed: {{ acceptTypesDisplay() }}
2925
+ {{ 'shared.upload.allowed.types' | translate }} {{ acceptTypesDisplay() }}
2247
2926
  } @else {
2248
- All file types allowed
2927
+ {{ 'shared.upload.all.types.allowed' | translate }}
2249
2928
  }
2250
2929
  @if (maxSizeMb()) {
2251
- <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2930
+ <span class="whitespace-nowrap">{{ 'shared.upload.max.size' | translate: { size: maxSizeMb() } }}</span>
2252
2931
  }
2253
2932
  </p>
2254
2933
  </div>
@@ -2270,10 +2949,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2270
2949
  @if (selectedFiles().length > 0 && showPreview()) {
2271
2950
  <div class="mt-3 space-y-2">
2272
2951
  @for (file of selectedFiles(); track file.name) {
2273
- <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2274
- <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2275
- <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2276
- <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
+ >
2277
2966
  <p-button
2278
2967
  icon="pi pi-times"
2279
2968
  [text]="true"
@@ -2287,71 +2976,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2287
2976
  }
2288
2977
  </div>
2289
2978
  }
2290
- </div>
2291
- `, 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"] }]
2292
- }], 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"] }] } });
2293
2983
 
2294
2984
  const DEFAULT_PAGE_SIZE = 20;
2985
+ const DEFAULT_SELECTOR_PAGE_SIZE = 50;
2295
2986
  /**
2296
- * File Selector Dialog - Browse and select existing files with filtering.
2987
+ * File Selector Dialog - Self-contained file browser with upload support.
2297
2988
  *
2298
- * 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.
2299
2991
  *
2300
2992
  * Features:
2301
2993
  * - Search with debouncing
2302
- * - File type filtering
2994
+ * - File type filtering (acceptTypes)
2995
+ * - Folder filtering
2303
2996
  * - Infinite scroll pagination
2304
2997
  * - Single or multiple selection
2998
+ * - Built-in file upload (withUploader)
2305
2999
  * - File preview with icons
2306
3000
  *
2307
3001
  * @example
2308
- * ```typescript
2309
- * // In component
2310
- * readonly fileService = inject(FileManagerApiService);
2311
- *
2312
- * readonly loadFiles: LoadFilesFn = (filter) =>
2313
- * this.fileService.getAll(filter.search, {
2314
- * pagination: { currentPage: filter.page, pageSize: filter.pageSize },
2315
- * filter: { contentTypes: filter.contentTypes },
2316
- * }).pipe(
2317
- * map(res => ({
2318
- * ...res,
2319
- * data: res.data?.map(f => ({
2320
- * id: f.id,
2321
- * name: f.name,
2322
- * contentType: f.contentType,
2323
- * size: f.size,
2324
- * url: f.url
2325
- * }))
2326
- * }))
2327
- * );
2328
- * ```
2329
- *
2330
3002
  * ```html
2331
3003
  * <lib-file-selector-dialog
2332
- * [(visible)]="showFileSelector"
2333
- * [loadFiles]="loadFiles"
3004
+ * [(visible)]="showSelector"
2334
3005
  * [acceptTypes]="['image/*']"
2335
- * [multiple]="false"
2336
- * (fileSelected)="onFileSelected($event)"
3006
+ * [multiple]="true"
3007
+ * [withUploader]="true"
3008
+ * (filesSelected)="onFilesSelected($event)"
2337
3009
  * />
2338
3010
  * ```
2339
3011
  */
2340
3012
  class FileSelectorDialogComponent {
2341
3013
  destroyRef = inject(DestroyRef);
3014
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
3015
+ fileProvider = inject(FILE_PROVIDER, { optional: true });
2342
3016
  abortController = null;
2343
3017
  searchDebounceTimer = null;
2344
- // Required: function to load files
2345
- loadFiles = input.required(...(ngDevMode ? [{ debugName: "loadFiles" }] : []));
2346
- // Inputs
2347
- header = input('Select File', ...(ngDevMode ? [{ debugName: "header" }] : []));
3018
+ // Configuration inputs
3019
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : []));
2348
3020
  acceptTypes = input([], ...(ngDevMode ? [{ debugName: "acceptTypes" }] : []));
2349
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" }] : []));
2350
3029
  maxSelection = input(10, ...(ngDevMode ? [{ debugName: "maxSelection" }] : []));
2351
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" }] : []));
2352
3033
  // Two-way visibility binding
2353
3034
  visible = model(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
2354
- // Outputs
3035
+ // Outputs - return file IDs
2355
3036
  fileSelected = output();
2356
3037
  filesSelected = output();
2357
3038
  closed = output();
@@ -2361,10 +3042,48 @@ class FileSelectorDialogComponent {
2361
3042
  files = signal([], ...(ngDevMode ? [{ debugName: "files" }] : []));
2362
3043
  selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : []));
2363
3044
  total = signal(undefined, ...(ngDevMode ? [{ debugName: "total" }] : []));
2364
- 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" }] : []));
2365
3049
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
2366
- // Computed
2367
- 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;
2368
3087
  constructor() {
2369
3088
  this.destroyRef.onDestroy(() => {
2370
3089
  this.abortController?.abort();
@@ -2372,13 +3091,17 @@ class FileSelectorDialogComponent {
2372
3091
  clearTimeout(this.searchDebounceTimer);
2373
3092
  }
2374
3093
  });
2375
- // Load files when dialog becomes visible
3094
+ // Load files and storage configs when dialog becomes visible
2376
3095
  effect(() => {
2377
3096
  const isVisible = this.visible();
2378
3097
  if (isVisible) {
2379
3098
  untracked(() => {
2380
3099
  this.resetState();
2381
3100
  this.fetchFiles();
3101
+ // Preload storage configs for upload default
3102
+ if (this.fileProvider?.loadStorageConfigs) {
3103
+ this.loadStorageConfigs();
3104
+ }
2382
3105
  });
2383
3106
  }
2384
3107
  });
@@ -2391,7 +3114,6 @@ class FileSelectorDialogComponent {
2391
3114
  });
2392
3115
  }
2393
3116
  onSearchChange(value) {
2394
- // Debounce search
2395
3117
  if (this.searchDebounceTimer) {
2396
3118
  clearTimeout(this.searchDebounceTimer);
2397
3119
  }
@@ -2471,15 +3193,131 @@ class FileSelectorDialogComponent {
2471
3193
  onDialogHide() {
2472
3194
  this.closed.emit();
2473
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
+ }
2474
3292
  resetState() {
3293
+ // Cancel any pending request and reset loading state
3294
+ this.abortController?.abort();
3295
+ this.abortController = null;
3296
+ this.isLoading.set(false);
2475
3297
  this.files.set([]);
2476
3298
  this.selectedFiles.set([]);
2477
3299
  this.searchTerm.set('');
2478
3300
  this.pagination.set({ pageSize: this.pageSize(), currentPage: 0 });
2479
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);
2480
3318
  }
2481
3319
  async fetchFiles(append = false) {
2482
- if (this.isLoading())
3320
+ if (!this.fileProvider || this.isLoading())
2483
3321
  return;
2484
3322
  this.abortController?.abort();
2485
3323
  this.abortController = new AbortController();
@@ -2490,15 +3328,33 @@ class FileSelectorDialogComponent {
2490
3328
  page: pag.currentPage,
2491
3329
  pageSize: pag.pageSize,
2492
3330
  search: this.searchTerm(),
2493
- 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,
2494
3336
  };
2495
- const response = await firstValueFrom(this.loadFiles()(filter));
3337
+ const response = await firstValueFrom(this.fileProvider.loadFiles(filter));
2496
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
+ }
2497
3353
  if (append) {
2498
- this.files.update((current) => [...current, ...response.data]);
3354
+ this.files.update((current) => [...current, ...files]);
2499
3355
  }
2500
3356
  else {
2501
- this.files.set(response.data);
3357
+ this.files.set(files);
2502
3358
  }
2503
3359
  this.total.set(response.meta?.total);
2504
3360
  }
@@ -2512,10 +3368,16 @@ class FileSelectorDialogComponent {
2512
3368
  this.isLoading.set(false);
2513
3369
  }
2514
3370
  }
3371
+ t(key, variables) {
3372
+ if (this.translateAdapter) {
3373
+ return this.translateAdapter.translate(key, variables);
3374
+ }
3375
+ return key;
3376
+ }
2515
3377
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2516
- 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: `
2517
3379
  <p-dialog
2518
- [header]="header()"
3380
+ [header]="dialogHeader()"
2519
3381
  [(visible)]="visible"
2520
3382
  [modal]="true"
2521
3383
  [closable]="true"
@@ -2526,107 +3388,246 @@ class FileSelectorDialogComponent {
2526
3388
  styleClass="file-selector-dialog"
2527
3389
  (onHide)="onDialogHide()"
2528
3390
  >
2529
- <!-- Search Bar -->
2530
- <div class="flex flex-col sm:flex-row gap-2 mb-3">
2531
- <span class="p-input-icon-left flex-1">
2532
- <i class="pi pi-search"></i>
2533
- <input
2534
- pInputText
2535
- type="text"
2536
- [ngModel]="searchTerm()"
2537
- (ngModelChange)="onSearchChange($event)"
2538
- placeholder="Search files..."
2539
- class="w-full"
2540
- />
2541
- </span>
2542
- @if (multiple()) {
2543
- <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2544
- {{ selectedFiles().length }} selected
2545
- </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>
2546
3426
  }
2547
- </div>
2548
3427
 
2549
- <!-- File Grid - Responsive columns -->
2550
- <div
2551
- class="file-grid"
2552
- #scrollContainer
2553
- (scroll)="onScroll($event)"
2554
- >
2555
- @if (isLoading() && files().length === 0) {
2556
- <div class="col-span-full flex justify-center p-4">
2557
- <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2558
- </div>
2559
- } @else if (files().length === 0) {
2560
- <div class="col-span-full text-center p-4 text-color-secondary">
2561
- <i class="pi pi-inbox text-4xl mb-2 block"></i>
2562
- <p>No files found</p>
2563
- </div>
2564
- } @else {
2565
- @for (file of files(); track file.id) {
2566
- <div
2567
- class="file-card"
2568
- [class.selected]="isSelected(file)"
2569
- [class.disabled]="!isFileAllowed(file)"
2570
- (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()"
2571
3461
  >
2572
- <!-- File Preview -->
2573
- <div class="file-preview">
2574
- @if (isImage(file) && file.url) {
2575
- <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2576
- } @else {
2577
- <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2578
- }
2579
- @if (isSelected(file)) {
2580
- <div class="selected-overlay">
2581
- <i class="pi pi-check text-xl sm:text-2xl"></i>
2582
- </div>
2583
- }
2584
- </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
+ }
2585
3476
 
2586
- <!-- File Info -->
2587
- <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2588
- <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2589
- {{ file.name }}
2590
- </span>
2591
- <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2592
- </div>
2593
- </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>
2594
3519
  }
2595
3520
 
2596
- @if (isLoading()) {
2597
- <div class="col-span-full flex justify-center p-2">
2598
- <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2599
- </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>
2600
3528
  }
2601
- }
2602
- </div>
3529
+ </div>
2603
3530
 
2604
- <!-- Footer -->
2605
- <ng-template #footer>
2606
- <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2607
- <button
2608
- pButton
2609
- label="Cancel"
2610
- class="p-button-text w-full sm:w-auto"
2611
- (click)="onCancel()"
2612
- ></button>
2613
- <button
2614
- pButton
2615
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2616
- [disabled]="selectedFiles().length === 0"
2617
- class="w-full sm:w-auto"
2618
- (click)="onConfirm()"
2619
- ></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
+ }
2620
3594
  </div>
2621
- </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
+ }
2622
3623
  </p-dialog>
2623
- `, 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" }] });
2624
3625
  }
2625
3626
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2626
3627
  type: Component,
2627
- args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule], template: `
3628
+ args: [{ selector: 'lib-file-selector-dialog', imports: [AngularModule, PrimeModule, FileUploaderComponent, TranslatePipe], template: `
2628
3629
  <p-dialog
2629
- [header]="header()"
3630
+ [header]="dialogHeader()"
2630
3631
  [(visible)]="visible"
2631
3632
  [modal]="true"
2632
3633
  [closable]="true"
@@ -2637,102 +3638,241 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
2637
3638
  styleClass="file-selector-dialog"
2638
3639
  (onHide)="onDialogHide()"
2639
3640
  >
2640
- <!-- Search Bar -->
2641
- <div class="flex flex-col sm:flex-row gap-2 mb-3">
2642
- <span class="p-input-icon-left flex-1">
2643
- <i class="pi pi-search"></i>
2644
- <input
2645
- pInputText
2646
- type="text"
2647
- [ngModel]="searchTerm()"
2648
- (ngModelChange)="onSearchChange($event)"
2649
- placeholder="Search files..."
2650
- class="w-full"
2651
- />
2652
- </span>
2653
- @if (multiple()) {
2654
- <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2655
- {{ selectedFiles().length }} selected
2656
- </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>
2657
3676
  }
2658
- </div>
2659
3677
 
2660
- <!-- File Grid - Responsive columns -->
2661
- <div
2662
- class="file-grid"
2663
- #scrollContainer
2664
- (scroll)="onScroll($event)"
2665
- >
2666
- @if (isLoading() && files().length === 0) {
2667
- <div class="col-span-full flex justify-center p-4">
2668
- <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2669
- </div>
2670
- } @else if (files().length === 0) {
2671
- <div class="col-span-full text-center p-4 text-color-secondary">
2672
- <i class="pi pi-inbox text-4xl mb-2 block"></i>
2673
- <p>No files found</p>
2674
- </div>
2675
- } @else {
2676
- @for (file of files(); track file.id) {
2677
- <div
2678
- class="file-card"
2679
- [class.selected]="isSelected(file)"
2680
- [class.disabled]="!isFileAllowed(file)"
2681
- (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()"
2682
3711
  >
2683
- <!-- File Preview -->
2684
- <div class="file-preview">
2685
- @if (isImage(file) && file.url) {
2686
- <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2687
- } @else {
2688
- <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2689
- }
2690
- @if (isSelected(file)) {
2691
- <div class="selected-overlay">
2692
- <i class="pi pi-check text-xl sm:text-2xl"></i>
2693
- </div>
2694
- }
2695
- </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
+ }
2696
3726
 
2697
- <!-- File Info -->
2698
- <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2699
- <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2700
- {{ file.name }}
2701
- </span>
2702
- <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2703
- </div>
2704
- </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>
2705
3769
  }
2706
3770
 
2707
- @if (isLoading()) {
2708
- <div class="col-span-full flex justify-center p-2">
2709
- <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2710
- </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>
2711
3778
  }
2712
- }
2713
- </div>
3779
+ </div>
2714
3780
 
2715
- <!-- Footer -->
2716
- <ng-template #footer>
2717
- <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2718
- <button
2719
- pButton
2720
- label="Cancel"
2721
- class="p-button-text w-full sm:w-auto"
2722
- (click)="onCancel()"
2723
- ></button>
2724
- <button
2725
- pButton
2726
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2727
- [disabled]="selectedFiles().length === 0"
2728
- class="w-full sm:w-auto"
2729
- (click)="onConfirm()"
2730
- ></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
+ }
2731
3844
  </div>
2732
- </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
+ }
2733
3873
  </p-dialog>
2734
- `, 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"] }]
2735
- }], 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"] }] } });
2736
3876
 
2737
3877
  function createGuard(guardName, redirectTo, evaluate, getDenialMessage) {
2738
3878
  return () => {
@@ -2828,8 +3968,7 @@ function allPermissionsGuard(permissions, redirectTo = '/') {
2828
3968
  * @Component({
2829
3969
  * selector: 'app-product-form',
2830
3970
  * standalone: true,
2831
- * changeDetection: ChangeDetectionStrategy.OnPush,
2832
- * template: `...`
3971
+ * * template: `...`
2833
3972
  * })
2834
3973
  * export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
2835
3974
  * private readonly productService = inject(ProductApiService);
@@ -2896,6 +4035,7 @@ class BaseFormPage {
2896
4035
  route = inject(ActivatedRoute);
2897
4036
  messageService = inject(MessageService);
2898
4037
  destroyRef = inject(DestroyRef);
4038
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
2899
4039
  routeParams = toSignal(this.route.paramMap);
2900
4040
  initialized = false;
2901
4041
  /** Loading state for async operations */
@@ -2933,8 +4073,8 @@ class BaseFormPage {
2933
4073
  : this.createItem(model);
2934
4074
  operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
2935
4075
  next: () => {
2936
- const action = this.isEditMode() ? 'updated' : 'created';
2937
- this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
4076
+ const messageKey = this.isEditMode() ? 'shared.update.success' : 'shared.create.success';
4077
+ this.showSuccess(this.t(messageKey));
2938
4078
  this.router.navigate([this.getResourceRoute()]);
2939
4079
  },
2940
4080
  error: () => {
@@ -2959,8 +4099,8 @@ class BaseFormPage {
2959
4099
  showValidationError() {
2960
4100
  this.messageService.add({
2961
4101
  severity: 'error',
2962
- summary: 'Validation Error',
2963
- detail: 'Please fill in all required fields.',
4102
+ summary: this.t('shared.validation.error'),
4103
+ detail: this.t('shared.fill.all.fields'),
2964
4104
  });
2965
4105
  }
2966
4106
  /**
@@ -2970,7 +4110,7 @@ class BaseFormPage {
2970
4110
  showSuccess(detail) {
2971
4111
  this.messageService.add({
2972
4112
  severity: 'success',
2973
- summary: 'Success',
4113
+ summary: this.t('shared.success'),
2974
4114
  detail,
2975
4115
  });
2976
4116
  }
@@ -2981,10 +4121,16 @@ class BaseFormPage {
2981
4121
  showError(detail) {
2982
4122
  this.messageService.add({
2983
4123
  severity: 'error',
2984
- summary: 'Error',
4124
+ summary: this.t('shared.error'),
2985
4125
  detail,
2986
4126
  });
2987
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
+ }
2988
4134
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2989
4135
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.5", type: BaseFormPage, isStandalone: true, ngImport: i0 });
2990
4136
  }
@@ -3021,6 +4167,7 @@ class BaseListPage {
3021
4167
  appConfig = inject(APP_CONFIG);
3022
4168
  confirmationService = inject(ConfirmationService);
3023
4169
  destroyRef = inject(DestroyRef);
4170
+ translateAdapter = inject(TRANSLATE_ADAPTER, { optional: true });
3024
4171
  /** Items list */
3025
4172
  items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3026
4173
  /** Loading state */
@@ -3069,26 +4216,32 @@ class BaseListPage {
3069
4216
  /**
3070
4217
  * Show success toast message
3071
4218
  */
3072
- showSuccess(detail, summary = 'Success') {
3073
- this.messageService.add({ severity: 'success', summary, detail });
4219
+ showSuccess(detail, summary) {
4220
+ this.messageService.add({ severity: 'success', summary: summary ?? this.t('shared.success'), detail });
3074
4221
  }
3075
4222
  /**
3076
4223
  * Show error toast message
3077
4224
  */
3078
- showError(detail, summary = 'Error') {
3079
- this.messageService.add({ severity: 'error', summary, detail });
4225
+ showError(detail, summary) {
4226
+ this.messageService.add({ severity: 'error', summary: summary ?? this.t('shared.error'), detail });
3080
4227
  }
3081
4228
  /**
3082
4229
  * Show info toast message
3083
4230
  */
3084
- showInfo(detail, summary = 'Info') {
3085
- this.messageService.add({ severity: 'info', summary, detail });
4231
+ showInfo(detail, summary) {
4232
+ this.messageService.add({ severity: 'info', summary: summary ?? this.t('shared.info'), detail });
3086
4233
  }
3087
4234
  /**
3088
4235
  * Show warning toast message
3089
4236
  */
3090
- showWarn(detail, summary = 'Warning') {
3091
- 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;
3092
4245
  }
3093
4246
  /**
3094
4247
  * Delete an item with confirmation dialog
@@ -3100,7 +4253,7 @@ class BaseListPage {
3100
4253
  onDelete(item, idGetter, deleteApiCall, options) {
3101
4254
  this.confirmationService.confirm({
3102
4255
  message: this.getDeleteConfirmMessage(item),
3103
- header: options?.header ?? 'Confirm Delete',
4256
+ header: options?.header ?? this.t('shared.confirm.delete.header'),
3104
4257
  icon: 'pi pi-exclamation-triangle',
3105
4258
  acceptButtonStyleClass: 'p-button-danger',
3106
4259
  accept: () => {
@@ -3108,11 +4261,11 @@ class BaseListPage {
3108
4261
  .pipe(takeUntilDestroyed(this.destroyRef))
3109
4262
  .subscribe({
3110
4263
  next: () => {
3111
- this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
4264
+ this.showSuccess(options?.successMessage ?? this.t('shared.delete.success'));
3112
4265
  this.loadData();
3113
4266
  },
3114
4267
  error: () => {
3115
- this.showError(options?.errorMessage ?? 'Failed to delete item.');
4268
+ this.showError(options?.errorMessage ?? this.t('shared.delete.failed'));
3116
4269
  },
3117
4270
  });
3118
4271
  },
@@ -3128,17 +4281,17 @@ class BaseListPage {
3128
4281
  async onDeleteAsync(item, idGetter, deleteApiCall, options) {
3129
4282
  this.confirmationService.confirm({
3130
4283
  message: this.getDeleteConfirmMessage(item),
3131
- header: options?.header ?? 'Confirm Delete',
4284
+ header: options?.header ?? this.t('shared.confirm.delete.header'),
3132
4285
  icon: 'pi pi-exclamation-triangle',
3133
4286
  acceptButtonStyleClass: 'p-button-danger',
3134
4287
  accept: async () => {
3135
4288
  try {
3136
4289
  await deleteApiCall(idGetter(item));
3137
- this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
4290
+ this.showSuccess(options?.successMessage ?? this.t('shared.delete.success'));
3138
4291
  await this.loadData();
3139
4292
  }
3140
4293
  catch {
3141
- this.showError(options?.errorMessage ?? 'Failed to delete item.');
4294
+ this.showError(options?.errorMessage ?? this.t('shared.delete.failed'));
3142
4295
  }
3143
4296
  },
3144
4297
  });
@@ -3162,5 +4315,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImpor
3162
4315
  * Generated bundle index. Do not edit.
3163
4316
  */
3164
4317
 
3165
- 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_TYPE_FILTERS, FOLDER_PERMISSIONS, FORM_PERMISSIONS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, NOTIFICATION_PERMISSIONS, 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 };
3166
4319
  //# sourceMappingURL=flusys-ng-shared.mjs.map