cloud-ide-element 1.0.81 → 1.0.83

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,7 +1,7 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule, NgTemplateOutlet } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, afterRenderEffect, afterNextRender, effect, Directive, ElementRef, viewChild } from '@angular/core';
4
+ import { Pipe, Injectable, inject, EventEmitter, ViewContainerRef, forwardRef, ViewChild, Output, Input, Component, HostListener, ContentChildren, signal, DestroyRef, computed, effect, afterRenderEffect, afterNextRender, ElementRef, Directive, viewChild } from '@angular/core';
5
5
  import * as i2 from '@angular/forms';
6
6
  import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
7
7
  import { BehaviorSubject, Subject, debounceTime, takeUntil, distinctUntilChanged, Observable, retry, catchError, finalize, throwError } from 'rxjs';
@@ -3148,1682 +3148,1888 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
3148
3148
  }]
3149
3149
  }], ctorParameters: () => [] });
3150
3150
 
3151
- class CideEleFileInputComponent {
3152
- fileManagerService = inject(CideEleFileManagerService);
3153
- notificationService = inject(NotificationService);
3154
- elementService = inject(CideElementsService);
3151
+ class CideEleFloatingFileUploaderComponent {
3155
3152
  destroyRef = inject(DestroyRef);
3156
- // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3157
- // Traditional @Input() decorators
3158
- label = 'Choose file';
3159
- accept = '';
3160
- multiple = false;
3161
- disabled = false;
3162
- required = false;
3163
- helperText = '';
3164
- errorText = '';
3165
- showPreview = false;
3166
- previewWidth = '200px';
3167
- previewHeight = '200px';
3168
- previewBoxMode = false;
3169
- showFileName = true;
3170
- placeholderText = 'Click to select image';
3171
- placeholderIcon = '📷';
3172
- autoUpload = false;
3173
- uploadData = {};
3174
- // Traditional @Output() decorators
3175
- fileChange = new EventEmitter();
3176
- uploadSuccess = new EventEmitter();
3177
- uploadError = new EventEmitter();
3178
- uploadProgressChange = new EventEmitter();
3179
- // Readable signals created from @Input() decorator values
3180
- labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3181
- acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
3182
- multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
3183
- disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
3184
- requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
3185
- helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
3186
- errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
3187
- showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
3188
- previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
3189
- previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
3190
- previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
3191
- showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
3192
- placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
3193
- placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
3194
- autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
3195
- uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
3196
- // Reactive state with signals
3197
- id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
3198
- isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
3199
- uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
3200
- uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
3201
- files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
3202
- fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
3203
- previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
3204
- uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
3205
- isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
3206
- groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
3207
- isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
3208
- // Computed signals for better relationships
3209
- hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
3210
- canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
3211
- isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
3212
- isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
3213
- // Angular 20: Computed values using new features
3214
- totalFileSize = computed(() => {
3215
- if (!this.files())
3216
- return 0;
3217
- return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
3218
- }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
3219
- fileSizeInMB = computed(() => {
3220
- // Angular 20: Using ** operator for exponentiation
3221
- return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
3222
- }, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
3223
- uploadProgressPercentage = computed(() => {
3224
- // Angular 20: Using ** operator for exponentiation
3225
- return Math.round(this.uploadProgress() ** 1); // Simple power operation
3226
- }, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
3227
- // ControlValueAccessor callbacks
3228
- onChange = (value) => { };
3229
- onTouched = () => { };
3230
- onValidatorChange = () => { };
3231
- // Computed values
3232
- hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
3233
- isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
3234
- isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
3153
+ fileManagerService = inject(CideEleFileManagerService);
3154
+ // Signals for reactive state
3155
+ isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
3156
+ isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
3157
+ currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
3158
+ currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
3159
+ // Local state to maintain completed files visibility
3160
+ completedFiles = signal(new Map(), ...(ngDevMode ? [{ debugName: "completedFiles" }] : []));
3161
+ // Use file manager service's upload queue as the single source of truth
3162
+ uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
3163
+ activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
3164
+ // Computed values based on file manager service state and local completed files
3165
+ hasUploads = computed(() => this.uploadQueue().length > 0 || this.activeUploads().size > 0 || this.completedFiles().size > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
3166
+ pendingUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "pendingUploads" }] : [])); // All files in queue are pending
3167
+ completedUploads = computed(() => {
3168
+ const active = this.activeUploads();
3169
+ const activeCompleted = Array.from(active.values()).filter(upload => upload.stage === 'complete');
3170
+ const localCompleted = Array.from(this.completedFiles().values());
3171
+ return [...activeCompleted, ...localCompleted];
3172
+ }, ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
3173
+ failedUploads = computed(() => {
3174
+ const active = this.activeUploads();
3175
+ return Array.from(active.values()).filter(upload => upload.stage === 'error');
3176
+ }, ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
3177
+ // Animation states
3178
+ isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
3235
3179
  constructor() {
3236
- // Angular 20: afterRenderEffect for DOM operations
3237
- afterRenderEffect(() => {
3238
- // Update file input element when files change
3239
- if (this.files()) {
3240
- const fileInput = document.getElementById('cide-file-input-' + this.id());
3241
- if (fileInput) {
3242
- // Ensure the input reflects the current state
3243
- fileInput.files = this.files();
3244
- }
3180
+ console.log('🚀 [FloatingFileUploader] Component initialized');
3181
+ // Set up effect to monitor file manager service's upload queue
3182
+ effect(() => {
3183
+ const uploadQueue = this.fileManagerService.uploadQueue();
3184
+ const activeUploads = this.fileManagerService.activeUploads();
3185
+ const hasUploads = uploadQueue.length > 0 || activeUploads.size > 0 || this.completedFiles().size > 0;
3186
+ console.log('🔄 [FloatingFileUploader] File manager service queue changed:', {
3187
+ queueLength: uploadQueue.length,
3188
+ activeUploadsCount: activeUploads.size,
3189
+ completedFilesCount: this.completedFiles().size,
3190
+ hasUploads,
3191
+ currentVisible: this.isVisible(),
3192
+ queueFiles: uploadQueue
3193
+ });
3194
+ // Show floating uploader when files are added to the file manager service queue
3195
+ // But don't auto-hide when uploads complete - let user manually close
3196
+ if (hasUploads && !this.isVisible()) {
3197
+ console.log('👁️ [FloatingFileUploader] Showing floating uploader due to file manager queue');
3198
+ this.showWithAnimation();
3245
3199
  }
3200
+ // Note: Removed auto-hide logic - floating uploader stays visible until manually closed
3246
3201
  });
3247
- // Angular 20: afterNextRender for one-time DOM operations
3248
- afterNextRender(() => {
3249
- console.log('🎯 [FileInput] Component rendered and DOM is ready');
3202
+ // Set up effect to track completed uploads
3203
+ effect(() => {
3204
+ const activeUploads = this.fileManagerService.activeUploads();
3205
+ const currentCompleted = this.completedFiles();
3206
+ // Check for newly completed uploads
3207
+ activeUploads.forEach((upload, fileId) => {
3208
+ if (upload.stage === 'complete' && !currentCompleted.has(fileId)) {
3209
+ console.log('✅ [FloatingFileUploader] File completed, adding to local completed files:', fileId);
3210
+ this.addCompletedFile(fileId, upload);
3211
+ }
3212
+ });
3250
3213
  });
3251
3214
  }
3252
3215
  ngOnInit() {
3253
- // Update signals with initial @Input() values
3254
- this.labelSignal.set(this.label);
3255
- this.acceptSignal.set(this.accept);
3256
- this.multipleSignal.set(this.multiple);
3257
- this.disabledSignal.set(this.disabled);
3258
- this.requiredSignal.set(this.required);
3259
- this.helperTextSignal.set(this.helperText);
3260
- this.errorTextSignal.set(this.errorText);
3261
- this.showPreviewSignal.set(this.showPreview);
3262
- this.previewWidthSignal.set(this.previewWidth);
3263
- this.previewHeightSignal.set(this.previewHeight);
3264
- this.previewBoxModeSignal.set(this.previewBoxMode);
3265
- this.showFileNameSignal.set(this.showFileName);
3266
- this.placeholderTextSignal.set(this.placeholderText);
3267
- this.placeholderIconSignal.set(this.placeholderIcon);
3268
- this.autoUploadSignal.set(this.autoUpload);
3269
- this.uploadDataSignal.set(this.uploadData);
3270
- }
3271
- ngOnChanges(changes) {
3272
- // Angular 20: Update signals when @Input() values change
3273
- if (changes['label'])
3274
- this.labelSignal.set(this.label);
3275
- if (changes['accept'])
3276
- this.acceptSignal.set(this.accept);
3277
- if (changes['multiple'])
3278
- this.multipleSignal.set(this.multiple);
3279
- if (changes['disabled'])
3280
- this.disabledSignal.set(this.disabled);
3281
- if (changes['required'])
3282
- this.requiredSignal.set(this.required);
3283
- if (changes['helperText'])
3284
- this.helperTextSignal.set(this.helperText);
3285
- if (changes['errorText'])
3286
- this.errorTextSignal.set(this.errorText);
3287
- if (changes['showPreview'])
3288
- this.showPreviewSignal.set(this.showPreview);
3289
- if (changes['previewWidth'])
3290
- this.previewWidthSignal.set(this.previewWidth);
3291
- if (changes['previewHeight'])
3292
- this.previewHeightSignal.set(this.previewHeight);
3293
- if (changes['previewBoxMode'])
3294
- this.previewBoxModeSignal.set(this.previewBoxMode);
3295
- if (changes['showFileName'])
3296
- this.showFileNameSignal.set(this.showFileName);
3297
- if (changes['placeholderText'])
3298
- this.placeholderTextSignal.set(this.placeholderText);
3299
- if (changes['placeholderIcon'])
3300
- this.placeholderIconSignal.set(this.placeholderIcon);
3301
- if (changes['autoUpload'])
3302
- this.autoUploadSignal.set(this.autoUpload);
3303
- if (changes['uploadData'])
3304
- this.uploadDataSignal.set(this.uploadData);
3216
+ // Set up drag and drop listeners
3217
+ this.setupDragAndDrop();
3218
+ // Set up file input change listeners
3219
+ this.setupFileInputListeners();
3305
3220
  }
3306
- writeValue(value) {
3307
- console.log('📝 [FileInput] writeValue called with:', value);
3308
- if (typeof value === 'string') {
3309
- // Value is an uploaded file ID - fetch file details and set preview
3310
- console.log('📝 [FileInput] Value is uploaded file ID:', value);
3311
- this.files.set(null);
3312
- this.fileNames.set([]);
3313
- this.clearPreviews();
3314
- // Fetch file details to get base64 and set preview
3315
- this.loadFileDetailsFromId(value);
3316
- }
3317
- else if (value instanceof FileList) {
3318
- // Value is a FileList
3319
- console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
3320
- this.files.set(value);
3321
- this.fileNames.set(Array.from(value).map(f => f.name));
3322
- this.generatePreviews();
3323
- }
3324
- else {
3325
- // Value is null
3326
- console.log('📝 [FileInput] Value is null');
3327
- this.files.set(null);
3328
- this.fileNames.set([]);
3329
- this.clearPreviews();
3330
- }
3221
+ ngOnDestroy() {
3222
+ console.log('🧹 [FloatingFileUploader] Component destroyed');
3223
+ this.removeDragAndDropListeners();
3224
+ this.removeFileInputListeners();
3331
3225
  }
3332
- registerOnChange(fn) {
3333
- this.onChange = fn;
3226
+ /**
3227
+ * Set up drag and drop functionality
3228
+ */
3229
+ setupDragAndDrop() {
3230
+ document.addEventListener('dragover', this.handleDragOver.bind(this));
3231
+ document.addEventListener('dragleave', this.handleDragLeave.bind(this));
3232
+ document.addEventListener('drop', this.handleDrop.bind(this));
3334
3233
  }
3335
- registerOnTouched(fn) {
3336
- this.onTouched = fn;
3234
+ /**
3235
+ * Remove drag and drop listeners
3236
+ */
3237
+ removeDragAndDropListeners() {
3238
+ document.removeEventListener('dragover', this.handleDragOver.bind(this));
3239
+ document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
3240
+ document.removeEventListener('drop', this.handleDrop.bind(this));
3337
3241
  }
3338
- registerOnValidatorChange(fn) {
3339
- this.onValidatorChange = fn;
3242
+ /**
3243
+ * Set up file input change listeners
3244
+ */
3245
+ setupFileInputListeners() {
3246
+ // Listen for file input change events globally
3247
+ document.addEventListener('change', this.handleFileInputChange.bind(this));
3340
3248
  }
3341
- setDisabledState(isDisabled) {
3342
- // Note: With input signals, disabled state is controlled by the parent component
3343
- // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3344
- console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3249
+ /**
3250
+ * Remove file input listeners
3251
+ */
3252
+ removeFileInputListeners() {
3253
+ document.removeEventListener('change', this.handleFileInputChange.bind(this));
3345
3254
  }
3346
- onFileSelected(event) {
3347
- console.log('🔍 [FileInput] onFileSelected called');
3348
- const input = event.target;
3349
- const selectedFiles = input.files;
3350
- this.files.set(selectedFiles);
3351
- this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3352
- console.log('📁 [FileInput] Files selected:', this.fileNames());
3353
- this.generatePreviews();
3354
- // Reset upload status when new file is selected
3355
- this.uploadStatus.set('idle');
3356
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3357
- this.onChange(selectedFiles);
3358
- this.fileChange.emit(selectedFiles);
3359
- this.onTouched();
3360
- // Auto upload if enabled
3361
- if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
3362
- if (this.multipleSignal()) {
3363
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
3364
- this.uploadMultipleFiles(Array.from(selectedFiles));
3365
- }
3366
- else {
3367
- console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
3368
- this.uploadFile(selectedFiles[0]);
3369
- }
3370
- }
3371
- else {
3372
- console.log('⏸️ [FileInput] Auto upload disabled or no files');
3373
- }
3374
- }
3375
- clearFiles() {
3376
- console.log('🗑️ [FileInput] clearFiles called');
3377
- this.files.set(null);
3378
- this.fileNames.set([]);
3379
- this.clearPreviews();
3380
- this.uploadStatus.set('idle');
3381
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3382
- this.onChange(null);
3383
- this.fileChange.emit(null);
3384
- }
3385
- uploadFile(file) {
3386
- console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
3387
- // Angular 20: Use PendingTasks for better loading state management
3388
- // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
3389
- // console.log('⏳ [FileInput] Pending task added for upload tracking');
3390
- // Set upload status to 'start' before starting upload
3391
- this.uploadStatus.set('start');
3392
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3393
- this.isUploading.set(true);
3394
- this.uploadProgress.set(0);
3395
- this.uploadProgressChange.emit(0);
3396
- console.log('📊 [FileInput] Upload progress initialized to 0%');
3397
- // Make form control invalid during upload - this prevents form submission
3398
- this.onChange(null);
3399
- console.log('🚫 [FileInput] Form control value set to null to prevent submission');
3400
- // Show initial progress notification with spinner (persistent - no auto-dismiss)
3401
- const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
3402
- this.uploadNotificationId.set(notificationId);
3403
- console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
3404
- this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
3405
- // Real progress callback from file manager service
3406
- this.uploadProgress.set(progress);
3407
- this.uploadProgressChange.emit(progress);
3408
- console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
3409
- // Set upload status to 'uploading' when progress starts
3410
- if (this.uploadStatus() === 'start') {
3411
- this.uploadStatus.set('uploading');
3412
- console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
3413
- }
3414
- // Update progress notification with spinner
3415
- const notificationId = this.uploadNotificationId();
3416
- if (notificationId) {
3417
- let progressMessage = '';
3418
- if (progress < 10) {
3419
- progressMessage = '🔄 Starting upload...';
3420
- }
3421
- else if (progress < 25) {
3422
- progressMessage = '🔄 Uploading file...';
3423
- }
3424
- else if (progress < 50) {
3425
- progressMessage = '🔄 Upload in progress...';
3426
- }
3427
- else if (progress < 75) {
3428
- progressMessage = '🔄 Almost done...';
3429
- }
3430
- else if (progress < 95) {
3431
- progressMessage = '🔄 Finishing upload...';
3432
- }
3433
- else {
3434
- progressMessage = '🔄 Finalizing...';
3435
- }
3436
- this.notificationService.updateProgress(notificationId, progress, progressMessage);
3437
- }
3438
- }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3439
- next: (response) => {
3440
- console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
3441
- // Angular 20: Complete the pending task
3442
- // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3443
- // console.log('✅ [FileInput] Pending task completed for successful upload');
3444
- // Set upload status to 'success'
3445
- this.uploadStatus.set('success');
3446
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3447
- // Complete the progress
3448
- this.uploadProgress.set(100);
3449
- this.uploadProgressChange.emit(100);
3450
- console.log('📊 [FileInput] Upload progress completed: 100%');
3451
- // Update progress notification to complete
3452
- const notificationId = this.uploadNotificationId();
3453
- if (notificationId) {
3454
- this.notificationService.remove(notificationId);
3455
- console.log('🔔 [FileInput] Progress notification removed');
3456
- }
3457
- this.notificationService.success('✅ File uploaded successfully!', { duration: 0 });
3458
- this.uploadNotificationId.set(null);
3459
- // Extract ID from CoreFileManagerInsertUpdateResponse
3460
- const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
3461
- if (uploadedId) {
3462
- console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
3463
- // Set the uploaded ID as the form control value
3464
- this.onChange(uploadedId);
3465
- console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
3466
- // Only emit individual uploadSuccess if not in multiple upload mode
3467
- if (!this.isMultipleUploadMode()) {
3468
- this.uploadSuccess.emit(uploadedId);
3469
- console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
3470
- }
3471
- else {
3472
- console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
3473
- }
3474
- }
3475
- else {
3476
- console.error('❌ [FileInput] Upload successful but no ID returned:', response);
3477
- this.uploadError.emit('Upload successful but no ID returned');
3478
- }
3479
- this.isUploading.set(false);
3480
- console.log('🔄 [FileInput] isUploading set to false');
3481
- },
3482
- error: (error) => {
3483
- console.error('💥 [FileInput] Upload FAILED:', error);
3484
- // Angular 20: Complete the pending task even on error
3485
- // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3486
- // console.log('❌ [FileInput] Pending task completed for failed upload');
3487
- // Set upload status to 'error' and remove upload validation error
3488
- this.uploadStatus.set('error');
3489
- console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3490
- // Remove progress notification and show error
3491
- const notificationId = this.uploadNotificationId();
3492
- if (notificationId) {
3493
- this.notificationService.remove(notificationId);
3494
- console.log('🔔 [FileInput] Progress notification removed due to error');
3495
- }
3496
- this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
3497
- this.uploadNotificationId.set(null);
3498
- this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
3499
- this.isUploading.set(false);
3500
- this.uploadProgress.set(0);
3501
- this.uploadProgressChange.emit(0);
3502
- console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
3503
- }
3504
- });
3505
- }
3506
- /**
3507
- * Upload multiple files with group ID support
3508
- * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
3509
- */
3510
- uploadMultipleFiles(files) {
3511
- console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
3512
- console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
3513
- // Set multiple upload mode flag
3514
- this.isMultipleUploadMode.set(true);
3515
- // Set upload status to 'start' before starting upload
3516
- this.uploadStatus.set('start');
3517
- this.isUploading.set(true);
3518
- this.uploadProgress.set(0);
3519
- this.uploadProgressChange.emit(0);
3520
- // Make form control invalid during upload
3521
- this.onChange(null);
3522
- // Show initial progress notification
3523
- const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
3524
- this.uploadNotificationId.set(notificationId);
3525
- // STEP 1: Generate or get group ID BEFORE starting any file uploads
3526
- const existingGroupId = this.uploadDataSignal().groupId;
3527
- if (existingGroupId) {
3528
- console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
3529
- console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
3530
- this.groupId.set(existingGroupId);
3531
- this.startMulti(files, existingGroupId);
3532
- }
3533
- else {
3534
- console.log('🆔 [FileInput] No existing group ID, generating new one...');
3535
- // Generate group ID BEFORE starting any file uploads
3536
- this.fileManagerService.generateObjectId().subscribe({
3537
- next: (response) => {
3538
- const newGroupId = response.data?.objectId;
3539
- console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
3540
- console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
3541
- this.groupId.set(newGroupId);
3542
- this.startMulti(files, newGroupId);
3543
- },
3544
- error: (error) => {
3545
- console.error('❌ [FileInput] Failed to generate group ID:', error);
3546
- this.uploadError.emit('Failed to generate group ID');
3547
- this.isUploading.set(false);
3548
- this.uploadStatus.set('error');
3549
- const notificationId = this.uploadNotificationId();
3550
- if (notificationId) {
3551
- this.notificationService.remove(notificationId);
3552
- }
3553
- this.notificationService.error('❌ Failed to generate group ID for multiple file upload', { duration: 0 });
3554
- this.uploadNotificationId.set(null);
3555
- }
3556
- });
3557
- }
3558
- }
3559
- /**
3560
- * Start uploading multiple files with the provided group ID
3561
- * All files will be uploaded with the SAME group ID that was generated before this method
3562
- */
3563
- startMulti(files, groupId) {
3564
- console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
3565
- console.log('📋 [FileInput] All files will use the same group ID:', groupId);
3566
- let completedUploads = 0;
3567
- let failedUploads = 0;
3568
- const totalFiles = files.length;
3569
- // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
3570
- const uploadDataWithGroupId = {
3571
- ...this.uploadDataSignal(),
3572
- groupId: groupId
3573
- };
3574
- files.forEach((file, index) => {
3575
- console.log(`📤 [FileInput] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
3576
- this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
3577
- // Calculate overall progress
3578
- const fileProgress = progress / totalFiles;
3579
- const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
3580
- this.uploadProgress.set(overallProgress);
3581
- this.uploadProgressChange.emit(overallProgress);
3582
- // Update progress notification
3583
- const notificationId = this.uploadNotificationId();
3584
- if (notificationId) {
3585
- const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
3586
- this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
3587
- }
3588
- }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3589
- next: (response) => {
3590
- completedUploads++;
3591
- console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded successfully`);
3592
- // Check if all files are completed
3593
- if (completedUploads + failedUploads === totalFiles) {
3594
- this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3595
- }
3596
- },
3597
- error: (error) => {
3598
- failedUploads++;
3599
- console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
3600
- // Check if all files are completed
3601
- if (completedUploads + failedUploads === totalFiles) {
3602
- this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
3603
- }
3604
- }
3605
- });
3606
- });
3607
- }
3608
- /**
3609
- * Handle completion of multiple file upload
3610
- */
3611
- handleMultipleUploadComplete(completed, failed, total, groupId) {
3612
- console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
3613
- this.isUploading.set(false);
3614
- this.uploadProgress.set(100);
3615
- this.uploadProgressChange.emit(100);
3616
- // Remove progress notification
3617
- const notificationId = this.uploadNotificationId();
3618
- if (notificationId) {
3619
- this.notificationService.remove(notificationId);
3620
- }
3621
- this.uploadNotificationId.set(null);
3622
- if (failed === 0) {
3623
- // All files uploaded successfully
3624
- this.uploadStatus.set('success');
3625
- this.notificationService.success(`✅ All ${total} files uploaded successfully!`, { duration: 0 });
3626
- // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
3627
- this.onChange(groupId);
3628
- this.uploadSuccess.emit(groupId);
3629
- console.log('📝 [FileInput] STEP 3 COMPLETE: Form control value set to group ID:', groupId);
3630
- console.log('✅ [FileInput] Multiple upload SUCCESS - Group ID emitted:', groupId);
3631
- }
3632
- else if (completed > 0) {
3633
- // Some files uploaded successfully
3634
- this.uploadStatus.set('error');
3635
- this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
3636
- this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
3637
- }
3638
- else {
3639
- // All files failed
3640
- this.uploadStatus.set('error');
3641
- this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
3642
- this.uploadError.emit('All files failed to upload');
3643
- }
3644
- // Reset multiple upload mode flag
3645
- this.isMultipleUploadMode.set(false);
3646
- }
3647
- generatePreviews() {
3648
- // Clear existing previews
3649
- this.clearPreviews();
3650
- if (!this.showPreviewSignal() || !this.files()) {
3651
- return;
3652
- }
3653
- Array.from(this.files()).forEach(file => {
3654
- if (this.isImageFile(file)) {
3655
- const reader = new FileReader();
3656
- reader.onload = (e) => {
3657
- if (e.target?.result) {
3658
- this.previewUrls.update(urls => [...urls, e.target.result]);
3659
- }
3660
- };
3661
- reader.readAsDataURL(file);
3662
- }
3663
- });
3664
- }
3665
- clearPreviews() {
3666
- // Revoke object URLs to prevent memory leaks
3667
- this.previewUrls().forEach(url => {
3668
- if (url.startsWith('blob:')) {
3669
- URL.revokeObjectURL(url);
3670
- }
3671
- });
3672
- this.previewUrls.set([]);
3673
- }
3674
- isImageFile(file) {
3675
- return file.type.startsWith('image/');
3676
- }
3677
- loadFileDetailsFromId(fileId) {
3678
- console.log('🔍 [FileInput] Loading file details for ID:', fileId);
3679
- if (!fileId)
3680
- return;
3681
- this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3682
- next: (fileDetails) => {
3683
- console.log('📋 [FileInput] File details received:', fileDetails);
3684
- if (fileDetails?.data?.length) {
3685
- const fileData = fileDetails.data[0];
3686
- console.log('📁 [FileInput] File data:', fileData);
3687
- // Set file name from the details
3688
- if (fileData.cyfm_name) {
3689
- this.fileNames.set([fileData.cyfm_name]);
3690
- console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
3691
- }
3692
- // If it's an image and we have base64 data, set preview
3693
- if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
3694
- // Check if it's an image file based on file name or type
3695
- const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
3696
- this.isImageFileFromType(fileData.cyfm_type || '');
3697
- if (isImage) {
3698
- // Add data URL prefix if not already present
3699
- let base64Data = fileData.cyfm_file_base64;
3700
- if (!base64Data.startsWith('data:')) {
3701
- const mimeType = fileData.cyfm_type || 'image/jpeg';
3702
- base64Data = `data:${mimeType};base64,${base64Data}`;
3703
- }
3704
- this.previewUrls.set([base64Data]);
3705
- console.log('🖼️ [FileInput] Preview set from base64 data');
3706
- }
3707
- }
3708
- }
3709
- else {
3710
- console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
3711
- }
3712
- },
3713
- error: (error) => {
3714
- console.error('❌ [FileInput] Error loading file details:', error);
3715
- this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
3716
- }
3255
+ /**
3256
+ * Handle file input change events
3257
+ */
3258
+ handleFileInputChange(event) {
3259
+ const target = event.target;
3260
+ console.log('🔍 [FloatingFileUploader] File input change event detected:', {
3261
+ type: target.type,
3262
+ filesLength: target.files?.length || 0,
3263
+ element: target
3717
3264
  });
3718
- }
3719
- isImageFileFromName(fileName) {
3720
- if (!fileName)
3721
- return false;
3722
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
3723
- const lowerFileName = fileName.toLowerCase();
3724
- return imageExtensions.some(ext => lowerFileName.endsWith(ext));
3725
- }
3726
- isImageFileFromType(fileType) {
3727
- if (!fileType)
3728
- return false;
3729
- return fileType.startsWith('image/');
3730
- }
3731
- removePreview(index) {
3732
- const currentFiles = this.files();
3733
- const currentUrls = this.previewUrls();
3734
- if (currentFiles && currentFiles.length > index) {
3735
- // Handle FileList case - remove file from FileList
3736
- const dt = new DataTransfer();
3737
- Array.from(currentFiles).forEach((file, i) => {
3738
- if (i !== index) {
3739
- dt.items.add(file);
3740
- }
3741
- });
3742
- const newFiles = dt.files;
3743
- this.files.set(newFiles);
3744
- this.fileNames.set(Array.from(newFiles).map(f => f.name));
3745
- // Remove the preview URL
3746
- if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
3747
- URL.revokeObjectURL(currentUrls[index]);
3265
+ // Check if this is a file input with files
3266
+ if (target.type === 'file' && target.files && target.files.length > 0) {
3267
+ console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
3268
+ // Check if the input has a data-user-id attribute for user context
3269
+ const userId = target.getAttribute('data-user-id');
3270
+ if (userId && userId !== this.currentUserId()) {
3271
+ this.setCurrentUserId(userId);
3748
3272
  }
3749
- this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
3750
- this.onChange(newFiles);
3751
- this.fileChange.emit(newFiles);
3752
- }
3753
- else if (currentUrls.length > index) {
3754
- // Handle uploaded file ID case - clear the preview and set control value to null
3755
- console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
3756
- // Clear preview
3757
- this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
3758
- this.fileNames.set([]);
3759
- // Set control value to null since we're removing the uploaded file
3760
- this.onChange(null);
3761
- this.fileChange.emit(null);
3762
- }
3763
- }
3764
- ngOnDestroy() {
3765
- // Clean up preview URLs to prevent memory leaks
3766
- this.clearPreviews();
3767
- // Clean up any active upload notification
3768
- const notificationId = this.uploadNotificationId();
3769
- if (notificationId) {
3770
- this.notificationService.remove(notificationId);
3771
- this.uploadNotificationId.set(null);
3772
- }
3773
- }
3774
- triggerFileSelect() {
3775
- const fileInput = document.getElementById('cide-file-input-' + this.id());
3776
- if (fileInput && !this.disabledSignal()) {
3777
- fileInput.click();
3273
+ // Handle the files
3274
+ this.handleFiles(Array.from(target.files));
3275
+ // Reset the input to allow selecting the same files again
3276
+ target.value = '';
3778
3277
  }
3779
3278
  }
3780
- // Drag and Drop Event Handlers
3781
- onDragOver(event) {
3279
+ /**
3280
+ * Handle drag over event
3281
+ */
3282
+ handleDragOver(event) {
3782
3283
  event.preventDefault();
3783
3284
  event.stopPropagation();
3784
- if (!this.disabledSignal()) {
3785
- this.isDragOver.set(true);
3786
- console.log('🔄 [FileInput] Drag over detected');
3285
+ // Show floating uploader when files are dragged over
3286
+ if (event.dataTransfer?.types.includes('Files')) {
3287
+ this.showWithAnimation();
3787
3288
  }
3788
3289
  }
3789
- onDragLeave(event) {
3790
- event.preventDefault();
3791
- event.stopPropagation();
3792
- this.isDragOver.set(false);
3793
- console.log('🔄 [FileInput] Drag leave detected');
3794
- }
3795
- onDragEnter(event) {
3290
+ /**
3291
+ * Handle drag leave event
3292
+ */
3293
+ handleDragLeave(event) {
3796
3294
  event.preventDefault();
3797
3295
  event.stopPropagation();
3798
- if (!this.disabledSignal()) {
3799
- this.isDragOver.set(true);
3800
- console.log('🔄 [FileInput] Drag enter detected');
3296
+ // Only hide if leaving the entire document
3297
+ if (!event.relatedTarget || event.relatedTarget === document.body) {
3298
+ this.updateVisibility();
3801
3299
  }
3802
3300
  }
3803
- onDrop(event) {
3301
+ /**
3302
+ * Handle drop event
3303
+ */
3304
+ handleDrop(event) {
3804
3305
  event.preventDefault();
3805
3306
  event.stopPropagation();
3806
- this.isDragOver.set(false);
3807
- if (this.disabledSignal()) {
3808
- console.log('⏸️ [FileInput] Drop ignored - component is disabled');
3809
- return;
3810
- }
3811
3307
  const files = event.dataTransfer?.files;
3812
3308
  if (files && files.length > 0) {
3813
- console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
3814
- // Validate file types if accept is specified
3815
- if (this.acceptSignal() && !this.validateFileTypes(files)) {
3816
- console.log('❌ [FileInput] Invalid file types dropped');
3817
- this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
3818
- return;
3819
- }
3820
- // Handle single vs multiple files
3821
- if (!this.multipleSignal() && files.length > 1) {
3822
- console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
3823
- this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
3824
- // Create a new FileList with only the first file
3825
- const dt = new DataTransfer();
3826
- dt.items.add(files[0]);
3827
- this.handleFileSelection(dt.files);
3828
- }
3829
- else {
3830
- this.handleFileSelection(files);
3831
- }
3309
+ this.handleFiles(Array.from(files));
3832
3310
  }
3833
3311
  }
3834
- validateFileTypes(files) {
3835
- const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
3836
- if (acceptTypes.length === 0 || acceptTypes[0] === '')
3837
- return true;
3838
- return Array.from(files).every(file => {
3839
- return acceptTypes.some(acceptType => {
3840
- if (acceptType.startsWith('.')) {
3841
- // Extension-based validation
3842
- return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
3843
- }
3844
- else if (acceptType.includes('/')) {
3845
- // MIME type validation
3846
- return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
3847
- }
3848
- return false;
3849
- });
3850
- });
3312
+ /**
3313
+ * Handle files from drag and drop or file input
3314
+ */
3315
+ handleFiles(files) {
3316
+ console.log('📁 [FloatingFileUploader] Handling files:', files.length);
3317
+ // Use handleExternalFiles to process the files
3318
+ this.handleExternalFiles(files, this.currentUserId());
3851
3319
  }
3852
- handleFileSelection(files) {
3853
- this.files.set(files);
3854
- this.fileNames.set(Array.from(files).map(f => f.name));
3855
- console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
3856
- this.generatePreviews();
3857
- // Reset upload status when new file is selected
3858
- this.uploadStatus.set('idle');
3859
- console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3860
- this.onChange(files);
3861
- this.fileChange.emit(files);
3862
- this.onTouched();
3863
- // Auto upload if enabled
3864
- if (this.autoUploadSignal() && files.length > 0) {
3865
- if (this.multipleSignal()) {
3866
- console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
3867
- this.uploadMultipleFiles(Array.from(files));
3868
- }
3869
- else {
3870
- console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
3871
- this.uploadFile(files[0]);
3872
- }
3320
+ /**
3321
+ * Update visibility - simplified for notification only
3322
+ */
3323
+ updateVisibility() {
3324
+ // This is just a notification component now
3325
+ // The actual uploads are handled by the global uploader
3326
+ }
3327
+ /**
3328
+ * Show with animation
3329
+ */
3330
+ showWithAnimation() {
3331
+ console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
3332
+ this.isAnimating.set(true);
3333
+ this.isVisible.set(true);
3334
+ // Remove animation class after animation completes
3335
+ setTimeout(() => {
3336
+ this.isAnimating.set(false);
3337
+ console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
3338
+ }, 300);
3339
+ }
3340
+ /**
3341
+ * Hide with animation
3342
+ */
3343
+ hideWithAnimation() {
3344
+ this.isAnimating.set(true);
3345
+ // Wait for animation to complete before hiding
3346
+ setTimeout(() => {
3347
+ this.isVisible.set(false);
3348
+ this.isAnimating.set(false);
3349
+ }, 300);
3350
+ }
3351
+ /**
3352
+ * Toggle minimize state
3353
+ */
3354
+ toggleMinimize() {
3355
+ this.isMinimized.set(!this.isMinimized());
3356
+ }
3357
+ /**
3358
+ * Close the floating uploader
3359
+ */
3360
+ close() {
3361
+ // Clear completed files when closing
3362
+ this.clearCompletedFiles();
3363
+ this.hideWithAnimation();
3364
+ }
3365
+ /**
3366
+ * Get upload summary text
3367
+ */
3368
+ getUploadSummary() {
3369
+ const queueLength = this.uploadQueue().length;
3370
+ const activeCount = this.activeUploads().size;
3371
+ const completed = this.completedUploads().length;
3372
+ const failed = this.failedUploads().length;
3373
+ if (activeCount > 0) {
3374
+ return `${activeCount} uploading`;
3873
3375
  }
3874
- else {
3875
- console.log('⏸️ [FileInput] Auto upload disabled or no files');
3376
+ else if (completed > 0 && failed === 0) {
3377
+ return `${completed} completed`;
3876
3378
  }
3877
- }
3878
- isRequired() {
3879
- return this.requiredSignal();
3379
+ else if (failed > 0) {
3380
+ return `${completed} completed, ${failed} failed`;
3381
+ }
3382
+ else if (queueLength > 0) {
3383
+ return `${queueLength} pending`;
3384
+ }
3385
+ return 'No uploads';
3880
3386
  }
3881
3387
  /**
3882
- * Angular 20: Utility method to get upload data with proper typing
3883
- * @returns Properly typed upload data
3388
+ * Get overall progress percentage
3884
3389
  */
3885
- getUploadData() {
3886
- return this.uploadDataSignal();
3390
+ getOverallProgress() {
3391
+ const activeUploads = this.activeUploads();
3392
+ if (activeUploads.size === 0)
3393
+ return 0;
3394
+ const totalProgress = Array.from(activeUploads.values()).reduce((sum, upload) => sum + upload.percentage, 0);
3395
+ return Math.round(totalProgress / activeUploads.size);
3887
3396
  }
3888
3397
  /**
3889
- * Angular 20: Utility method to update upload data with type safety
3890
- * @param data Partial upload data to merge with existing data
3398
+ * Get status icon based on upload stage
3891
3399
  */
3892
- updateUploadData(data) {
3893
- const currentData = this.uploadDataSignal();
3894
- const updatedData = { ...currentData, ...data };
3895
- // Note: This would require the uploadData to be a writable signal
3896
- // For now, this method serves as a type-safe way to work with upload data
3897
- console.log('📝 [FileInput] Upload data updated:', updatedData);
3898
- }
3899
- getCurrentState() {
3900
- return {
3901
- id: this.id(),
3902
- label: this.labelSignal(),
3903
- required: this.requiredSignal(),
3904
- disabled: this.disabledSignal(),
3905
- accept: this.acceptSignal(),
3906
- multiple: this.multipleSignal(),
3907
- showPreview: this.showPreviewSignal(),
3908
- autoUpload: this.autoUploadSignal(),
3909
- uploadStatus: this.uploadStatus(),
3910
- isUploading: this.isUploading(),
3911
- uploadProgress: this.uploadProgress(),
3912
- files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
3913
- fileNames: this.fileNames(),
3914
- previewUrls: this.previewUrls().length,
3915
- helperText: this.helperTextSignal(),
3916
- errorText: this.errorTextSignal(),
3917
- placeholderText: this.placeholderTextSignal(),
3918
- placeholderIcon: this.placeholderIconSignal(),
3919
- previewWidth: this.previewWidthSignal(),
3920
- previewHeight: this.previewHeightSignal(),
3921
- previewBoxMode: this.previewBoxModeSignal(),
3922
- showFileName: this.showFileNameSignal(),
3923
- uploadData: this.uploadDataSignal()
3924
- };
3925
- }
3926
- async getControlData() {
3927
- console.log('🔍 [FileInput] getControlData called');
3928
- const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
3929
- if (cide_element_data) {
3930
- console.log('📋 [FileInput] Element data loaded:', cide_element_data);
3931
- // Note: Since we're using input signals, we can't directly set their values
3932
- // This method would need to be refactored to work with the new signal-based approach
3933
- // For now, we'll log the data and trigger validation
3934
- console.log('✅ [FileInput] Control data received from element service');
3935
- console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
3936
- // Trigger validation update
3937
- this.onValidatorChange();
3938
- }
3939
- else {
3940
- console.log('⚠️ [FileInput] No element data found for key:', this.id());
3400
+ getStatusIcon(stage) {
3401
+ switch (stage) {
3402
+ case 'reading': return 'schedule';
3403
+ case 'uploading': return 'cloud_upload';
3404
+ case 'complete': return 'check_circle';
3405
+ case 'error': return 'error';
3406
+ default: return 'help';
3941
3407
  }
3942
3408
  }
3943
- // Validator implementation
3944
- validate(control) {
3945
- console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
3946
- // If upload is in progress (start or uploading status), return validation error
3947
- if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
3948
- console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
3949
- return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
3409
+ /**
3410
+ * Get status class based on upload stage
3411
+ */
3412
+ getStatusClass(stage) {
3413
+ switch (stage) {
3414
+ case 'reading': return 'status-pending';
3415
+ case 'uploading': return 'status-uploading';
3416
+ case 'complete': return 'status-completed';
3417
+ case 'error': return 'status-error';
3418
+ default: return 'status-unknown';
3950
3419
  }
3951
- // If required and no file is selected and no control value (uploaded file ID), return validation error
3952
- if (this.requiredSignal() && !this.files() && !control.value) {
3953
- console.log('⚠️ [FileInput] Validation ERROR: File required');
3954
- return { 'required': { message: 'Please select a file to upload.' } };
3420
+ }
3421
+ /**
3422
+ * Cancel upload
3423
+ */
3424
+ cancelUpload(fileId) {
3425
+ console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
3426
+ this.fileManagerService.cancelUpload(fileId);
3427
+ }
3428
+ /**
3429
+ * Get file name from file ID (extract from the ID format)
3430
+ */
3431
+ getFileNameFromId(fileId) {
3432
+ // Extract filename from the fileId format: filename_size_timestamp
3433
+ const parts = fileId.split('_');
3434
+ if (parts.length >= 3) {
3435
+ // Remove the last two parts (size and timestamp) to get the filename
3436
+ return parts.slice(0, -2).join('_');
3955
3437
  }
3956
- console.log('✅ [FileInput] Validation PASSED: No errors');
3957
- return null; // No validation errors
3438
+ return fileId;
3958
3439
  }
3959
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3960
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFileInputComponent, isStandalone: true, selector: "cide-ele-file-input", inputs: { label: "label", accept: "accept", multiple: "multiple", disabled: "disabled", required: "required", helperText: "helperText", errorText: "errorText", showPreview: "showPreview", previewWidth: "previewWidth", previewHeight: "previewHeight", previewBoxMode: "previewBoxMode", showFileName: "showFileName", placeholderText: "placeholderText", placeholderIcon: "placeholderIcon", autoUpload: "autoUpload", uploadData: "uploadData" }, outputs: { fileChange: "fileChange", uploadSuccess: "uploadSuccess", uploadError: "uploadError", uploadProgressChange: "uploadProgressChange" }, providers: [
3961
- {
3962
- provide: NG_VALUE_ACCESSOR,
3963
- useExisting: CideEleFileInputComponent,
3964
- multi: true
3965
- },
3966
- {
3967
- provide: NG_VALIDATORS,
3968
- useExisting: CideEleFileInputComponent,
3969
- multi: true
3970
- }
3971
- ], usesOnChanges: true, ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
3972
- }
3973
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
3974
- type: Component,
3975
- args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
3976
- {
3977
- provide: NG_VALUE_ACCESSOR,
3978
- useExisting: CideEleFileInputComponent,
3979
- multi: true
3980
- },
3981
- {
3982
- provide: NG_VALIDATORS,
3983
- useExisting: CideEleFileInputComponent,
3984
- multi: true
3985
- }
3986
- ], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
3987
- }], ctorParameters: () => [], propDecorators: { label: [{
3988
- type: Input
3989
- }], accept: [{
3990
- type: Input
3991
- }], multiple: [{
3992
- type: Input
3993
- }], disabled: [{
3994
- type: Input
3995
- }], required: [{
3996
- type: Input
3997
- }], helperText: [{
3998
- type: Input
3999
- }], errorText: [{
4000
- type: Input
4001
- }], showPreview: [{
4002
- type: Input
4003
- }], previewWidth: [{
4004
- type: Input
4005
- }], previewHeight: [{
4006
- type: Input
4007
- }], previewBoxMode: [{
4008
- type: Input
4009
- }], showFileName: [{
4010
- type: Input
4011
- }], placeholderText: [{
4012
- type: Input
4013
- }], placeholderIcon: [{
4014
- type: Input
4015
- }], autoUpload: [{
4016
- type: Input
4017
- }], uploadData: [{
4018
- type: Input
4019
- }], fileChange: [{
4020
- type: Output
4021
- }], uploadSuccess: [{
4022
- type: Output
4023
- }], uploadError: [{
4024
- type: Output
4025
- }], uploadProgressChange: [{
4026
- type: Output
4027
- }] } });
4028
-
4029
- class CideEleGlobalFileUploaderComponent {
4030
- // Dependency injection
4031
- destroyRef = inject(DestroyRef);
4032
- fileService = inject(CideEleFileManagerService);
4033
- // Input properties
4034
- userId = '';
4035
- multiple = true;
4036
- accept = '';
4037
- maxFileSize = 10; // MB
4038
- allowedTypes = [];
4039
- // Output events
4040
- uploadComplete = new EventEmitter();
4041
- uploadError = new EventEmitter();
4042
- uploadCancelled = new EventEmitter();
4043
- allUploadsComplete = new EventEmitter();
4044
- // Signals for reactive state management
4045
- isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4046
- isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
4047
- uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4048
- currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
4049
- // Computed values
4050
- hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
4051
- pendingUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'pending'), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
4052
- activeUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4053
- completedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'completed'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4054
- failedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4055
- constructor() {
4056
- console.log('🚀 [GlobalFileUploader] Component initialized');
3440
+ /**
3441
+ * Add completed file to local state
3442
+ */
3443
+ addCompletedFile(fileId, upload) {
3444
+ const fileName = this.getFileNameFromId(fileId);
3445
+ this.completedFiles.update(completed => {
3446
+ const newCompleted = new Map(completed);
3447
+ newCompleted.set(fileId, {
3448
+ fileName,
3449
+ completedAt: new Date(),
3450
+ groupId: this.currentGroupId() || undefined
3451
+ });
3452
+ return newCompleted;
3453
+ });
4057
3454
  }
4058
3455
  /**
4059
- * Handle drag over event
3456
+ * Clear all completed files
4060
3457
  */
4061
- onDragOver(event) {
4062
- event.preventDefault();
4063
- event.stopPropagation();
4064
- this.isDragOver.set(true);
3458
+ clearCompletedFiles() {
3459
+ this.completedFiles.set(new Map());
4065
3460
  }
4066
3461
  /**
4067
- * Handle drag leave event
3462
+ * Set current user ID
4068
3463
  */
4069
- onDragLeave(event) {
4070
- event.preventDefault();
4071
- event.stopPropagation();
4072
- this.isDragOver.set(false);
3464
+ setCurrentUserId(userId) {
3465
+ this.currentUserId.set(userId);
3466
+ this.fileManagerService.setUserId(userId);
4073
3467
  }
4074
3468
  /**
4075
- * Handle drop event
3469
+ * Public method to handle files from external sources
3470
+ * This can be called by other components to trigger the floating uploader
4076
3471
  */
4077
- onDrop(event) {
4078
- event.preventDefault();
4079
- event.stopPropagation();
4080
- this.isDragOver.set(false);
4081
- const files = event.dataTransfer?.files;
4082
- if (files && files.length > 0) {
4083
- this.addFilesToQueue(Array.from(files));
3472
+ handleExternalFiles(files, userId, groupId) {
3473
+ console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
3474
+ // Set user ID if provided
3475
+ if (userId && userId !== this.currentUserId()) {
3476
+ this.setCurrentUserId(userId);
3477
+ }
3478
+ // Set group ID if provided
3479
+ if (groupId) {
3480
+ this.currentGroupId.set(groupId);
4084
3481
  }
3482
+ // Upload files using file manager service
3483
+ // The file manager service will handle adding to its queue and the effect will show the floating uploader
3484
+ files.forEach((file, index) => {
3485
+ console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
3486
+ this.fileManagerService.uploadFile(file, {
3487
+ userId: this.currentUserId(),
3488
+ groupId: groupId,
3489
+ permissions: ['read', 'write'],
3490
+ tags: []
3491
+ })
3492
+ .pipe(takeUntilDestroyed(this.destroyRef))
3493
+ .subscribe({
3494
+ next: (response) => {
3495
+ console.log('✅ [FloatingFileUploader] Upload completed:', response);
3496
+ },
3497
+ error: (error) => {
3498
+ console.error('❌ [FloatingFileUploader] Upload failed:', error);
3499
+ }
3500
+ });
3501
+ });
4085
3502
  }
4086
3503
  /**
4087
- * Handle file input change
3504
+ * Manually show the floating uploader
3505
+ * This can be called from file input components or other triggers
4088
3506
  */
4089
- onFileInputChange(event) {
4090
- const target = event.target;
4091
- const files = target.files;
4092
- if (files && files.length > 0) {
4093
- this.addFilesToQueue(Array.from(files));
3507
+ showUploader(groupId) {
3508
+ console.log('👁️ [FloatingFileUploader] Manually showing uploader', groupId ? `for group: ${groupId}` : '');
3509
+ if (groupId) {
3510
+ this.currentGroupId.set(groupId);
4094
3511
  }
4095
- // Reset input value to allow selecting the same file again
4096
- target.value = '';
3512
+ this.showWithAnimation();
4097
3513
  }
4098
3514
  /**
4099
- * Add files to upload queue
3515
+ * Check if there are any uploads for the current group
4100
3516
  */
4101
- addFilesToQueue(files) {
4102
- // Validate files
4103
- const validFiles = files.filter(file => this.validateFile(file));
4104
- if (validFiles.length === 0) {
4105
- console.warn('⚠️ [GlobalFileUploader] No valid files to upload');
4106
- return;
3517
+ hasUploadsForCurrentGroup() {
3518
+ const groupId = this.currentGroupId();
3519
+ if (!groupId) {
3520
+ // If no group filter, show all uploads
3521
+ return this.hasUploads();
4107
3522
  }
4108
- const newUploads = validFiles.map(file => ({
4109
- fileId: this.generateFileId(file),
4110
- fileName: file.name,
4111
- progress: 0,
4112
- status: 'pending',
4113
- file: file
4114
- }));
4115
- this.uploadQueue.update(queue => [...queue, ...newUploads]);
4116
- this.processUploadQueue();
3523
+ // Check if any uploads belong to the current group
3524
+ // Note: This would need to be enhanced based on how group IDs are stored in the file manager service
3525
+ return this.hasUploads();
4117
3526
  }
4118
- /**
4119
- * Validate file
4120
- */
4121
- validateFile(file) {
4122
- // Check file size
4123
- const maxSizeBytes = this.maxFileSize * 1024 * 1024;
4124
- if (file.size > maxSizeBytes) {
4125
- console.warn(`⚠️ [GlobalFileUploader] File ${file.name} exceeds size limit`);
4126
- return false;
3527
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3528
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFloatingFileUploaderComponent, isStandalone: true, selector: "cide-ele-floating-file-uploader", ngImport: i0, template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0 || completedFiles().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show completed files from local state -->\r\n @for (entry of completedFiles() | keyvalue; track entry.key) {\r\n <div class=\"upload-item status-completed\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">check_circle</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ entry.value.fileName }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-green-600\">Completed</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!-- No uploads message when manually opened -->\r\n <div class=\"no-uploads-message\">\r\n <div class=\"message-content\">\r\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\r\n <div class=\"message-text\">\r\n <h4>No active uploads</h4>\r\n <p>Upload files to see their progress here</p>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}.no-uploads-message{padding:2rem;text-align:center;color:#6b7280}.no-uploads-message .message-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.no-uploads-message .message-content .message-icon{color:#9ca3af;opacity:.7}.no-uploads-message .message-content .message-text h4{margin:0 0 .5rem;font-size:1.1rem;font-weight:600;color:#374151}.no-uploads-message .message-content .message-text p{margin:0;font-size:.9rem;color:#6b7280}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
3529
+ }
3530
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
3531
+ type: Component,
3532
+ args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
3533
+ CommonModule,
3534
+ CideIconComponent
3535
+ ], template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0 || completedFiles().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show completed files from local state -->\r\n @for (entry of completedFiles() | keyvalue; track entry.key) {\r\n <div class=\"upload-item status-completed\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">check_circle</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ entry.value.fileName }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-green-600\">Completed</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!-- No uploads message when manually opened -->\r\n <div class=\"no-uploads-message\">\r\n <div class=\"message-content\">\r\n <cide-ele-icon size=\"md\" class=\"message-icon\">cloud_upload</cide-ele-icon>\r\n <div class=\"message-text\">\r\n <h4>No active uploads</h4>\r\n <p>Upload files to see their progress here</p>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}.no-uploads-message{padding:2rem;text-align:center;color:#6b7280}.no-uploads-message .message-content{display:flex;flex-direction:column;align-items:center;gap:1rem}.no-uploads-message .message-content .message-icon{color:#9ca3af;opacity:.7}.no-uploads-message .message-content .message-text h4{margin:0 0 .5rem;font-size:1.1rem;font-weight:600;color:#374151}.no-uploads-message .message-content .message-text p{margin:0;font-size:.9rem;color:#6b7280}\n"] }]
3536
+ }], ctorParameters: () => [] });
3537
+
3538
+ class CideEleFileInputComponent {
3539
+ fileManagerService = inject(CideEleFileManagerService);
3540
+ notificationService = inject(NotificationService);
3541
+ elementService = inject(CideElementsService);
3542
+ destroyRef = inject(DestroyRef);
3543
+ floatingUploader = inject(CideEleFloatingFileUploaderComponent, { optional: true });
3544
+ // private readonly pendingTasks = inject(PendingTasks); // TODO: Fix PendingTasks API usage
3545
+ // Traditional @Input() decorators
3546
+ label = 'Choose file';
3547
+ accept = '';
3548
+ multiple = false;
3549
+ disabled = false;
3550
+ required = false;
3551
+ helperText = '';
3552
+ errorText = '';
3553
+ showPreview = false;
3554
+ previewWidth = '200px';
3555
+ previewHeight = '200px';
3556
+ previewBoxMode = false;
3557
+ showFileName = true;
3558
+ placeholderText = 'Click to select image';
3559
+ placeholderIcon = '📷';
3560
+ autoUpload = false;
3561
+ uploadData = {};
3562
+ showFloatingUploader = true;
3563
+ floatingUploaderGroupId;
3564
+ // Traditional @Output() decorators
3565
+ fileChange = new EventEmitter();
3566
+ uploadSuccess = new EventEmitter();
3567
+ uploadError = new EventEmitter();
3568
+ uploadProgressChange = new EventEmitter();
3569
+ // Readable signals created from @Input() decorator values
3570
+ labelSignal = signal(this.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : []));
3571
+ acceptSignal = signal(this.accept, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : []));
3572
+ multipleSignal = signal(this.multiple, ...(ngDevMode ? [{ debugName: "multipleSignal" }] : []));
3573
+ disabledSignal = signal(this.disabled, ...(ngDevMode ? [{ debugName: "disabledSignal" }] : []));
3574
+ requiredSignal = signal(this.required, ...(ngDevMode ? [{ debugName: "requiredSignal" }] : []));
3575
+ helperTextSignal = signal(this.helperText, ...(ngDevMode ? [{ debugName: "helperTextSignal" }] : []));
3576
+ errorTextSignal = signal(this.errorText, ...(ngDevMode ? [{ debugName: "errorTextSignal" }] : []));
3577
+ showPreviewSignal = signal(this.showPreview, ...(ngDevMode ? [{ debugName: "showPreviewSignal" }] : []));
3578
+ previewWidthSignal = signal(this.previewWidth, ...(ngDevMode ? [{ debugName: "previewWidthSignal" }] : []));
3579
+ previewHeightSignal = signal(this.previewHeight, ...(ngDevMode ? [{ debugName: "previewHeightSignal" }] : []));
3580
+ previewBoxModeSignal = signal(this.previewBoxMode, ...(ngDevMode ? [{ debugName: "previewBoxModeSignal" }] : []));
3581
+ showFileNameSignal = signal(this.showFileName, ...(ngDevMode ? [{ debugName: "showFileNameSignal" }] : []));
3582
+ placeholderTextSignal = signal(this.placeholderText, ...(ngDevMode ? [{ debugName: "placeholderTextSignal" }] : []));
3583
+ placeholderIconSignal = signal(this.placeholderIcon, ...(ngDevMode ? [{ debugName: "placeholderIconSignal" }] : []));
3584
+ autoUploadSignal = signal(this.autoUpload, ...(ngDevMode ? [{ debugName: "autoUploadSignal" }] : []));
3585
+ uploadDataSignal = signal(this.uploadData, ...(ngDevMode ? [{ debugName: "uploadDataSignal" }] : []));
3586
+ showFloatingUploaderSignal = signal(this.showFloatingUploader, ...(ngDevMode ? [{ debugName: "showFloatingUploaderSignal" }] : []));
3587
+ floatingUploaderGroupIdSignal = signal(this.floatingUploaderGroupId, ...(ngDevMode ? [{ debugName: "floatingUploaderGroupIdSignal" }] : []));
3588
+ // Reactive state with signals
3589
+ id = signal(Math.random().toString(36).substring(2, 10), ...(ngDevMode ? [{ debugName: "id" }] : []));
3590
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
3591
+ uploadProgress = signal(0, ...(ngDevMode ? [{ debugName: "uploadProgress" }] : []));
3592
+ uploadStatus = signal('idle', ...(ngDevMode ? [{ debugName: "uploadStatus" }] : []));
3593
+ files = signal(null, ...(ngDevMode ? [{ debugName: "files" }] : []));
3594
+ fileNames = signal([], ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
3595
+ previewUrls = signal([], ...(ngDevMode ? [{ debugName: "previewUrls" }] : []));
3596
+ uploadNotificationId = signal(null, ...(ngDevMode ? [{ debugName: "uploadNotificationId" }] : []));
3597
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
3598
+ groupId = signal(null, ...(ngDevMode ? [{ debugName: "groupId" }] : [])); // Group ID for multiple file uploads
3599
+ isMultipleUploadMode = signal(false, ...(ngDevMode ? [{ debugName: "isMultipleUploadMode" }] : [])); // Flag to track if we're in multiple upload mode
3600
+ // Computed signals for better relationships
3601
+ hasFiles = computed(() => this.files() !== null && this.files().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
3602
+ canUpload = computed(() => this.hasFiles() && !this.isUploading() && !this.disabledSignal(), ...(ngDevMode ? [{ debugName: "canUpload" }] : []));
3603
+ isInErrorState = computed(() => this.uploadStatus() === 'error', ...(ngDevMode ? [{ debugName: "isInErrorState" }] : []));
3604
+ isInSuccessState = computed(() => this.uploadStatus() === 'success', ...(ngDevMode ? [{ debugName: "isInSuccessState" }] : []));
3605
+ // Angular 20: Computed values using new features
3606
+ totalFileSize = computed(() => {
3607
+ if (!this.files())
3608
+ return 0;
3609
+ return Array.from(this.files()).reduce((total, file) => total + file.size, 0);
3610
+ }, ...(ngDevMode ? [{ debugName: "totalFileSize" }] : []));
3611
+ fileSizeInMB = computed(() => {
3612
+ // Angular 20: Using ** operator for exponentiation
3613
+ return (this.totalFileSize() / (1024 ** 2)).toFixed(2);
3614
+ }, ...(ngDevMode ? [{ debugName: "fileSizeInMB" }] : []));
3615
+ uploadProgressPercentage = computed(() => {
3616
+ // Angular 20: Using ** operator for exponentiation
3617
+ return Math.round(this.uploadProgress() ** 1); // Simple power operation
3618
+ }, ...(ngDevMode ? [{ debugName: "uploadProgressPercentage" }] : []));
3619
+ // ControlValueAccessor callbacks
3620
+ onChange = (value) => { };
3621
+ onTouched = () => { };
3622
+ onValidatorChange = () => { };
3623
+ // Computed values
3624
+ hasImages = computed(() => this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "hasImages" }] : []));
3625
+ isPreviewBoxMode = computed(() => this.previewBoxModeSignal() && this.showPreviewSignal(), ...(ngDevMode ? [{ debugName: "isPreviewBoxMode" }] : []));
3626
+ isImagePreviewAvailable = computed(() => this.showPreviewSignal() && this.previewUrls().length > 0, ...(ngDevMode ? [{ debugName: "isImagePreviewAvailable" }] : []));
3627
+ constructor() {
3628
+ // Angular 20: afterRenderEffect for DOM operations
3629
+ afterRenderEffect(() => {
3630
+ // Update file input element when files change
3631
+ if (this.files()) {
3632
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
3633
+ if (fileInput) {
3634
+ // Ensure the input reflects the current state
3635
+ fileInput.files = this.files();
3636
+ }
3637
+ }
3638
+ });
3639
+ // Angular 20: afterNextRender for one-time DOM operations
3640
+ afterNextRender(() => {
3641
+ console.log('🎯 [FileInput] Component rendered and DOM is ready');
3642
+ });
3643
+ }
3644
+ ngOnInit() {
3645
+ // Update signals with initial @Input() values
3646
+ this.labelSignal.set(this.label);
3647
+ this.acceptSignal.set(this.accept);
3648
+ this.multipleSignal.set(this.multiple);
3649
+ this.disabledSignal.set(this.disabled);
3650
+ this.requiredSignal.set(this.required);
3651
+ this.helperTextSignal.set(this.helperText);
3652
+ this.errorTextSignal.set(this.errorText);
3653
+ this.showPreviewSignal.set(this.showPreview);
3654
+ this.previewWidthSignal.set(this.previewWidth);
3655
+ this.previewHeightSignal.set(this.previewHeight);
3656
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3657
+ this.showFileNameSignal.set(this.showFileName);
3658
+ this.placeholderTextSignal.set(this.placeholderText);
3659
+ this.placeholderIconSignal.set(this.placeholderIcon);
3660
+ this.autoUploadSignal.set(this.autoUpload);
3661
+ this.uploadDataSignal.set(this.uploadData);
3662
+ }
3663
+ ngOnChanges(changes) {
3664
+ // Angular 20: Update signals when @Input() values change
3665
+ if (changes['label'])
3666
+ this.labelSignal.set(this.label);
3667
+ if (changes['accept'])
3668
+ this.acceptSignal.set(this.accept);
3669
+ if (changes['multiple'])
3670
+ this.multipleSignal.set(this.multiple);
3671
+ if (changes['disabled'])
3672
+ this.disabledSignal.set(this.disabled);
3673
+ if (changes['required'])
3674
+ this.requiredSignal.set(this.required);
3675
+ if (changes['helperText'])
3676
+ this.helperTextSignal.set(this.helperText);
3677
+ if (changes['errorText'])
3678
+ this.errorTextSignal.set(this.errorText);
3679
+ if (changes['showPreview'])
3680
+ this.showPreviewSignal.set(this.showPreview);
3681
+ if (changes['previewWidth'])
3682
+ this.previewWidthSignal.set(this.previewWidth);
3683
+ if (changes['previewHeight'])
3684
+ this.previewHeightSignal.set(this.previewHeight);
3685
+ if (changes['previewBoxMode'])
3686
+ this.previewBoxModeSignal.set(this.previewBoxMode);
3687
+ if (changes['showFileName'])
3688
+ this.showFileNameSignal.set(this.showFileName);
3689
+ if (changes['placeholderText'])
3690
+ this.placeholderTextSignal.set(this.placeholderText);
3691
+ if (changes['placeholderIcon'])
3692
+ this.placeholderIconSignal.set(this.placeholderIcon);
3693
+ if (changes['autoUpload'])
3694
+ this.autoUploadSignal.set(this.autoUpload);
3695
+ if (changes['uploadData'])
3696
+ this.uploadDataSignal.set(this.uploadData);
3697
+ }
3698
+ writeValue(value) {
3699
+ console.log('📝 [FileInput] writeValue called with:', value);
3700
+ if (typeof value === 'string') {
3701
+ // Value is an uploaded file ID - fetch file details and set preview
3702
+ console.log('📝 [FileInput] Value is uploaded file ID:', value);
3703
+ this.files.set(null);
3704
+ this.fileNames.set([]);
3705
+ this.clearPreviews();
3706
+ // Fetch file details to get base64 and set preview
3707
+ this.loadFileDetailsFromId(value);
4127
3708
  }
4128
- // Check file type
4129
- if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(file.type)) {
4130
- console.warn(`⚠️ [GlobalFileUploader] File type ${file.type} not allowed`);
4131
- return false;
3709
+ else if (value instanceof FileList) {
3710
+ // Value is a FileList
3711
+ console.log('📝 [FileInput] Value is FileList:', Array.from(value).map(f => f.name));
3712
+ this.files.set(value);
3713
+ this.fileNames.set(Array.from(value).map(f => f.name));
3714
+ this.generatePreviews();
4132
3715
  }
4133
- return true;
3716
+ else {
3717
+ // Value is null
3718
+ console.log('📝 [FileInput] Value is null');
3719
+ this.files.set(null);
3720
+ this.fileNames.set([]);
3721
+ this.clearPreviews();
3722
+ }
3723
+ }
3724
+ registerOnChange(fn) {
3725
+ this.onChange = fn;
3726
+ }
3727
+ registerOnTouched(fn) {
3728
+ this.onTouched = fn;
3729
+ }
3730
+ registerOnValidatorChange(fn) {
3731
+ this.onValidatorChange = fn;
4134
3732
  }
4135
- /**
4136
- * Process upload queue
4137
- */
4138
- processUploadQueue() {
4139
- const pendingUploads = this.uploadQueue().filter(upload => upload.status === 'pending');
4140
- if (pendingUploads.length > 0 && !this.isUploading()) {
4141
- this.isUploading.set(true);
4142
- if (this.multiple && pendingUploads.length > 1) {
4143
- // Multiple file upload with group ID
4144
- this.uploadMultipleFiles(pendingUploads);
3733
+ setDisabledState(isDisabled) {
3734
+ // Note: With input signals, disabled state is controlled by the parent component
3735
+ // This method is kept for ControlValueAccessor compatibility but doesn't modify the signal
3736
+ console.log('🔧 [FileInput] setDisabledState called with:', isDisabled, '(controlled by parent component)');
3737
+ }
3738
+ onFileSelected(event) {
3739
+ console.log('🔍 [FileInput] onFileSelected called');
3740
+ const input = event.target;
3741
+ const selectedFiles = input.files;
3742
+ this.files.set(selectedFiles);
3743
+ this.fileNames.set(selectedFiles ? Array.from(selectedFiles).map(f => f.name) : []);
3744
+ console.log('📁 [FileInput] Files selected:', this.fileNames());
3745
+ this.generatePreviews();
3746
+ // Reset upload status when new file is selected
3747
+ this.uploadStatus.set('idle');
3748
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3749
+ this.onChange(selectedFiles);
3750
+ this.fileChange.emit(selectedFiles);
3751
+ this.onTouched();
3752
+ // Show floating uploader if enabled and files are selected
3753
+ if (this.showFloatingUploaderSignal() && selectedFiles && selectedFiles.length > 0 && this.floatingUploader) {
3754
+ console.log('👁️ [FileInput] Showing floating uploader for', selectedFiles.length, 'files');
3755
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
3756
+ }
3757
+ // Auto upload if enabled
3758
+ if (this.autoUploadSignal() && selectedFiles && selectedFiles.length > 0) {
3759
+ if (this.multipleSignal()) {
3760
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode:', selectedFiles.length, 'files');
3761
+ this.uploadMultipleFiles(Array.from(selectedFiles));
4145
3762
  }
4146
3763
  else {
4147
- // Single file upload
4148
- this.uploadNextFile();
3764
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode:', selectedFiles[0].name);
3765
+ this.uploadFile(selectedFiles[0]);
4149
3766
  }
4150
3767
  }
3768
+ else {
3769
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
3770
+ }
4151
3771
  }
4152
- /**
4153
- * Upload multiple files with group ID
4154
- */
4155
- uploadMultipleFiles(uploads) {
4156
- console.log('📤 [GlobalFileUploader] Starting multiple file upload for:', uploads.length, 'files');
4157
- // Generate group ID first
4158
- this.fileService.generateObjectId()
4159
- .pipe(takeUntilDestroyed(this.destroyRef))
4160
- .subscribe({
3772
+ clearFiles() {
3773
+ console.log('🗑️ [FileInput] clearFiles called');
3774
+ this.files.set(null);
3775
+ this.fileNames.set([]);
3776
+ this.clearPreviews();
3777
+ this.uploadStatus.set('idle');
3778
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
3779
+ this.onChange(null);
3780
+ this.fileChange.emit(null);
3781
+ }
3782
+ uploadFile(file) {
3783
+ console.log('📤 [FileInput] uploadFile called for:', file.name, 'Size:', file.size, 'bytes');
3784
+ // Angular 20: Use PendingTasks for better loading state management
3785
+ // const uploadTask = this.pendingTasks.add(); // TODO: Fix PendingTasks API usage
3786
+ // console.log('⏳ [FileInput] Pending task added for upload tracking');
3787
+ // Set upload status to 'start' before starting upload
3788
+ this.uploadStatus.set('start');
3789
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3790
+ this.isUploading.set(true);
3791
+ this.uploadProgress.set(0);
3792
+ this.uploadProgressChange.emit(0);
3793
+ console.log('📊 [FileInput] Upload progress initialized to 0%');
3794
+ // Make form control invalid during upload - this prevents form submission
3795
+ this.onChange(null);
3796
+ console.log('🚫 [FileInput] Form control value set to null to prevent submission');
3797
+ // Show initial progress notification with spinner (persistent - no auto-dismiss)
3798
+ const notificationId = this.notificationService.showProgress('🔄 Preparing file upload...', 0, { duration: 0 });
3799
+ this.uploadNotificationId.set(notificationId);
3800
+ console.log('🔔 [FileInput] Progress notification started with ID:', notificationId);
3801
+ this.fileManagerService.uploadFile(file, this.uploadDataSignal(), (progress) => {
3802
+ // Real progress callback from file manager service
3803
+ this.uploadProgress.set(progress);
3804
+ this.uploadProgressChange.emit(progress);
3805
+ console.log('📈 [FileInput] Upload progress:', Math.round(progress) + '%');
3806
+ // Set upload status to 'uploading' when progress starts
3807
+ if (this.uploadStatus() === 'start') {
3808
+ this.uploadStatus.set('uploading');
3809
+ console.log('🔄 [FileInput] Upload status changed to:', this.uploadStatus());
3810
+ }
3811
+ // Update progress notification with spinner
3812
+ const notificationId = this.uploadNotificationId();
3813
+ if (notificationId) {
3814
+ let progressMessage = '';
3815
+ if (progress < 10) {
3816
+ progressMessage = '🔄 Starting upload...';
3817
+ }
3818
+ else if (progress < 25) {
3819
+ progressMessage = '🔄 Uploading file...';
3820
+ }
3821
+ else if (progress < 50) {
3822
+ progressMessage = '🔄 Upload in progress...';
3823
+ }
3824
+ else if (progress < 75) {
3825
+ progressMessage = '🔄 Almost done...';
3826
+ }
3827
+ else if (progress < 95) {
3828
+ progressMessage = '🔄 Finishing upload...';
3829
+ }
3830
+ else {
3831
+ progressMessage = '🔄 Finalizing...';
3832
+ }
3833
+ this.notificationService.updateProgress(notificationId, progress, progressMessage);
3834
+ }
3835
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4161
3836
  next: (response) => {
4162
- const groupId = response.data?.objectId;
4163
- console.log('🆔 [GlobalFileUploader] Generated group ID:', groupId);
4164
- this.currentGroupId.set(groupId || null);
4165
- if (groupId) {
4166
- // Upload all files with the same group ID
4167
- this.uploadFilesWithGroupId(uploads, groupId);
3837
+ console.log('🎉 [FileInput] Upload SUCCESS - Response received:', response);
3838
+ // Angular 20: Complete the pending task
3839
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3840
+ // console.log('✅ [FileInput] Pending task completed for successful upload');
3841
+ // Set upload status to 'success'
3842
+ this.uploadStatus.set('success');
3843
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3844
+ // Complete the progress
3845
+ this.uploadProgress.set(100);
3846
+ this.uploadProgressChange.emit(100);
3847
+ console.log('📊 [FileInput] Upload progress completed: 100%');
3848
+ // Update progress notification to complete
3849
+ const notificationId = this.uploadNotificationId();
3850
+ if (notificationId) {
3851
+ this.notificationService.remove(notificationId);
3852
+ console.log('🔔 [FileInput] Progress notification removed');
3853
+ }
3854
+ // Success notification removed for cleaner UX
3855
+ this.uploadNotificationId.set(null);
3856
+ // Extract ID from CoreFileManagerInsertUpdateResponse
3857
+ const uploadedId = response?.data?.core_file_manager?.[0]?.cyfm_id;
3858
+ if (uploadedId) {
3859
+ console.log('✅ [FileInput] File uploaded successfully with ID:', uploadedId);
3860
+ // Set the uploaded ID as the form control value
3861
+ this.onChange(uploadedId);
3862
+ console.log('📝 [FileInput] Form control value set to uploaded ID:', uploadedId);
3863
+ // Only emit individual uploadSuccess if not in multiple upload mode
3864
+ if (!this.isMultipleUploadMode()) {
3865
+ this.uploadSuccess.emit(uploadedId);
3866
+ console.log('📝 [FileInput] Upload success event emitted with file ID:', uploadedId);
3867
+ }
3868
+ else {
3869
+ console.log('📝 [FileInput] Individual upload success suppressed (multiple upload mode) - file ID:', uploadedId);
3870
+ }
4168
3871
  }
4169
3872
  else {
4170
- console.error('❌ [GlobalFileUploader] No group ID received');
4171
- this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
3873
+ console.error('❌ [FileInput] Upload successful but no ID returned:', response);
3874
+ this.uploadError.emit('Upload successful but no ID returned');
4172
3875
  }
3876
+ this.isUploading.set(false);
3877
+ console.log('🔄 [FileInput] isUploading set to false');
4173
3878
  },
4174
3879
  error: (error) => {
4175
- console.error(' [GlobalFileUploader] Failed to generate group ID:', error);
4176
- this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
3880
+ console.error('💥 [FileInput] Upload FAILED:', error);
3881
+ // Angular 20: Complete the pending task even on error
3882
+ // this.pendingTasks.complete(uploadTask); // TODO: Fix PendingTasks API usage
3883
+ // console.log('❌ [FileInput] Pending task completed for failed upload');
3884
+ // Set upload status to 'error' and remove upload validation error
3885
+ this.uploadStatus.set('error');
3886
+ console.log('🔄 [FileInput] Upload status set to:', this.uploadStatus());
3887
+ // Remove progress notification and show error
3888
+ const notificationId = this.uploadNotificationId();
3889
+ if (notificationId) {
3890
+ this.notificationService.remove(notificationId);
3891
+ console.log('🔔 [FileInput] Progress notification removed due to error');
3892
+ }
3893
+ this.notificationService.error(`❌ File upload failed: ${error.message || error.error?.message || 'Unknown error occurred'}`, { duration: 0 });
3894
+ this.uploadNotificationId.set(null);
3895
+ this.uploadError.emit(error.message || error.error?.message || 'Upload failed');
3896
+ this.isUploading.set(false);
3897
+ this.uploadProgress.set(0);
3898
+ this.uploadProgressChange.emit(0);
3899
+ console.log('🔄 [FileInput] Upload state reset - isUploading: false, progress: 0%');
4177
3900
  }
4178
3901
  });
4179
3902
  }
4180
3903
  /**
4181
- * Upload files with group ID
3904
+ * Upload multiple files with group ID support
3905
+ * FLOW: 1) Generate group ID first, 2) Upload all files with same group ID, 3) Emit group ID on completion
4182
3906
  */
4183
- uploadFilesWithGroupId(uploads, groupId) {
4184
- let completedCount = 0;
4185
- let failedCount = 0;
4186
- const totalFiles = uploads.length;
4187
- const uploadedFiles = [];
4188
- uploads.forEach((upload, index) => {
4189
- // Update status to uploading
4190
- this.updateUploadStatus(upload.fileId, 'uploading', 0);
4191
- const uploadRequest = {
4192
- file: upload.file,
4193
- userId: this.userId,
4194
- groupId: groupId,
4195
- tags: [],
4196
- permissions: ['read', 'write']
4197
- };
4198
- this.fileService.uploadFile(upload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4199
- // Use the original fileId for progress updates during upload
4200
- this.updateUploadProgress(upload.fileId, progress);
4201
- })
4202
- .pipe(takeUntilDestroyed(this.destroyRef))
4203
- .subscribe({
4204
- next: (response) => {
4205
- completedCount++;
4206
- console.log(`✅ [GlobalFileUploader] File ${index + 1}/${totalFiles} uploaded successfully`);
4207
- // Find the upload item by cyfm_temp_unique_id from response
4208
- const responseFile = response?.data?.core_file_manager?.[0];
4209
- const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4210
- console.log('🔍 [GlobalFileUploader] Response tempUniqueId:', tempUniqueId);
4211
- console.log('🔍 [GlobalFileUploader] Original upload fileId:', upload.fileId);
4212
- // Use the tempUniqueId to find and update the correct upload item
4213
- if (tempUniqueId) {
4214
- this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4215
- }
4216
- else {
4217
- // Fallback to original fileId
4218
- this.updateUploadStatus(upload.fileId, 'completed', 100, undefined, response);
4219
- }
4220
- this.uploadComplete.emit({
4221
- fileId: tempUniqueId || upload.fileId,
4222
- fileName: upload.fileName,
4223
- progress: 100,
4224
- status: 'completed',
4225
- uploadedFile: response
4226
- });
4227
- if (responseFile) {
4228
- uploadedFiles.push(responseFile);
3907
+ uploadMultipleFiles(files) {
3908
+ console.log('📤 [FileInput] uploadMultipleFiles called for:', files.length, 'files');
3909
+ console.log('🔄 [FileInput] STEP 1: Generate group ID before starting any file uploads');
3910
+ // Set multiple upload mode flag
3911
+ this.isMultipleUploadMode.set(true);
3912
+ // Set upload status to 'start' before starting upload
3913
+ this.uploadStatus.set('start');
3914
+ this.isUploading.set(true);
3915
+ this.uploadProgress.set(0);
3916
+ this.uploadProgressChange.emit(0);
3917
+ // Make form control invalid during upload
3918
+ this.onChange(null);
3919
+ // Show initial progress notification
3920
+ const notificationId = this.notificationService.showProgress('🔄 Preparing multiple file upload...', 0, { duration: 0 });
3921
+ this.uploadNotificationId.set(notificationId);
3922
+ // STEP 1: Generate or get group ID BEFORE starting any file uploads
3923
+ const existingGroupId = this.uploadDataSignal().groupId;
3924
+ if (existingGroupId) {
3925
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Using existing group ID:', existingGroupId);
3926
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', existingGroupId);
3927
+ this.groupId.set(existingGroupId);
3928
+ this.startMulti(files, existingGroupId);
3929
+ }
3930
+ else {
3931
+ console.log('🆔 [FileInput] No existing group ID, generating new one...');
3932
+ // Generate group ID BEFORE starting any file uploads
3933
+ this.fileManagerService.generateObjectId().subscribe({
3934
+ next: (response) => {
3935
+ const newGroupId = response.data?.objectId;
3936
+ console.log('🆔 [FileInput] STEP 1 COMPLETE: Generated new group ID:', newGroupId);
3937
+ console.log('🔄 [FileInput] STEP 2: Starting file uploads with group ID:', newGroupId);
3938
+ this.groupId.set(newGroupId);
3939
+ this.startMulti(files, newGroupId);
3940
+ },
3941
+ error: (error) => {
3942
+ console.error(' [FileInput] Failed to generate group ID:', error);
3943
+ this.uploadError.emit('Failed to generate group ID');
3944
+ this.isUploading.set(false);
3945
+ this.uploadStatus.set('error');
3946
+ const notificationId = this.uploadNotificationId();
3947
+ if (notificationId) {
3948
+ this.notificationService.remove(notificationId);
4229
3949
  }
4230
- console.log('🔍 [GlobalFileUploader] Upload response received:', response);
4231
- console.log('🔍 [GlobalFileUploader] Response data structure:', response?.data);
3950
+ this.notificationService.error(' Failed to generate group ID for multiple file upload', { duration: 0 });
3951
+ this.uploadNotificationId.set(null);
3952
+ }
3953
+ });
3954
+ }
3955
+ }
3956
+ /**
3957
+ * Start uploading multiple files with the provided group ID
3958
+ * All files will be uploaded with the SAME group ID that was generated before this method
3959
+ */
3960
+ startMulti(files, groupId) {
3961
+ console.log('🚀 [FileInput] STEP 2: Starting upload for', files.length, 'files with group ID:', groupId);
3962
+ console.log('📋 [FileInput] All files will use the same group ID:', groupId);
3963
+ let completedUploads = 0;
3964
+ let failedUploads = 0;
3965
+ const totalFiles = files.length;
3966
+ // IMPORTANT: All files use the SAME group ID that was generated before starting uploads
3967
+ const uploadDataWithGroupId = {
3968
+ ...this.uploadDataSignal(),
3969
+ groupId: groupId
3970
+ };
3971
+ files.forEach((file, index) => {
3972
+ console.log(`📤 [FileInput] Uploading file ${index + 1}/${totalFiles}: "${file.name}" with group ID: ${groupId}`);
3973
+ this.fileManagerService.uploadFile(file, uploadDataWithGroupId, (progress) => {
3974
+ // Calculate overall progress
3975
+ const fileProgress = progress / totalFiles;
3976
+ const overallProgress = ((completedUploads * 100) + fileProgress) / totalFiles;
3977
+ this.uploadProgress.set(overallProgress);
3978
+ this.uploadProgressChange.emit(overallProgress);
3979
+ // Update progress notification
3980
+ const notificationId = this.uploadNotificationId();
3981
+ if (notificationId) {
3982
+ const progressMessage = `🔄 Uploading file ${index + 1} of ${totalFiles}...`;
3983
+ this.notificationService.updateProgress(notificationId, overallProgress, progressMessage);
3984
+ }
3985
+ }).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3986
+ next: (response) => {
3987
+ completedUploads++;
3988
+ console.log(`✅ [FileInput] File ${index + 1}/${totalFiles} uploaded`);
4232
3989
  // Check if all files are completed
4233
- if (completedCount + failedCount === totalFiles) {
4234
- this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
3990
+ if (completedUploads + failedUploads === totalFiles) {
3991
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4235
3992
  }
4236
3993
  },
4237
3994
  error: (error) => {
4238
- failedCount++;
4239
- console.error(`❌ [GlobalFileUploader] File ${index + 1}/${totalFiles} upload failed:`, error);
4240
- this.updateUploadStatus(upload.fileId, 'error', 0, error.message || 'Upload failed');
4241
- this.uploadError.emit({
4242
- fileId: upload.fileId,
4243
- error: error.message || 'Upload failed'
4244
- });
3995
+ failedUploads++;
3996
+ console.error(`❌ [FileInput] File ${index + 1}/${totalFiles} upload failed:`, error);
4245
3997
  // Check if all files are completed
4246
- if (completedCount + failedCount === totalFiles) {
4247
- this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
3998
+ if (completedUploads + failedUploads === totalFiles) {
3999
+ this.handleMultipleUploadComplete(completedUploads, failedUploads, totalFiles, groupId);
4248
4000
  }
4249
4001
  }
4250
4002
  });
4251
4003
  });
4252
4004
  }
4253
4005
  /**
4254
- * Handle multiple upload completion
4006
+ * Handle completion of multiple file upload
4255
4007
  */
4256
- handleMultipleUploadComplete(completed, failed, groupId, uploadedFiles) {
4257
- console.log(`📊 [GlobalFileUploader] Multiple upload complete: ${completed}/${completed + failed} successful`);
4008
+ handleMultipleUploadComplete(completed, failed, total, groupId) {
4009
+ console.log(`📊 [FileInput] Multiple upload complete: ${completed}/${total} successful, ${failed} failed`);
4258
4010
  this.isUploading.set(false);
4259
- if (completed > 0) {
4260
- this.allUploadsComplete.emit({
4261
- groupId: groupId,
4262
- uploadedFiles: uploadedFiles
4263
- });
4011
+ this.uploadProgress.set(100);
4012
+ this.uploadProgressChange.emit(100);
4013
+ // Remove progress notification
4014
+ const notificationId = this.uploadNotificationId();
4015
+ if (notificationId) {
4016
+ this.notificationService.remove(notificationId);
4017
+ }
4018
+ this.uploadNotificationId.set(null);
4019
+ if (failed === 0) {
4020
+ // All files uploaded successfully
4021
+ this.uploadStatus.set('success');
4022
+ // Success notification removed for cleaner UX
4023
+ // STEP 3: For multiple file upload, emit the group ID (not individual file IDs)
4024
+ this.onChange(groupId);
4025
+ this.uploadSuccess.emit(groupId);
4026
+ console.log('📝 [FileInput] Multiple upload completed with group ID:', groupId);
4027
+ }
4028
+ else if (completed > 0) {
4029
+ // Some files uploaded successfully
4030
+ this.uploadStatus.set('error');
4031
+ this.notificationService.warning(`⚠️ ${completed}/${total} files uploaded. ${failed} failed.`, { duration: 0 });
4032
+ this.uploadError.emit(`${failed} out of ${total} files failed to upload`);
4033
+ }
4034
+ else {
4035
+ // All files failed
4036
+ this.uploadStatus.set('error');
4037
+ this.notificationService.error(`❌ All ${total} files failed to upload.`, { duration: 0 });
4038
+ this.uploadError.emit('All files failed to upload');
4264
4039
  }
4040
+ // Reset multiple upload mode flag
4041
+ this.isMultipleUploadMode.set(false);
4265
4042
  }
4266
- /**
4267
- * Upload next file in queue (single file mode)
4268
- */
4269
- uploadNextFile() {
4270
- const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
4271
- if (!pendingUpload) {
4272
- this.isUploading.set(false);
4043
+ generatePreviews() {
4044
+ // Clear existing previews
4045
+ this.clearPreviews();
4046
+ if (!this.showPreviewSignal() || !this.files()) {
4273
4047
  return;
4274
4048
  }
4275
- // Update status to uploading
4276
- this.updateUploadStatus(pendingUpload.fileId, 'uploading', 0);
4277
- const uploadRequest = {
4278
- file: pendingUpload.file,
4279
- userId: this.userId,
4280
- tags: [],
4281
- permissions: ['read', 'write']
4282
- };
4283
- this.fileService.uploadFile(pendingUpload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4284
- this.updateUploadProgress(pendingUpload.fileId, progress);
4285
- })
4286
- .pipe(takeUntilDestroyed(this.destroyRef))
4287
- .subscribe({
4288
- next: (response) => {
4289
- console.log('✅ [GlobalFileUploader] Single file upload successful:', response);
4290
- // Find the upload item by cyfm_temp_unique_id from response
4291
- const responseFile = response?.data?.core_file_manager?.[0];
4292
- const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4293
- console.log('🔍 [GlobalFileUploader] Single upload - Response tempUniqueId:', tempUniqueId);
4294
- console.log('🔍 [GlobalFileUploader] Single upload - Original upload fileId:', pendingUpload.fileId);
4295
- // Use the tempUniqueId to find and update the correct upload item
4296
- if (tempUniqueId) {
4297
- this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4049
+ Array.from(this.files()).forEach(file => {
4050
+ if (this.isImageFile(file)) {
4051
+ const reader = new FileReader();
4052
+ reader.onload = (e) => {
4053
+ if (e.target?.result) {
4054
+ this.previewUrls.update(urls => [...urls, e.target.result]);
4055
+ }
4056
+ };
4057
+ reader.readAsDataURL(file);
4058
+ }
4059
+ });
4060
+ }
4061
+ clearPreviews() {
4062
+ // Revoke object URLs to prevent memory leaks
4063
+ this.previewUrls().forEach(url => {
4064
+ if (url.startsWith('blob:')) {
4065
+ URL.revokeObjectURL(url);
4066
+ }
4067
+ });
4068
+ this.previewUrls.set([]);
4069
+ }
4070
+ isImageFile(file) {
4071
+ return file.type.startsWith('image/');
4072
+ }
4073
+ loadFileDetailsFromId(fileId) {
4074
+ console.log('🔍 [FileInput] Loading file details for ID:', fileId);
4075
+ if (!fileId)
4076
+ return;
4077
+ this.fileManagerService?.getFileDetails({ cyfm_id: fileId })?.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
4078
+ next: (fileDetails) => {
4079
+ console.log('📋 [FileInput] File details received:', fileDetails);
4080
+ if (fileDetails?.data?.length) {
4081
+ const fileData = fileDetails.data[0];
4082
+ console.log('📁 [FileInput] File data:', fileData);
4083
+ // Set file name from the details
4084
+ if (fileData.cyfm_name) {
4085
+ this.fileNames.set([fileData.cyfm_name]);
4086
+ console.log('📝 [FileInput] File name set:', fileData.cyfm_name);
4087
+ }
4088
+ // If it's an image and we have base64 data, set preview
4089
+ if (this.showPreviewSignal() && fileData.cyfm_file_base64) {
4090
+ // Check if it's an image file based on file name or type
4091
+ const isImage = this.isImageFileFromName(fileData.cyfm_name || '') ||
4092
+ this.isImageFileFromType(fileData.cyfm_type || '');
4093
+ if (isImage) {
4094
+ // Add data URL prefix if not already present
4095
+ let base64Data = fileData.cyfm_file_base64;
4096
+ if (!base64Data.startsWith('data:')) {
4097
+ const mimeType = fileData.cyfm_type || 'image/jpeg';
4098
+ base64Data = `data:${mimeType};base64,${base64Data}`;
4099
+ }
4100
+ this.previewUrls.set([base64Data]);
4101
+ console.log('🖼️ [FileInput] Preview set from base64 data');
4102
+ }
4103
+ }
4298
4104
  }
4299
4105
  else {
4300
- // Fallback to original fileId
4301
- this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
4106
+ console.warn('⚠️ [FileInput] No file data found for ID:', fileId);
4302
4107
  }
4303
- this.uploadComplete.emit({
4304
- fileId: tempUniqueId || pendingUpload.fileId,
4305
- fileName: pendingUpload.fileName,
4306
- progress: 100,
4307
- status: 'completed',
4308
- uploadedFile: response
4309
- });
4310
- // Process next file
4311
- setTimeout(() => this.uploadNextFile(), 500);
4312
4108
  },
4313
4109
  error: (error) => {
4314
- console.error('❌ [GlobalFileUploader] Upload error:', error);
4315
- this.updateUploadStatus(pendingUpload.fileId, 'error', 0, error.message || 'Upload failed');
4316
- this.uploadError.emit({
4317
- fileId: pendingUpload.fileId,
4318
- error: error.message || 'Upload failed'
4319
- });
4320
- // Process next file
4321
- setTimeout(() => this.uploadNextFile(), 500);
4110
+ console.error('❌ [FileInput] Error loading file details:', error);
4111
+ this.notificationService.error(`Failed to load file details: ${error.message || 'Unknown error'}`, { duration: 0 });
4322
4112
  }
4323
4113
  });
4324
4114
  }
4325
- /**
4326
- * Update upload progress
4327
- */
4328
- updateUploadProgress(fileId, progress) {
4329
- this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4330
- ? { ...upload, progress }
4331
- : upload));
4332
- }
4333
- /**
4334
- * Update upload status
4335
- */
4336
- updateUploadStatus(fileId, status, progress, error, uploadedFile) {
4337
- console.log('🔄 [GlobalFileUploader] Updating upload status:', { fileId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4338
- this.uploadQueue.update(queue => {
4339
- const updatedQueue = queue.map(upload => {
4340
- if (upload.fileId === fileId) {
4341
- console.log('🔄 [GlobalFileUploader] Found upload to update:', upload.fileName, 'from', upload.status, 'to', status);
4342
- return { ...upload, status, progress, error, uploadedFile };
4343
- }
4344
- return upload;
4345
- });
4346
- console.log('🔄 [GlobalFileUploader] Updated queue:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status })));
4347
- return updatedQueue;
4348
- });
4115
+ isImageFileFromName(fileName) {
4116
+ if (!fileName)
4117
+ return false;
4118
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
4119
+ const lowerFileName = fileName.toLowerCase();
4120
+ return imageExtensions.some(ext => lowerFileName.endsWith(ext));
4349
4121
  }
4350
- /**
4351
- * Update upload status by cyfm_temp_unique_id
4352
- */
4353
- updateUploadStatusByTempId(tempUniqueId, status, progress, error, uploadedFile) {
4354
- console.log('🔄 [GlobalFileUploader] Updating upload status by tempId:', { tempUniqueId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4355
- this.uploadQueue.update(queue => {
4356
- const updatedQueue = queue.map(upload => {
4357
- // Check if this upload's fileId matches the tempUniqueId
4358
- if (upload.fileId === tempUniqueId) {
4359
- console.log('🔄 [GlobalFileUploader] Found upload by tempId to update:', upload.fileName, 'from', upload.status, 'to', status);
4360
- return { ...upload, status, progress, error, uploadedFile };
4122
+ isImageFileFromType(fileType) {
4123
+ if (!fileType)
4124
+ return false;
4125
+ return fileType.startsWith('image/');
4126
+ }
4127
+ removePreview(index) {
4128
+ const currentFiles = this.files();
4129
+ const currentUrls = this.previewUrls();
4130
+ if (currentFiles && currentFiles.length > index) {
4131
+ // Handle FileList case - remove file from FileList
4132
+ const dt = new DataTransfer();
4133
+ Array.from(currentFiles).forEach((file, i) => {
4134
+ if (i !== index) {
4135
+ dt.items.add(file);
4361
4136
  }
4362
- return upload;
4363
4137
  });
4364
- console.log('🔄 [GlobalFileUploader] Updated queue by tempId:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status, fileId: u.fileId })));
4365
- return updatedQueue;
4366
- });
4138
+ const newFiles = dt.files;
4139
+ this.files.set(newFiles);
4140
+ this.fileNames.set(Array.from(newFiles).map(f => f.name));
4141
+ // Remove the preview URL
4142
+ if (currentUrls[index] && currentUrls[index].startsWith('blob:')) {
4143
+ URL.revokeObjectURL(currentUrls[index]);
4144
+ }
4145
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4146
+ this.onChange(newFiles);
4147
+ this.fileChange.emit(newFiles);
4148
+ }
4149
+ else if (currentUrls.length > index) {
4150
+ // Handle uploaded file ID case - clear the preview and set control value to null
4151
+ console.log('🗑️ [FileInput] Removing preview for uploaded file ID');
4152
+ // Clear preview
4153
+ this.previewUrls.update(urls => urls.filter((_, i) => i !== index));
4154
+ this.fileNames.set([]);
4155
+ // Set control value to null since we're removing the uploaded file
4156
+ this.onChange(null);
4157
+ this.fileChange.emit(null);
4158
+ }
4367
4159
  }
4368
- /**
4369
- * Handle upload error
4370
- */
4371
- handleUploadError(error, fileId) {
4372
- if (fileId) {
4373
- this.updateUploadStatus(fileId, 'error', 0, error);
4374
- this.uploadError.emit({ fileId, error });
4160
+ ngOnDestroy() {
4161
+ // Clean up preview URLs to prevent memory leaks
4162
+ this.clearPreviews();
4163
+ // Clean up any active upload notification
4164
+ const notificationId = this.uploadNotificationId();
4165
+ if (notificationId) {
4166
+ this.notificationService.remove(notificationId);
4167
+ this.uploadNotificationId.set(null);
4375
4168
  }
4376
- this.isUploading.set(false);
4377
4169
  }
4378
- /**
4379
- * Cancel upload
4380
- */
4381
- cancelUpload(fileId) {
4382
- this.updateUploadStatus(fileId, 'cancelled', 0);
4383
- this.uploadCancelled.emit(fileId);
4384
- // Remove from queue after a delay
4385
- setTimeout(() => {
4386
- this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4387
- }, 1000);
4170
+ triggerFileSelect() {
4171
+ const fileInput = document.getElementById('cide-file-input-' + this.id());
4172
+ if (fileInput && !this.disabledSignal()) {
4173
+ fileInput.click();
4174
+ }
4388
4175
  }
4389
4176
  /**
4390
- * Retry upload
4177
+ * Show floating uploader manually
4178
+ * This can be called to show the floating uploader even when no files are selected
4391
4179
  */
4392
- retryUpload(fileId) {
4393
- this.updateUploadStatus(fileId, 'pending', 0);
4394
- this.processUploadQueue();
4180
+ showUploader() {
4181
+ if (this.floatingUploader && this.showFloatingUploaderSignal()) {
4182
+ console.log('👁️ [FileInput] Manually showing floating uploader');
4183
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4184
+ }
4395
4185
  }
4396
- /**
4397
- * Remove upload from queue
4398
- */
4399
- removeUpload(fileId) {
4400
- this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4186
+ // Drag and Drop Event Handlers
4187
+ onDragOver(event) {
4188
+ event.preventDefault();
4189
+ event.stopPropagation();
4190
+ if (!this.disabledSignal()) {
4191
+ this.isDragOver.set(true);
4192
+ console.log('🔄 [FileInput] Drag over detected');
4193
+ }
4401
4194
  }
4402
- /**
4403
- * Clear completed uploads
4404
- */
4405
- clearCompleted() {
4406
- this.uploadQueue.update(queue => queue.filter(upload => upload.status !== 'completed'));
4195
+ onDragLeave(event) {
4196
+ event.preventDefault();
4197
+ event.stopPropagation();
4198
+ this.isDragOver.set(false);
4199
+ console.log('🔄 [FileInput] Drag leave detected');
4407
4200
  }
4408
- /**
4409
- * Clear all uploads
4410
- */
4411
- clearAll() {
4412
- this.uploadQueue.set([]);
4413
- this.currentGroupId.set(null);
4201
+ onDragEnter(event) {
4202
+ event.preventDefault();
4203
+ event.stopPropagation();
4204
+ if (!this.disabledSignal()) {
4205
+ this.isDragOver.set(true);
4206
+ console.log('🔄 [FileInput] Drag enter detected');
4207
+ }
4414
4208
  }
4415
- /**
4416
- * Get status icon
4417
- */
4418
- getStatusIcon(status) {
4419
- switch (status) {
4420
- case 'pending': return 'schedule';
4421
- case 'uploading': return 'cloud_upload';
4422
- case 'completed': return 'check_circle';
4423
- case 'error': return 'error';
4424
- case 'cancelled': return 'cancel';
4425
- default: return 'help';
4209
+ onDrop(event) {
4210
+ event.preventDefault();
4211
+ event.stopPropagation();
4212
+ this.isDragOver.set(false);
4213
+ if (this.disabledSignal()) {
4214
+ console.log('⏸️ [FileInput] Drop ignored - component is disabled');
4215
+ return;
4216
+ }
4217
+ const files = event.dataTransfer?.files;
4218
+ if (files && files.length > 0) {
4219
+ console.log('📁 [FileInput] Files dropped:', Array.from(files).map(f => f.name));
4220
+ // Validate file types if accept is specified
4221
+ if (this.acceptSignal() && !this.validateFileTypes(files)) {
4222
+ console.log('❌ [FileInput] Invalid file types dropped');
4223
+ this.notificationService.error('❌ Invalid file type. Please select files of the correct type.', { duration: 0 });
4224
+ return;
4225
+ }
4226
+ // Handle single vs multiple files
4227
+ if (!this.multipleSignal() && files.length > 1) {
4228
+ console.log('⚠️ [FileInput] Multiple files dropped but multiple is disabled');
4229
+ this.notificationService.warning('⚠️ Only one file is allowed. Using the first file.', { duration: 0 });
4230
+ // Create a new FileList with only the first file
4231
+ const dt = new DataTransfer();
4232
+ dt.items.add(files[0]);
4233
+ this.handleFileSelection(dt.files);
4234
+ }
4235
+ else {
4236
+ this.handleFileSelection(files);
4237
+ }
4426
4238
  }
4427
4239
  }
4428
- /**
4429
- * Get status class
4430
- */
4431
- getStatusClass(status) {
4432
- switch (status) {
4433
- case 'pending': return 'status-pending';
4434
- case 'uploading': return 'status-uploading';
4435
- case 'completed': return 'status-completed';
4436
- case 'error': return 'status-error';
4437
- case 'cancelled': return 'status-cancelled';
4438
- default: return 'status-unknown';
4240
+ validateFileTypes(files) {
4241
+ const acceptTypes = this.acceptSignal().split(',').map(type => type.trim());
4242
+ if (acceptTypes.length === 0 || acceptTypes[0] === '')
4243
+ return true;
4244
+ return Array.from(files).every(file => {
4245
+ return acceptTypes.some(acceptType => {
4246
+ if (acceptType.startsWith('.')) {
4247
+ // Extension-based validation
4248
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
4249
+ }
4250
+ else if (acceptType.includes('/')) {
4251
+ // MIME type validation
4252
+ return file.type === acceptType || file.type.startsWith(acceptType.replace('*', ''));
4253
+ }
4254
+ return false;
4255
+ });
4256
+ });
4257
+ }
4258
+ handleFileSelection(files) {
4259
+ this.files.set(files);
4260
+ this.fileNames.set(Array.from(files).map(f => f.name));
4261
+ console.log('📁 [FileInput] Files selected via drag & drop:', this.fileNames());
4262
+ this.generatePreviews();
4263
+ // Reset upload status when new file is selected
4264
+ this.uploadStatus.set('idle');
4265
+ console.log('🔄 [FileInput] Upload status reset to:', this.uploadStatus());
4266
+ this.onChange(files);
4267
+ this.fileChange.emit(files);
4268
+ this.onTouched();
4269
+ // Show floating uploader if enabled and files are selected
4270
+ if (this.showFloatingUploaderSignal() && files.length > 0 && this.floatingUploader) {
4271
+ console.log('👁️ [FileInput] Showing floating uploader for dropped files:', files.length, 'files');
4272
+ this.floatingUploader.showUploader(this.floatingUploaderGroupIdSignal());
4273
+ }
4274
+ // Auto upload if enabled
4275
+ if (this.autoUploadSignal() && files.length > 0) {
4276
+ if (this.multipleSignal()) {
4277
+ console.log('🚀 [FileInput] Auto upload enabled for multiple files mode (drag & drop):', files.length, 'files');
4278
+ this.uploadMultipleFiles(Array.from(files));
4279
+ }
4280
+ else {
4281
+ console.log('🚀 [FileInput] Auto upload enabled for single file mode (drag & drop):', files[0].name);
4282
+ this.uploadFile(files[0]);
4283
+ }
4284
+ }
4285
+ else {
4286
+ console.log('⏸️ [FileInput] Auto upload disabled or no files');
4439
4287
  }
4440
4288
  }
4441
- /**
4442
- * Get file size display
4443
- */
4444
- getFileSizeDisplay(file) {
4445
- const bytes = file.size;
4446
- if (bytes === 0)
4447
- return '0 Bytes';
4448
- const k = 1024;
4449
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
4450
- const i = Math.floor(Math.log(bytes) / Math.log(k));
4451
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
4289
+ isRequired() {
4290
+ return this.requiredSignal();
4452
4291
  }
4453
4292
  /**
4454
- * Generate unique file ID - this should match the service's generateFileId method
4455
- * The service uses: ${file.name}_${file.size}_${Date.now()}
4293
+ * Angular 20: Utility method to get upload data with proper typing
4294
+ * @returns Properly typed upload data
4456
4295
  */
4457
- generateFileId(file) {
4458
- return `${file.name}_${file.size}_${Date.now()}`;
4296
+ getUploadData() {
4297
+ return this.uploadDataSignal();
4459
4298
  }
4460
4299
  /**
4461
- * Convert IFileUploadRequest to FileUploadOptions
4300
+ * Angular 20: Utility method to update upload data with type safety
4301
+ * @param data Partial upload data to merge with existing data
4462
4302
  */
4463
- convertToFileUploadOptions(request) {
4303
+ updateUploadData(data) {
4304
+ const currentData = this.uploadDataSignal();
4305
+ const updatedData = { ...currentData, ...data };
4306
+ // Note: This would require the uploadData to be a writable signal
4307
+ // For now, this method serves as a type-safe way to work with upload data
4308
+ console.log('📝 [FileInput] Upload data updated:', updatedData);
4309
+ }
4310
+ getCurrentState() {
4464
4311
  return {
4465
- altText: request.altText,
4466
- userId: request.userId,
4467
- permissions: request.permissions,
4468
- tags: request.tags,
4469
- groupId: request.groupId
4312
+ id: this.id(),
4313
+ label: this.labelSignal(),
4314
+ required: this.requiredSignal(),
4315
+ disabled: this.disabledSignal(),
4316
+ accept: this.acceptSignal(),
4317
+ multiple: this.multipleSignal(),
4318
+ showPreview: this.showPreviewSignal(),
4319
+ autoUpload: this.autoUploadSignal(),
4320
+ uploadStatus: this.uploadStatus(),
4321
+ isUploading: this.isUploading(),
4322
+ uploadProgress: this.uploadProgress(),
4323
+ files: this.files() ? Array.from(this.files()).map(f => ({ name: f.name, size: f.size, type: f.type })) : null,
4324
+ fileNames: this.fileNames(),
4325
+ previewUrls: this.previewUrls().length,
4326
+ helperText: this.helperTextSignal(),
4327
+ errorText: this.errorTextSignal(),
4328
+ placeholderText: this.placeholderTextSignal(),
4329
+ placeholderIcon: this.placeholderIconSignal(),
4330
+ previewWidth: this.previewWidthSignal(),
4331
+ previewHeight: this.previewHeightSignal(),
4332
+ previewBoxMode: this.previewBoxModeSignal(),
4333
+ showFileName: this.showFileNameSignal(),
4334
+ uploadData: this.uploadDataSignal()
4470
4335
  };
4471
4336
  }
4472
- /**
4473
- * Trigger file input click
4474
- */
4475
- triggerFileInput() {
4476
- const fileInput = document.getElementById('global-file-input');
4477
- if (fileInput) {
4478
- fileInput.click();
4337
+ async getControlData() {
4338
+ console.log('🔍 [FileInput] getControlData called');
4339
+ const cide_element_data = await this.elementService?.getElementData({ sype_key: this.id() });
4340
+ if (cide_element_data) {
4341
+ console.log('📋 [FileInput] Element data loaded:', cide_element_data);
4342
+ // Note: Since we're using input signals, we can't directly set their values
4343
+ // This method would need to be refactored to work with the new signal-based approach
4344
+ // For now, we'll log the data and trigger validation
4345
+ console.log('✅ [FileInput] Control data received from element service');
4346
+ console.log('⚠️ [FileInput] Note: Input signals cannot be modified after component initialization');
4347
+ // Trigger validation update
4348
+ this.onValidatorChange();
4349
+ }
4350
+ else {
4351
+ console.log('⚠️ [FileInput] No element data found for key:', this.id());
4479
4352
  }
4480
4353
  }
4481
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4482
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleGlobalFileUploaderComponent, isStandalone: true, selector: "cide-ele-global-file-uploader", inputs: { userId: "userId", multiple: "multiple", accept: "accept", maxFileSize: "maxFileSize", allowedTypes: "allowedTypes" }, outputs: { uploadComplete: "uploadComplete", uploadError: "uploadError", uploadCancelled: "uploadCancelled", allUploadsComplete: "allUploadsComplete" }, ngImport: i0, template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideEleButtonComponent, selector: "button[cideEleButton], a[cideEleButton]", inputs: ["label", "variant", "size", "type", "shape", "elevation", "disabled", "id", "loading", "fullWidth", "leftIcon", "rightIcon", "customClass", "tooltip", "ariaLabel", "testId", "routerLink", "routerExtras", "preventDoubleClick", "animated"], outputs: ["btnClick", "doubleClick"] }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
4354
+ // Validator implementation
4355
+ validate(control) {
4356
+ console.log('🔍 [FileInput] validate() called - uploadStatus:', this.uploadStatus(), 'required:', this.requiredSignal(), 'files:', !!this.files(), 'control.value:', control.value);
4357
+ // If upload is in progress (start or uploading status), return validation error
4358
+ if (this.uploadStatus() === 'start' || this.uploadStatus() === 'uploading') {
4359
+ console.log('⚠️ [FileInput] Validation ERROR: Upload in progress');
4360
+ return { 'uploadInProgress': { message: 'File upload in progress. Please wait...' } };
4361
+ }
4362
+ // If required and no file is selected and no control value (uploaded file ID), return validation error
4363
+ if (this.requiredSignal() && !this.files() && !control.value) {
4364
+ console.log('⚠️ [FileInput] Validation ERROR: File required');
4365
+ return { 'required': { message: 'Please select a file to upload.' } };
4366
+ }
4367
+ console.log('✅ [FileInput] Validation PASSED: No errors');
4368
+ return null; // No validation errors
4369
+ }
4370
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4371
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFileInputComponent, isStandalone: true, selector: "cide-ele-file-input", inputs: { label: "label", accept: "accept", multiple: "multiple", disabled: "disabled", required: "required", helperText: "helperText", errorText: "errorText", showPreview: "showPreview", previewWidth: "previewWidth", previewHeight: "previewHeight", previewBoxMode: "previewBoxMode", showFileName: "showFileName", placeholderText: "placeholderText", placeholderIcon: "placeholderIcon", autoUpload: "autoUpload", uploadData: "uploadData", showFloatingUploader: "showFloatingUploader", floatingUploaderGroupId: "floatingUploaderGroupId" }, outputs: { fileChange: "fileChange", uploadSuccess: "uploadSuccess", uploadError: "uploadError", uploadProgressChange: "uploadProgressChange" }, providers: [
4372
+ {
4373
+ provide: NG_VALUE_ACCESSOR,
4374
+ useExisting: CideEleFileInputComponent,
4375
+ multi: true
4376
+ },
4377
+ {
4378
+ provide: NG_VALIDATORS,
4379
+ useExisting: CideEleFileInputComponent,
4380
+ multi: true
4381
+ }
4382
+ ], usesOnChanges: true, ngImport: i0, template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @if (multipleSignal() && fileNames().length > 1) {\n <!-- Show file count for multiple files -->\n <div class=\"cide-file-input-multiple-count\">\n {{ fileNames().length }} files selected\n </div>\n } @else {\n <!-- Show individual file names for single file or when only one file selected -->\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-multiple-count{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;background-color:#f0f9ff;border:1px solid #0ea5e9;border-radius:.5rem;color:#0369a1;font-weight:500;font-size:.9rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
4483
4383
  }
4484
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, decorators: [{
4384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFileInputComponent, decorators: [{
4485
4385
  type: Component,
4486
- args: [{ selector: 'cide-ele-global-file-uploader', standalone: true, imports: [
4487
- CommonModule,
4488
- CideEleButtonComponent,
4489
- CideIconComponent
4490
- ], template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"] }]
4491
- }], ctorParameters: () => [], propDecorators: { userId: [{
4386
+ args: [{ selector: 'cide-ele-file-input', standalone: true, imports: [CommonModule, FormsModule, CideIconComponent], providers: [
4387
+ {
4388
+ provide: NG_VALUE_ACCESSOR,
4389
+ useExisting: CideEleFileInputComponent,
4390
+ multi: true
4391
+ },
4392
+ {
4393
+ provide: NG_VALIDATORS,
4394
+ useExisting: CideEleFileInputComponent,
4395
+ multi: true
4396
+ }
4397
+ ], template: "<div class=\"cide-file-input\">\n <!-- Label (shown when not in preview box mode or when preview box mode but no label override) -->\n @if (labelSignal() && !isPreviewBoxMode()) {\n <label class=\"cide-file-input-label\" [attr.for]=\"'cide-file-input-' + id()\">\n {{ labelSignal() }}@if (requiredSignal()) {<span class=\"cide-file-input-required\"> *</span>}\n </label>\n }\n \n <!-- Preview Box Mode -->\n @if (isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview-box-container\">\n <!-- Hidden file input -->\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-hidden\"\n />\n \n <!-- Preview Box -->\n <div \n class=\"cide-file-input-preview-box\"\n [class.cide-file-input-preview-box-disabled]=\"disabledSignal()\"\n [class.cide-file-input-preview-box-has-image]=\"hasImages()\"\n [class.cide-file-input-preview-box-drag-over]=\"isDragOver()\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\"\n (click)=\"triggerFileSelect()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n [attr.title]=\"disabledSignal() ? 'File selection disabled' : placeholderTextSignal()\">\n \n <!-- No Image State -->\n @if (!hasImages()) {\n <div class=\"cide-file-input-preview-box-placeholder\">\n <div class=\"cide-file-input-preview-box-icon\">\n <cide-ele-icon>{{ isDragOver() ? '\uD83D\uDCC1' : placeholderIconSignal() }}</cide-ele-icon>\n </div>\n <div class=\"cide-file-input-preview-box-text\">\n {{ isDragOver() ? 'Drop files here...' : placeholderTextSignal() }}\n </div>\n </div>\n }\n \n <!-- Image Preview State -->\n @if (hasImages()) {\n <div class=\"cide-file-input-preview-box-content\">\n <img \n [src]=\"previewUrls()[0]\" \n [alt]=\"fileNames()[0] || 'Preview image'\"\n class=\"cide-file-input-preview-box-image\">\n <div class=\"cide-file-input-preview-box-overlay\">\n <div class=\"cide-file-input-preview-box-overlay-text\">Click to change</div>\n </div>\n @if (!disabledSignal()) {\n <button \n type=\"button\" \n class=\"cide-file-input-preview-box-remove\"\n (click)=\"clearFiles(); $event.stopPropagation()\"\n title=\"Remove image\">\n \u00D7\n </button>\n }\n </div>\n }\n </div>\n \n <!-- File name display for preview box mode -->\n @if (hasImages() && fileNames().length && showFileNameSignal()) {\n <div class=\"cide-file-input-preview-box-filename\">\n {{ fileNames()[0] }}\n </div>\n }\n </div>\n }\n\n <!-- Standard Mode -->\n @if (!isPreviewBoxMode()) {\n <div \n class=\"cide-file-input-wrapper\"\n [class.cide-file-input-drag-over]=\"isDragOver()\"\n (dragover)=\"onDragOver($event)\"\n (dragenter)=\"onDragEnter($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n <input\n type=\"file\"\n [attr.id]=\"'cide-file-input-' + id()\"\n [attr.accept]=\"acceptSignal()\"\n [attr.multiple]=\"multipleSignal() ? true : null\"\n [disabled]=\"disabledSignal()\"\n (change)=\"onFileSelected($event)\"\n class=\"cide-file-input-element\"\n />\n @if (hasFiles()) {\n <button type=\"button\" class=\"cide-file-input-clear\" (click)=\"clearFiles()\">\n Clear\n </button>\n }\n </div>\n @if (hasFiles() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-files\">\n @if (multipleSignal() && fileNames().length > 1) {\n <!-- Show file count for multiple files -->\n <div class=\"cide-file-input-multiple-count\">\n {{ fileNames().length }} files selected\n </div>\n } @else {\n <!-- Show individual file names for single file or when only one file selected -->\n @for (name of fileNames(); track name) {\n <span>{{ name }}</span>\n }\n }\n <!-- Angular 20: Display file size using new computed values -->\n @if (totalFileSize() > 0) {\n <div class=\"cide-file-input-size\">\n Total size: {{ fileSizeInMB() }} MB\n </div>\n }\n </div>\n }\n }\n \n <!-- Image Preview Section (only for standard mode) -->\n @if (isImagePreviewAvailable() && !isPreviewBoxMode()) {\n <div class=\"cide-file-input-preview\">\n <div class=\"cide-file-input-preview-label\">Preview:</div>\n <div class=\"cide-file-input-preview-container\">\n @for (previewUrl of previewUrls(); track previewUrl; let i = $index) {\n <div \n class=\"cide-file-input-preview-item\"\n [style.width]=\"previewWidthSignal()\"\n [style.height]=\"previewHeightSignal()\">\n <button \n type=\"button\" \n class=\"cide-file-input-preview-remove\"\n (click)=\"removePreview(i)\"\n title=\"Remove image\">\n \u00D7\n </button>\n <img \n [src]=\"previewUrl\" \n [alt]=\"fileNames()[i] || 'Preview image'\"\n class=\"cide-file-input-preview-image\"\n loading=\"lazy\">\n <div class=\"cide-file-input-preview-filename\">{{ fileNames()[i] }}</div>\n </div>\n }\n </div>\n </div>\n }\n \n @if (errorTextSignal()) {\n <div class=\"cide-file-input-error\">{{ errorTextSignal() }}</div>\n }\n @if (helperTextSignal() && !errorTextSignal()) {\n <div class=\"cide-file-input-helper\">{{ helperTextSignal() }}</div>\n }\n</div> ", styles: [".cide-file-input{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-label{font-weight:500;margin-bottom:.25rem}.cide-file-input-required{color:#d32f2f;font-weight:700}.cide-file-input-wrapper{display:flex;align-items:center;gap:.5rem;border:2px dashed transparent;border-radius:.5rem;padding:.5rem;transition:all .2s ease-in-out}.cide-file-input-wrapper.cide-file-input-drag-over{border-color:#3b82f6;background-color:#eff6ff;transform:scale(1.02)}.cide-file-input-element{flex:1}.cide-file-input-clear{background:none;border:none;color:#d32f2f;cursor:pointer;font-size:.9rem}.cide-file-input-files{font-size:.95rem;color:#333;margin-top:.25rem}.cide-file-input-multiple-count{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;background-color:#f0f9ff;border:1px solid #0ea5e9;border-radius:.5rem;color:#0369a1;font-weight:500;font-size:.9rem}.cide-file-input-size{margin-top:.5rem;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;font-size:.75rem;color:#4b5563;font-weight:500}.cide-file-input-error{color:#d32f2f;font-size:.9rem}.cide-file-input-helper{color:#666;font-size:.9rem}.cide-file-input-preview{margin-top:.75rem;padding:.75rem;background-color:#f8f9fa;border:1px solid #e1e5e9;border-radius:.375rem}.cide-file-input-preview-label{font-weight:500;margin-bottom:.5rem;color:#374151;font-size:.875rem}.cide-file-input-preview-container{display:flex;flex-wrap:wrap;gap:.75rem}.cide-file-input-preview-item{position:relative;display:flex;flex-direction:column;border:1px solid #d1d5db;border-radius:.5rem;overflow:hidden;background-color:#fff;box-shadow:0 1px 3px #0000001a;transition:box-shadow .2s ease-in-out}.cide-file-input-preview-item:hover{box-shadow:0 4px 6px -1px #0000001a}.cide-file-input-preview-image{width:100%;height:calc(100% - 2rem);object-fit:cover;object-position:center;background-color:#f3f4f6}.cide-file-input-preview-filename{padding:.375rem .5rem;background-color:#f9fafbf2;border-top:1px solid #e5e7eb;font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:2rem;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-remove{position:absolute;top:.25rem;right:.25rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;transition:all .2s ease-in-out}.cide-file-input-preview-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-hidden{display:none}.cide-file-input-preview-box-container{display:flex;flex-direction:column;gap:.5rem}.cide-file-input-preview-box{position:relative;border:2px dashed #d1d5db;border-radius:.5rem;cursor:pointer;background-color:#f9fafb;display:flex;align-items:center;justify-content:center;overflow:hidden;transition:all .2s ease-in-out}.cide-file-input-preview-box:hover{border-color:#3b82f6;background-color:#eff6ff}.cide-file-input-preview-box.cide-file-input-preview-box-disabled{cursor:not-allowed;opacity:.6;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-disabled:hover{border-color:#d1d5db;background-color:#f3f4f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image{border-style:solid;border-color:#e5e7eb;padding:0}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover{border-color:#3b82f6}.cide-file-input-preview-box.cide-file-input-preview-box-has-image:hover .cide-file-input-preview-box-overlay{opacity:1}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over{border-color:#3b82f6!important;background-color:#eff6ff!important;transform:scale(1.02);box-shadow:0 0 0 4px #3b82f61a}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-icon{color:#3b82f6;transform:scale(1.1)}.cide-file-input-preview-box.cide-file-input-preview-box-drag-over .cide-file-input-preview-box-placeholder .cide-file-input-preview-box-text{color:#3b82f6;font-weight:600}.cide-file-input-preview-box-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;padding:1rem;text-align:center}.cide-file-input-preview-box-icon{font-size:2rem;color:#6b7280}.cide-file-input-preview-box-text{font-size:.875rem;color:#6b7280;font-weight:500}.cide-file-input-preview-box-content{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}.cide-file-input-preview-box-image{width:100%;height:100%;object-fit:cover;object-position:center}.cide-file-input-preview-box-overlay{position:absolute;inset:0;background-color:#0009;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .2s ease-in-out}.cide-file-input-preview-box-overlay-text{color:#fff;font-size:.875rem;font-weight:500;text-align:center}.cide-file-input-preview-box-remove{position:absolute;top:.375rem;right:.375rem;width:1.5rem;height:1.5rem;background-color:#ef4444e6;color:#fff;border:none;border-radius:50%;font-size:1rem;font-weight:700;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:20;transition:all .2s ease-in-out}.cide-file-input-preview-box-remove:hover{background-color:#dc2626f2;transform:scale(1.1)}.cide-file-input-preview-box-remove:focus{outline:2px solid #3b82f6;outline-offset:2px}.cide-file-input-preview-box-filename{font-size:.75rem;color:#374151;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:.25rem .5rem;background-color:#f3f4f6;border-radius:.25rem;margin-top:.25rem}@media (max-width: 640px){.cide-file-input-preview-container{justify-content:center}.cide-file-input-preview-item{min-width:120px;max-width:150px}.cide-file-input-preview-box-icon{font-size:1.5rem}.cide-file-input-preview-box-text{font-size:.75rem}}\n"] }]
4398
+ }], ctorParameters: () => [], propDecorators: { label: [{
4399
+ type: Input
4400
+ }], accept: [{
4492
4401
  type: Input
4493
4402
  }], multiple: [{
4494
4403
  type: Input
4495
- }], accept: [{
4404
+ }], disabled: [{
4496
4405
  type: Input
4497
- }], maxFileSize: [{
4406
+ }], required: [{
4498
4407
  type: Input
4499
- }], allowedTypes: [{
4408
+ }], helperText: [{
4500
4409
  type: Input
4501
- }], uploadComplete: [{
4410
+ }], errorText: [{
4411
+ type: Input
4412
+ }], showPreview: [{
4413
+ type: Input
4414
+ }], previewWidth: [{
4415
+ type: Input
4416
+ }], previewHeight: [{
4417
+ type: Input
4418
+ }], previewBoxMode: [{
4419
+ type: Input
4420
+ }], showFileName: [{
4421
+ type: Input
4422
+ }], placeholderText: [{
4423
+ type: Input
4424
+ }], placeholderIcon: [{
4425
+ type: Input
4426
+ }], autoUpload: [{
4427
+ type: Input
4428
+ }], uploadData: [{
4429
+ type: Input
4430
+ }], showFloatingUploader: [{
4431
+ type: Input
4432
+ }], floatingUploaderGroupId: [{
4433
+ type: Input
4434
+ }], fileChange: [{
4502
4435
  type: Output
4503
- }], uploadError: [{
4436
+ }], uploadSuccess: [{
4504
4437
  type: Output
4505
- }], uploadCancelled: [{
4438
+ }], uploadError: [{
4506
4439
  type: Output
4507
- }], allUploadsComplete: [{
4440
+ }], uploadProgressChange: [{
4508
4441
  type: Output
4509
4442
  }] } });
4510
4443
 
4511
- class CideEleFloatingFileUploaderComponent {
4444
+ class CideEleGlobalFileUploaderComponent {
4445
+ // Dependency injection
4512
4446
  destroyRef = inject(DestroyRef);
4513
- fileManagerService = inject(CideEleFileManagerService);
4514
- // Signals for reactive state
4515
- isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
4516
- isMinimized = signal(false, ...(ngDevMode ? [{ debugName: "isMinimized" }] : []));
4517
- currentUserId = signal('', ...(ngDevMode ? [{ debugName: "currentUserId" }] : []));
4518
- // Use file manager service's upload queue as the single source of truth
4519
- uploadQueue = computed(() => this.fileManagerService.uploadQueue(), ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4520
- activeUploads = computed(() => this.fileManagerService.activeUploads(), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4521
- // Computed values based on file manager service state
4447
+ fileService = inject(CideEleFileManagerService);
4448
+ // Input properties
4449
+ userId = '';
4450
+ multiple = true;
4451
+ accept = '';
4452
+ maxFileSize = 10; // MB
4453
+ allowedTypes = [];
4454
+ // Output events
4455
+ uploadComplete = new EventEmitter();
4456
+ uploadError = new EventEmitter();
4457
+ uploadCancelled = new EventEmitter();
4458
+ allUploadsComplete = new EventEmitter();
4459
+ // Signals for reactive state management
4460
+ isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : []));
4461
+ isUploading = signal(false, ...(ngDevMode ? [{ debugName: "isUploading" }] : []));
4462
+ uploadQueue = signal([], ...(ngDevMode ? [{ debugName: "uploadQueue" }] : []));
4463
+ currentGroupId = signal(null, ...(ngDevMode ? [{ debugName: "currentGroupId" }] : []));
4464
+ // Computed values
4522
4465
  hasUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "hasUploads" }] : []));
4523
- pendingUploads = computed(() => this.uploadQueue().length > 0, ...(ngDevMode ? [{ debugName: "pendingUploads" }] : [])); // All files in queue are pending
4524
- completedUploads = computed(() => {
4525
- const active = this.activeUploads();
4526
- return Array.from(active.values()).filter(upload => upload.stage === 'complete');
4527
- }, ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4528
- failedUploads = computed(() => {
4529
- const active = this.activeUploads();
4530
- return Array.from(active.values()).filter(upload => upload.stage === 'error');
4531
- }, ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4532
- // Animation states
4533
- isAnimating = signal(false, ...(ngDevMode ? [{ debugName: "isAnimating" }] : []));
4466
+ pendingUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'pending'), ...(ngDevMode ? [{ debugName: "pendingUploads" }] : []));
4467
+ activeUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'uploading'), ...(ngDevMode ? [{ debugName: "activeUploads" }] : []));
4468
+ completedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'completed'), ...(ngDevMode ? [{ debugName: "completedUploads" }] : []));
4469
+ failedUploads = computed(() => this.uploadQueue().filter(upload => upload.status === 'error'), ...(ngDevMode ? [{ debugName: "failedUploads" }] : []));
4534
4470
  constructor() {
4535
- console.log('🚀 [FloatingFileUploader] Component initialized');
4536
- // Set up effect to monitor file manager service's upload queue
4537
- effect(() => {
4538
- const uploadQueue = this.fileManagerService.uploadQueue();
4539
- const hasUploads = uploadQueue.length > 0;
4540
- console.log('🔄 [FloatingFileUploader] File manager service queue changed:', {
4541
- queueLength: uploadQueue.length,
4542
- hasUploads,
4543
- currentVisible: this.isVisible(),
4544
- queueFiles: uploadQueue
4545
- });
4546
- // Show floating uploader when files are added to the file manager service queue
4547
- if (hasUploads && !this.isVisible()) {
4548
- console.log('👁️ [FloatingFileUploader] Showing floating uploader due to file manager queue');
4549
- this.showWithAnimation();
4550
- }
4551
- else if (!hasUploads && this.isVisible()) {
4552
- console.log('👁️ [FloatingFileUploader] Hiding floating uploader - no files in file manager queue');
4553
- this.hideWithAnimation();
4554
- }
4555
- });
4471
+ console.log('🚀 [GlobalFileUploader] Component initialized');
4556
4472
  }
4557
- ngOnInit() {
4558
- // Set up drag and drop listeners
4559
- this.setupDragAndDrop();
4560
- // Set up file input change listeners
4561
- this.setupFileInputListeners();
4473
+ /**
4474
+ * Handle drag over event
4475
+ */
4476
+ onDragOver(event) {
4477
+ event.preventDefault();
4478
+ event.stopPropagation();
4479
+ this.isDragOver.set(true);
4562
4480
  }
4563
- ngOnDestroy() {
4564
- console.log('🧹 [FloatingFileUploader] Component destroyed');
4565
- this.removeDragAndDropListeners();
4566
- this.removeFileInputListeners();
4481
+ /**
4482
+ * Handle drag leave event
4483
+ */
4484
+ onDragLeave(event) {
4485
+ event.preventDefault();
4486
+ event.stopPropagation();
4487
+ this.isDragOver.set(false);
4567
4488
  }
4568
4489
  /**
4569
- * Set up drag and drop functionality
4490
+ * Handle drop event
4570
4491
  */
4571
- setupDragAndDrop() {
4572
- document.addEventListener('dragover', this.handleDragOver.bind(this));
4573
- document.addEventListener('dragleave', this.handleDragLeave.bind(this));
4574
- document.addEventListener('drop', this.handleDrop.bind(this));
4492
+ onDrop(event) {
4493
+ event.preventDefault();
4494
+ event.stopPropagation();
4495
+ this.isDragOver.set(false);
4496
+ const files = event.dataTransfer?.files;
4497
+ if (files && files.length > 0) {
4498
+ this.addFilesToQueue(Array.from(files));
4499
+ }
4500
+ }
4501
+ /**
4502
+ * Handle file input change
4503
+ */
4504
+ onFileInputChange(event) {
4505
+ const target = event.target;
4506
+ const files = target.files;
4507
+ if (files && files.length > 0) {
4508
+ this.addFilesToQueue(Array.from(files));
4509
+ }
4510
+ // Reset input value to allow selecting the same file again
4511
+ target.value = '';
4512
+ }
4513
+ /**
4514
+ * Add files to upload queue
4515
+ */
4516
+ addFilesToQueue(files) {
4517
+ // Validate files
4518
+ const validFiles = files.filter(file => this.validateFile(file));
4519
+ if (validFiles.length === 0) {
4520
+ console.warn('⚠️ [GlobalFileUploader] No valid files to upload');
4521
+ return;
4522
+ }
4523
+ const newUploads = validFiles.map(file => ({
4524
+ fileId: this.generateFileId(file),
4525
+ fileName: file.name,
4526
+ progress: 0,
4527
+ status: 'pending',
4528
+ file: file
4529
+ }));
4530
+ this.uploadQueue.update(queue => [...queue, ...newUploads]);
4531
+ this.processUploadQueue();
4575
4532
  }
4576
4533
  /**
4577
- * Remove drag and drop listeners
4534
+ * Validate file
4578
4535
  */
4579
- removeDragAndDropListeners() {
4580
- document.removeEventListener('dragover', this.handleDragOver.bind(this));
4581
- document.removeEventListener('dragleave', this.handleDragLeave.bind(this));
4582
- document.removeEventListener('drop', this.handleDrop.bind(this));
4536
+ validateFile(file) {
4537
+ // Check file size
4538
+ const maxSizeBytes = this.maxFileSize * 1024 * 1024;
4539
+ if (file.size > maxSizeBytes) {
4540
+ console.warn(`⚠️ [GlobalFileUploader] File ${file.name} exceeds size limit`);
4541
+ return false;
4542
+ }
4543
+ // Check file type
4544
+ if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(file.type)) {
4545
+ console.warn(`⚠️ [GlobalFileUploader] File type ${file.type} not allowed`);
4546
+ return false;
4547
+ }
4548
+ return true;
4583
4549
  }
4584
4550
  /**
4585
- * Set up file input change listeners
4551
+ * Process upload queue
4586
4552
  */
4587
- setupFileInputListeners() {
4588
- // Listen for file input change events globally
4589
- document.addEventListener('change', this.handleFileInputChange.bind(this));
4553
+ processUploadQueue() {
4554
+ const pendingUploads = this.uploadQueue().filter(upload => upload.status === 'pending');
4555
+ if (pendingUploads.length > 0 && !this.isUploading()) {
4556
+ this.isUploading.set(true);
4557
+ if (this.multiple && pendingUploads.length > 1) {
4558
+ // Multiple file upload with group ID
4559
+ this.uploadMultipleFiles(pendingUploads);
4560
+ }
4561
+ else {
4562
+ // Single file upload
4563
+ this.uploadNextFile();
4564
+ }
4565
+ }
4590
4566
  }
4591
4567
  /**
4592
- * Remove file input listeners
4568
+ * Upload multiple files with group ID
4593
4569
  */
4594
- removeFileInputListeners() {
4595
- document.removeEventListener('change', this.handleFileInputChange.bind(this));
4570
+ uploadMultipleFiles(uploads) {
4571
+ console.log('📤 [GlobalFileUploader] Starting multiple file upload for:', uploads.length, 'files');
4572
+ // Generate group ID first
4573
+ this.fileService.generateObjectId()
4574
+ .pipe(takeUntilDestroyed(this.destroyRef))
4575
+ .subscribe({
4576
+ next: (response) => {
4577
+ const groupId = response.data?.objectId;
4578
+ console.log('🆔 [GlobalFileUploader] Generated group ID:', groupId);
4579
+ this.currentGroupId.set(groupId || null);
4580
+ if (groupId) {
4581
+ // Upload all files with the same group ID
4582
+ this.uploadFilesWithGroupId(uploads, groupId);
4583
+ }
4584
+ else {
4585
+ console.error('❌ [GlobalFileUploader] No group ID received');
4586
+ this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
4587
+ }
4588
+ },
4589
+ error: (error) => {
4590
+ console.error('❌ [GlobalFileUploader] Failed to generate group ID:', error);
4591
+ this.handleUploadError('Failed to generate group ID', uploads[0]?.fileId);
4592
+ }
4593
+ });
4596
4594
  }
4597
4595
  /**
4598
- * Handle file input change events
4596
+ * Upload files with group ID
4599
4597
  */
4600
- handleFileInputChange(event) {
4601
- const target = event.target;
4602
- console.log('🔍 [FloatingFileUploader] File input change event detected:', {
4603
- type: target.type,
4604
- filesLength: target.files?.length || 0,
4605
- element: target
4598
+ uploadFilesWithGroupId(uploads, groupId) {
4599
+ let completedCount = 0;
4600
+ let failedCount = 0;
4601
+ const totalFiles = uploads.length;
4602
+ const uploadedFiles = [];
4603
+ uploads.forEach((upload, index) => {
4604
+ // Update status to uploading
4605
+ this.updateUploadStatus(upload.fileId, 'uploading', 0);
4606
+ const uploadRequest = {
4607
+ file: upload.file,
4608
+ userId: this.userId,
4609
+ groupId: groupId,
4610
+ tags: [],
4611
+ permissions: ['read', 'write']
4612
+ };
4613
+ this.fileService.uploadFile(upload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4614
+ // Use the original fileId for progress updates during upload
4615
+ this.updateUploadProgress(upload.fileId, progress);
4616
+ })
4617
+ .pipe(takeUntilDestroyed(this.destroyRef))
4618
+ .subscribe({
4619
+ next: (response) => {
4620
+ completedCount++;
4621
+ console.log(`✅ [GlobalFileUploader] File ${index + 1}/${totalFiles} uploaded successfully`);
4622
+ // Find the upload item by cyfm_temp_unique_id from response
4623
+ const responseFile = response?.data?.core_file_manager?.[0];
4624
+ const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4625
+ console.log('🔍 [GlobalFileUploader] Response tempUniqueId:', tempUniqueId);
4626
+ console.log('🔍 [GlobalFileUploader] Original upload fileId:', upload.fileId);
4627
+ // Use the tempUniqueId to find and update the correct upload item
4628
+ if (tempUniqueId) {
4629
+ this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4630
+ }
4631
+ else {
4632
+ // Fallback to original fileId
4633
+ this.updateUploadStatus(upload.fileId, 'completed', 100, undefined, response);
4634
+ }
4635
+ this.uploadComplete.emit({
4636
+ fileId: tempUniqueId || upload.fileId,
4637
+ fileName: upload.fileName,
4638
+ progress: 100,
4639
+ status: 'completed',
4640
+ uploadedFile: response
4641
+ });
4642
+ if (responseFile) {
4643
+ uploadedFiles.push(responseFile);
4644
+ }
4645
+ console.log('🔍 [GlobalFileUploader] Upload response received:', response);
4646
+ console.log('🔍 [GlobalFileUploader] Response data structure:', response?.data);
4647
+ // Check if all files are completed
4648
+ if (completedCount + failedCount === totalFiles) {
4649
+ this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
4650
+ }
4651
+ },
4652
+ error: (error) => {
4653
+ failedCount++;
4654
+ console.error(`❌ [GlobalFileUploader] File ${index + 1}/${totalFiles} upload failed:`, error);
4655
+ this.updateUploadStatus(upload.fileId, 'error', 0, error.message || 'Upload failed');
4656
+ this.uploadError.emit({
4657
+ fileId: upload.fileId,
4658
+ error: error.message || 'Upload failed'
4659
+ });
4660
+ // Check if all files are completed
4661
+ if (completedCount + failedCount === totalFiles) {
4662
+ this.handleMultipleUploadComplete(completedCount, failedCount, groupId, uploadedFiles);
4663
+ }
4664
+ }
4665
+ });
4606
4666
  });
4607
- // Check if this is a file input with files
4608
- if (target.type === 'file' && target.files && target.files.length > 0) {
4609
- console.log('📁 [FloatingFileUploader] File input change detected:', target.files.length, 'files');
4610
- // Check if the input has a data-user-id attribute for user context
4611
- const userId = target.getAttribute('data-user-id');
4612
- if (userId && userId !== this.currentUserId()) {
4613
- this.setCurrentUserId(userId);
4614
- }
4615
- // Handle the files
4616
- this.handleFiles(Array.from(target.files));
4617
- // Reset the input to allow selecting the same files again
4618
- target.value = '';
4619
- }
4620
4667
  }
4621
4668
  /**
4622
- * Handle drag over event
4669
+ * Handle multiple upload completion
4623
4670
  */
4624
- handleDragOver(event) {
4625
- event.preventDefault();
4626
- event.stopPropagation();
4627
- // Show floating uploader when files are dragged over
4628
- if (event.dataTransfer?.types.includes('Files')) {
4629
- this.showWithAnimation();
4671
+ handleMultipleUploadComplete(completed, failed, groupId, uploadedFiles) {
4672
+ console.log(`📊 [GlobalFileUploader] Multiple upload complete: ${completed}/${completed + failed} successful`);
4673
+ this.isUploading.set(false);
4674
+ if (completed > 0) {
4675
+ this.allUploadsComplete.emit({
4676
+ groupId: groupId,
4677
+ uploadedFiles: uploadedFiles
4678
+ });
4630
4679
  }
4631
4680
  }
4632
4681
  /**
4633
- * Handle drag leave event
4682
+ * Upload next file in queue (single file mode)
4634
4683
  */
4635
- handleDragLeave(event) {
4636
- event.preventDefault();
4637
- event.stopPropagation();
4638
- // Only hide if leaving the entire document
4639
- if (!event.relatedTarget || event.relatedTarget === document.body) {
4640
- this.updateVisibility();
4684
+ uploadNextFile() {
4685
+ const pendingUpload = this.uploadQueue().find(upload => upload.status === 'pending');
4686
+ if (!pendingUpload) {
4687
+ this.isUploading.set(false);
4688
+ return;
4641
4689
  }
4690
+ // Update status to uploading
4691
+ this.updateUploadStatus(pendingUpload.fileId, 'uploading', 0);
4692
+ const uploadRequest = {
4693
+ file: pendingUpload.file,
4694
+ userId: this.userId,
4695
+ tags: [],
4696
+ permissions: ['read', 'write']
4697
+ };
4698
+ this.fileService.uploadFile(pendingUpload.file, this.convertToFileUploadOptions(uploadRequest), (progress) => {
4699
+ this.updateUploadProgress(pendingUpload.fileId, progress);
4700
+ })
4701
+ .pipe(takeUntilDestroyed(this.destroyRef))
4702
+ .subscribe({
4703
+ next: (response) => {
4704
+ console.log('✅ [GlobalFileUploader] Single file upload successful:', response);
4705
+ // Find the upload item by cyfm_temp_unique_id from response
4706
+ const responseFile = response?.data?.core_file_manager?.[0];
4707
+ const tempUniqueId = responseFile?.cyfm_temp_unique_id;
4708
+ console.log('🔍 [GlobalFileUploader] Single upload - Response tempUniqueId:', tempUniqueId);
4709
+ console.log('🔍 [GlobalFileUploader] Single upload - Original upload fileId:', pendingUpload.fileId);
4710
+ // Use the tempUniqueId to find and update the correct upload item
4711
+ if (tempUniqueId) {
4712
+ this.updateUploadStatusByTempId(tempUniqueId, 'completed', 100, undefined, response);
4713
+ }
4714
+ else {
4715
+ // Fallback to original fileId
4716
+ this.updateUploadStatus(pendingUpload.fileId, 'completed', 100, undefined, response);
4717
+ }
4718
+ this.uploadComplete.emit({
4719
+ fileId: tempUniqueId || pendingUpload.fileId,
4720
+ fileName: pendingUpload.fileName,
4721
+ progress: 100,
4722
+ status: 'completed',
4723
+ uploadedFile: response
4724
+ });
4725
+ // Process next file
4726
+ setTimeout(() => this.uploadNextFile(), 500);
4727
+ },
4728
+ error: (error) => {
4729
+ console.error('❌ [GlobalFileUploader] Upload error:', error);
4730
+ this.updateUploadStatus(pendingUpload.fileId, 'error', 0, error.message || 'Upload failed');
4731
+ this.uploadError.emit({
4732
+ fileId: pendingUpload.fileId,
4733
+ error: error.message || 'Upload failed'
4734
+ });
4735
+ // Process next file
4736
+ setTimeout(() => this.uploadNextFile(), 500);
4737
+ }
4738
+ });
4642
4739
  }
4643
4740
  /**
4644
- * Handle drop event
4741
+ * Update upload progress
4645
4742
  */
4646
- handleDrop(event) {
4647
- event.preventDefault();
4648
- event.stopPropagation();
4649
- const files = event.dataTransfer?.files;
4650
- if (files && files.length > 0) {
4651
- this.handleFiles(Array.from(files));
4652
- }
4743
+ updateUploadProgress(fileId, progress) {
4744
+ this.uploadQueue.update(queue => queue.map(upload => upload.fileId === fileId
4745
+ ? { ...upload, progress }
4746
+ : upload));
4653
4747
  }
4654
4748
  /**
4655
- * Handle files from drag and drop or file input
4749
+ * Update upload status
4656
4750
  */
4657
- handleFiles(files) {
4658
- console.log('📁 [FloatingFileUploader] Handling files:', files.length);
4659
- // Use handleExternalFiles to process the files
4660
- this.handleExternalFiles(files, this.currentUserId());
4751
+ updateUploadStatus(fileId, status, progress, error, uploadedFile) {
4752
+ console.log('🔄 [GlobalFileUploader] Updating upload status:', { fileId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4753
+ this.uploadQueue.update(queue => {
4754
+ const updatedQueue = queue.map(upload => {
4755
+ if (upload.fileId === fileId) {
4756
+ console.log('🔄 [GlobalFileUploader] Found upload to update:', upload.fileName, 'from', upload.status, 'to', status);
4757
+ return { ...upload, status, progress, error, uploadedFile };
4758
+ }
4759
+ return upload;
4760
+ });
4761
+ console.log('🔄 [GlobalFileUploader] Updated queue:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status })));
4762
+ return updatedQueue;
4763
+ });
4661
4764
  }
4662
4765
  /**
4663
- * Update visibility - simplified for notification only
4766
+ * Update upload status by cyfm_temp_unique_id
4664
4767
  */
4665
- updateVisibility() {
4666
- // This is just a notification component now
4667
- // The actual uploads are handled by the global uploader
4768
+ updateUploadStatusByTempId(tempUniqueId, status, progress, error, uploadedFile) {
4769
+ console.log('🔄 [GlobalFileUploader] Updating upload status by tempId:', { tempUniqueId, status, progress, error: !!error, uploadedFile: !!uploadedFile });
4770
+ this.uploadQueue.update(queue => {
4771
+ const updatedQueue = queue.map(upload => {
4772
+ // Check if this upload's fileId matches the tempUniqueId
4773
+ if (upload.fileId === tempUniqueId) {
4774
+ console.log('🔄 [GlobalFileUploader] Found upload by tempId to update:', upload.fileName, 'from', upload.status, 'to', status);
4775
+ return { ...upload, status, progress, error, uploadedFile };
4776
+ }
4777
+ return upload;
4778
+ });
4779
+ console.log('🔄 [GlobalFileUploader] Updated queue by tempId:', updatedQueue.map(u => ({ fileName: u.fileName, status: u.status, fileId: u.fileId })));
4780
+ return updatedQueue;
4781
+ });
4668
4782
  }
4669
4783
  /**
4670
- * Show with animation
4784
+ * Handle upload error
4671
4785
  */
4672
- showWithAnimation() {
4673
- console.log('🎬 [FloatingFileUploader] showWithAnimation called - setting isVisible to true');
4674
- this.isAnimating.set(true);
4675
- this.isVisible.set(true);
4676
- // Remove animation class after animation completes
4677
- setTimeout(() => {
4678
- this.isAnimating.set(false);
4679
- console.log('🎬 [FloatingFileUploader] Animation completed, isVisible:', this.isVisible());
4680
- }, 300);
4786
+ handleUploadError(error, fileId) {
4787
+ if (fileId) {
4788
+ this.updateUploadStatus(fileId, 'error', 0, error);
4789
+ this.uploadError.emit({ fileId, error });
4790
+ }
4791
+ this.isUploading.set(false);
4681
4792
  }
4682
4793
  /**
4683
- * Hide with animation
4794
+ * Cancel upload
4684
4795
  */
4685
- hideWithAnimation() {
4686
- this.isAnimating.set(true);
4687
- // Wait for animation to complete before hiding
4796
+ cancelUpload(fileId) {
4797
+ this.updateUploadStatus(fileId, 'cancelled', 0);
4798
+ this.uploadCancelled.emit(fileId);
4799
+ // Remove from queue after a delay
4688
4800
  setTimeout(() => {
4689
- this.isVisible.set(false);
4690
- this.isAnimating.set(false);
4691
- }, 300);
4801
+ this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4802
+ }, 1000);
4692
4803
  }
4693
4804
  /**
4694
- * Toggle minimize state
4805
+ * Retry upload
4695
4806
  */
4696
- toggleMinimize() {
4697
- this.isMinimized.set(!this.isMinimized());
4807
+ retryUpload(fileId) {
4808
+ this.updateUploadStatus(fileId, 'pending', 0);
4809
+ this.processUploadQueue();
4698
4810
  }
4699
4811
  /**
4700
- * Close the floating uploader
4812
+ * Remove upload from queue
4701
4813
  */
4702
- close() {
4703
- this.hideWithAnimation();
4814
+ removeUpload(fileId) {
4815
+ this.uploadQueue.update(queue => queue.filter(upload => upload.fileId !== fileId));
4704
4816
  }
4705
4817
  /**
4706
- * Get upload summary text
4818
+ * Clear completed uploads
4707
4819
  */
4708
- getUploadSummary() {
4709
- const queueLength = this.uploadQueue().length;
4710
- const activeCount = this.activeUploads().size;
4711
- const completed = this.completedUploads().length;
4712
- const failed = this.failedUploads().length;
4713
- if (activeCount > 0) {
4714
- return `${activeCount} uploading`;
4715
- }
4716
- else if (completed > 0 && failed === 0) {
4717
- return `${completed} completed`;
4718
- }
4719
- else if (failed > 0) {
4720
- return `${completed} completed, ${failed} failed`;
4721
- }
4722
- else if (queueLength > 0) {
4723
- return `${queueLength} pending`;
4724
- }
4725
- return 'No uploads';
4820
+ clearCompleted() {
4821
+ this.uploadQueue.update(queue => queue.filter(upload => upload.status !== 'completed'));
4726
4822
  }
4727
4823
  /**
4728
- * Get overall progress percentage
4824
+ * Clear all uploads
4729
4825
  */
4730
- getOverallProgress() {
4731
- const activeUploads = this.activeUploads();
4732
- if (activeUploads.size === 0)
4733
- return 0;
4734
- const totalProgress = Array.from(activeUploads.values()).reduce((sum, upload) => sum + upload.percentage, 0);
4735
- return Math.round(totalProgress / activeUploads.size);
4826
+ clearAll() {
4827
+ this.uploadQueue.set([]);
4828
+ this.currentGroupId.set(null);
4736
4829
  }
4737
4830
  /**
4738
- * Get status icon based on upload stage
4831
+ * Get status icon
4739
4832
  */
4740
- getStatusIcon(stage) {
4741
- switch (stage) {
4742
- case 'reading': return 'schedule';
4833
+ getStatusIcon(status) {
4834
+ switch (status) {
4835
+ case 'pending': return 'schedule';
4743
4836
  case 'uploading': return 'cloud_upload';
4744
- case 'complete': return 'check_circle';
4837
+ case 'completed': return 'check_circle';
4745
4838
  case 'error': return 'error';
4839
+ case 'cancelled': return 'cancel';
4746
4840
  default: return 'help';
4747
4841
  }
4748
4842
  }
4749
4843
  /**
4750
- * Get status class based on upload stage
4844
+ * Get status class
4751
4845
  */
4752
- getStatusClass(stage) {
4753
- switch (stage) {
4754
- case 'reading': return 'status-pending';
4846
+ getStatusClass(status) {
4847
+ switch (status) {
4848
+ case 'pending': return 'status-pending';
4755
4849
  case 'uploading': return 'status-uploading';
4756
- case 'complete': return 'status-completed';
4850
+ case 'completed': return 'status-completed';
4757
4851
  case 'error': return 'status-error';
4852
+ case 'cancelled': return 'status-cancelled';
4758
4853
  default: return 'status-unknown';
4759
4854
  }
4760
4855
  }
4761
4856
  /**
4762
- * Cancel upload
4857
+ * Get file size display
4763
4858
  */
4764
- cancelUpload(fileId) {
4765
- console.log('🚫 [FloatingFileUploader] Cancelling upload:', fileId);
4766
- this.fileManagerService.cancelUpload(fileId);
4859
+ getFileSizeDisplay(file) {
4860
+ const bytes = file.size;
4861
+ if (bytes === 0)
4862
+ return '0 Bytes';
4863
+ const k = 1024;
4864
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
4865
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4866
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
4767
4867
  }
4768
4868
  /**
4769
- * Get file name from file ID (extract from the ID format)
4869
+ * Generate unique file ID - this should match the service's generateFileId method
4870
+ * The service uses: ${file.name}_${file.size}_${Date.now()}
4770
4871
  */
4771
- getFileNameFromId(fileId) {
4772
- // Extract filename from the fileId format: filename_size_timestamp
4773
- const parts = fileId.split('_');
4774
- if (parts.length >= 3) {
4775
- // Remove the last two parts (size and timestamp) to get the filename
4776
- return parts.slice(0, -2).join('_');
4777
- }
4778
- return fileId;
4872
+ generateFileId(file) {
4873
+ return `${file.name}_${file.size}_${Date.now()}`;
4779
4874
  }
4780
4875
  /**
4781
- * Set current user ID
4876
+ * Convert IFileUploadRequest to FileUploadOptions
4782
4877
  */
4783
- setCurrentUserId(userId) {
4784
- this.currentUserId.set(userId);
4785
- this.fileManagerService.setUserId(userId);
4878
+ convertToFileUploadOptions(request) {
4879
+ return {
4880
+ altText: request.altText,
4881
+ userId: request.userId,
4882
+ permissions: request.permissions,
4883
+ tags: request.tags,
4884
+ groupId: request.groupId
4885
+ };
4786
4886
  }
4787
4887
  /**
4788
- * Public method to handle files from external sources
4789
- * This can be called by other components to trigger the floating uploader
4888
+ * Trigger file input click
4790
4889
  */
4791
- handleExternalFiles(files, userId) {
4792
- console.log('📁 [FloatingFileUploader] External files received:', files.length, 'files');
4793
- // Set user ID if provided
4794
- if (userId && userId !== this.currentUserId()) {
4795
- this.setCurrentUserId(userId);
4890
+ triggerFileInput() {
4891
+ const fileInput = document.getElementById('global-file-input');
4892
+ if (fileInput) {
4893
+ fileInput.click();
4796
4894
  }
4797
- // Upload files using file manager service
4798
- // The file manager service will handle adding to its queue and the effect will show the floating uploader
4799
- files.forEach((file, index) => {
4800
- console.log(`📁 [FloatingFileUploader] Starting upload for file ${index + 1}/${files.length}:`, file.name);
4801
- this.fileManagerService.uploadFile(file, {
4802
- userId: this.currentUserId(),
4803
- permissions: ['read', 'write'],
4804
- tags: []
4805
- })
4806
- .pipe(takeUntilDestroyed(this.destroyRef))
4807
- .subscribe({
4808
- next: (response) => {
4809
- console.log('✅ [FloatingFileUploader] Upload completed:', response);
4810
- },
4811
- error: (error) => {
4812
- console.error('❌ [FloatingFileUploader] Upload failed:', error);
4813
- }
4814
- });
4815
- });
4816
4895
  }
4817
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4818
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleFloatingFileUploaderComponent, isStandalone: true, selector: "cide-ele-floating-file-uploader", ngImport: i0, template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
4896
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4897
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.7", type: CideEleGlobalFileUploaderComponent, isStandalone: true, selector: "cide-ele-global-file-uploader", inputs: { userId: "userId", multiple: "multiple", accept: "accept", maxFileSize: "maxFileSize", allowedTypes: "allowedTypes" }, outputs: { uploadComplete: "uploadComplete", uploadError: "uploadError", uploadCancelled: "uploadCancelled", allUploadsComplete: "allUploadsComplete" }, ngImport: i0, template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CideEleButtonComponent, selector: "button[cideEleButton], a[cideEleButton]", inputs: ["label", "variant", "size", "type", "shape", "elevation", "disabled", "id", "loading", "fullWidth", "leftIcon", "rightIcon", "customClass", "tooltip", "ariaLabel", "testId", "routerLink", "routerExtras", "preventDoubleClick", "animated"], outputs: ["btnClick", "doubleClick"] }, { kind: "component", type: CideIconComponent, selector: "cide-ele-icon", inputs: ["size", "type", "toolTip"] }] });
4819
4898
  }
4820
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleFloatingFileUploaderComponent, decorators: [{
4899
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideEleGlobalFileUploaderComponent, decorators: [{
4821
4900
  type: Component,
4822
- args: [{ selector: 'cide-ele-floating-file-uploader', standalone: true, imports: [
4901
+ args: [{ selector: 'cide-ele-global-file-uploader', standalone: true, imports: [
4823
4902
  CommonModule,
4903
+ CideEleButtonComponent,
4824
4904
  CideIconComponent
4825
- ], template: "<!-- Floating File Uploader Container -->\r\n@if (isVisible()) {\r\n<div class=\"floating-uploader\" \r\n [class.minimized]=\"isMinimized()\" \r\n [class.animating]=\"isAnimating()\">\r\n\r\n <!-- Header -->\r\n <div class=\"uploader-header\">\r\n <div class=\"header-left\">\r\n <div class=\"upload-icon\">\r\n <cide-ele-icon size=\"sm\">cloud_upload</cide-ele-icon>\r\n </div>\r\n <div class=\"upload-info\">\r\n <div class=\"upload-title\">File Upload</div>\r\n <div class=\"upload-summary\">{{ getUploadSummary() }}</div>\r\n </div>\r\n </div>\r\n \r\n <div class=\"header-actions\">\r\n <button class=\"action-btn minimize-btn\" (click)=\"toggleMinimize()\" [title]=\"isMinimized() ? 'Expand' : 'Minimize'\">\r\n <cide-ele-icon size=\"xs\">{{ isMinimized() ? 'expand_more' : 'expand_less' }}</cide-ele-icon>\r\n </button>\r\n <button class=\"action-btn close-btn\" (click)=\"close()\" title=\"Close\">\r\n <cide-ele-icon size=\"xs\">close</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Content (hidden when minimized) -->\r\n @if (!isMinimized()) {\r\n <div class=\"uploader-content\">\r\n \r\n <!-- Overall Progress Bar -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"overall-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"getOverallProgress()\"></div>\r\n </div>\r\n <div class=\"progress-text\">{{ getOverallProgress() }}%</div>\r\n </div>\r\n }\r\n\r\n <!-- Upload Queue - Show files from file manager service -->\r\n @if (uploadQueue().length > 0 || activeUploads().size > 0) {\r\n <div class=\"upload-queue\">\r\n <!-- Show files in queue (pending) -->\r\n @for (fileId of uploadQueue(); track fileId) {\r\n <div class=\"upload-item status-pending\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">schedule</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(fileId) }}</div>\r\n <div class=\"file-status\">\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"upload-actions\">\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(fileId)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Show active uploads -->\r\n @for (entry of activeUploads() | keyvalue; track entry.key) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(entry.value.stage)\">\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"xs\">{{ getStatusIcon(entry.value.stage) }}</cide-ele-icon>\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ getFileNameFromId(entry.key) }}</div>\r\n <div class=\"file-status\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <span class=\"text-yellow-600\">Reading...</span>\r\n }\r\n @case ('uploading') {\r\n <span class=\"text-blue-600\">Uploading...</span>\r\n }\r\n @case ('complete') {\r\n <span class=\"text-green-600\">Completed</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">Failed</span>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar (for uploading files) -->\r\n @if (entry.value.stage === 'uploading') {\r\n <div class=\"file-progress\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"entry.value.percentage\"></div>\r\n </div>\r\n <span class=\"progress-text\">{{ entry.value.percentage }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (entry.value.stage) {\r\n @case ('reading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button class=\"action-btn cancel-btn\" (click)=\"cancelUpload(entry.key)\" title=\"Cancel\">\r\n <cide-ele-icon size=\"xs\">cancel</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('complete') {\r\n <button class=\"action-btn success-btn\" title=\"Completed\">\r\n <cide-ele-icon size=\"xs\">check_circle</cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button class=\"action-btn retry-btn\" title=\"Retry\">\r\n <cide-ele-icon size=\"xs\">refresh</cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n</div>\r\n}\r\n", styles: [".floating-uploader{position:fixed;bottom:20px;right:20px;width:320px;max-height:500px;background:#fff;border-radius:12px;box-shadow:0 8px 32px #0000001f;border:1px solid rgba(0,0,0,.08);z-index:1000;overflow:hidden;transition:all .3s cubic-bezier(.4,0,.2,1);transform:translateY(0);opacity:1}.floating-uploader.animating{transition:all .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.minimized .uploader-content{display:none}.floating-uploader.minimized .uploader-footer{border-top:none}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f626}.floating-uploader .uploader-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#f8fafc;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-header .header-left{display:flex;align-items:center;gap:8px}.floating-uploader .uploader-header .header-left .upload-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px;background:#3b82f6;border-radius:6px;color:#fff}.floating-uploader .uploader-header .header-left .upload-info .upload-title{font-size:14px;font-weight:600;color:#1e293b;margin:0}.floating-uploader .uploader-header .header-left .upload-info .upload-summary{font-size:12px;color:#64748b;margin:0}.floating-uploader .uploader-header .header-actions{display:flex;gap:4px}.floating-uploader .uploader-header .header-actions .action-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:background-color .2s;color:#64748b}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#e2e8f0;color:#1e293b}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content{max-height:400px;overflow-y:auto}.floating-uploader .uploader-content .overall-progress{padding:12px 16px;border-bottom:1px solid #e2e8f0}.floating-uploader .uploader-content .overall-progress .progress-bar{width:100%;height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden;margin-bottom:4px}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .overall-progress .progress-text{font-size:12px;color:#64748b;text-align:center;display:block}.floating-uploader .uploader-content .upload-queue .upload-item{display:flex;align-items:center;padding:8px 16px;border-bottom:1px solid #f1f5f9;transition:background-color .2s}.floating-uploader .uploader-content .upload-queue .upload-item:last-child{border-bottom:none}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#f0f9ff}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#f0fdf4}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#fef2f2}.floating-uploader .uploader-content .upload-queue .upload-item .file-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .status-icon{flex-shrink:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details{min-width:0;flex:1}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{font-size:13px;font-weight:500;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status{font-size:11px;margin:0}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-status span{font-weight:500}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress{display:flex;align-items:center;gap:8px;margin:0 8px;min-width:80px}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{flex:1;height:3px;background:#e2e8f0;border-radius:2px;overflow:hidden}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{height:100%;background:#3b82f6;transition:width .3s ease}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text{font-size:10px;color:#64748b;min-width:24px;text-align:right}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions{display:flex;gap:4px}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:none;background:transparent;border-radius:4px;cursor:pointer;transition:all .2s;color:#64748b}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#e2e8f0}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#fef2f2;color:#dc2626}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#f0f9ff;color:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#16a34a}.floating-uploader .uploader-content .hidden-uploader{display:none}.floating-uploader .uploader-footer{padding:8px 16px;background:#f8fafc;border-top:1px solid #e2e8f0}.floating-uploader .uploader-footer .footer-stats{display:flex;gap:12px;font-size:11px}.floating-uploader .uploader-footer .footer-stats .stat{display:flex;align-items:center;gap:4px;color:#64748b}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#3b82f6}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#16a34a}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#dc2626}@media (max-width: 640px){.floating-uploader{bottom:10px;right:10px;left:10px;width:auto;max-width:none}}@media (prefers-color-scheme: dark){.floating-uploader{background:#1e293b;border-color:#334155;box-shadow:0 8px 32px #0000004d}.floating-uploader.uploading{border-color:#3b82f6;box-shadow:0 8px 32px #3b82f633}.floating-uploader .uploader-header{background:#334155;border-bottom-color:#475569}.floating-uploader .uploader-header .header-left .upload-icon{background:#3b82f6}.floating-uploader .uploader-header .header-left .upload-info .upload-title{color:#f1f5f9}.floating-uploader .uploader-header .header-left .upload-info .upload-summary,.floating-uploader .uploader-header .header-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-header .header-actions .action-btn:hover{background:#475569;color:#f1f5f9}.floating-uploader .uploader-header .header-actions .action-btn.close-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .overall-progress{border-bottom-color:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .overall-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .overall-progress .progress-text{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item{border-bottom-color:#334155}.floating-uploader .uploader-content .upload-queue .upload-item.status-uploading{background:#1e3a8a}.floating-uploader .uploader-content .upload-queue .upload-item.status-completed{background:#14532d}.floating-uploader .uploader-content .upload-queue .upload-item.status-error{background:#7f1d1d}.floating-uploader .uploader-content .upload-queue .upload-item .file-info .file-details .file-name{color:#f1f5f9}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-bar .progress-fill{background:#3b82f6}.floating-uploader .uploader-content .upload-queue .upload-item .file-progress .progress-text,.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn{color:#94a3b8}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn:hover{background:#475569}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.cancel-btn:hover{background:#7f1d1d;color:#fca5a5}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.retry-btn:hover{background:#1e3a8a;color:#60a5fa}.floating-uploader .uploader-content .upload-queue .upload-item .upload-actions .action-btn.success-btn{color:#4ade80}.floating-uploader .uploader-footer{background:#334155;border-top-color:#475569}.floating-uploader .uploader-footer .footer-stats .stat{color:#94a3b8}.floating-uploader .uploader-footer .footer-stats .stat.uploading{color:#60a5fa}.floating-uploader .uploader-footer .footer-stats .stat.completed{color:#4ade80}.floating-uploader .uploader-footer .footer-stats .stat.failed{color:#fca5a5}}@keyframes slideInUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}@keyframes slideOutDown{0%{transform:translateY(0);opacity:1}to{transform:translateY(100%);opacity:0}}.floating-uploader.animating{animation:slideInUp .3s cubic-bezier(.4,0,.2,1)}.floating-uploader.animating.hiding{animation:slideOutDown .3s cubic-bezier(.4,0,.2,1)}\n"] }]
4826
- }], ctorParameters: () => [] });
4905
+ ], template: "<!-- Global File Uploader Component -->\r\n<div class=\"global-file-uploader\">\r\n\r\n <!-- Hidden file input -->\r\n <input id=\"global-file-input\" type=\"file\" [multiple]=\"multiple\" [accept]=\"accept\" \r\n (change)=\"onFileInputChange($event)\" style=\"display: none;\">\r\n\r\n <!-- Drag and Drop Zone -->\r\n <div class=\"upload-zone\" [class.drag-over]=\"isDragOver()\" (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\" (drop)=\"onDrop($event)\" (click)=\"triggerFileInput()\">\r\n\r\n <div class=\"upload-zone-content\">\r\n <cide-ele-icon name=\"cloud_upload\" class=\"upload-icon\" size=\"lg\">\r\n </cide-ele-icon>\r\n\r\n <h3 class=\"upload-title\">\r\n {{ isDragOver() ? 'Drop files here' : 'Upload Files' }}\r\n </h3>\r\n\r\n <p class=\"upload-subtitle\">\r\n {{ isDragOver() ? 'Release to upload' : 'Drag and drop files here or click to browse' }}\r\n </p>\r\n\r\n <button cideEleButton cideEleButton variant=\"outline\" size=\"sm\"\r\n (click)=\"triggerFileInput(); $event.stopPropagation()\">\r\n <cide-ele-icon name=\"add\" size=\"sm\"></cide-ele-icon>\r\n Choose Files\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Upload Queue -->\r\n @if (hasUploads()) {\r\n <div class=\"upload-queue\">\r\n <div class=\"queue-header\">\r\n <h4 class=\"queue-title\">Upload Queue</h4>\r\n <div class=\"queue-actions\">\r\n @if (completedUploads().length > 0) {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearCompleted()\">\r\n Clear Completed\r\n </button>\r\n }\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"clearAll()\">\r\n Clear All\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"queue-items\">\r\n @for (upload of uploadQueue(); track upload.fileId) {\r\n <div class=\"upload-item\" [class]=\"getStatusClass(upload.status)\">\r\n\r\n <!-- File Info -->\r\n <div class=\"file-info\">\r\n <cide-ele-icon class=\"status-icon\" size=\"sm\">\r\n {{getStatusIcon(upload.status)}}\r\n </cide-ele-icon>\r\n\r\n <div class=\"file-details\">\r\n <div class=\"file-name\">{{ upload.fileName }}</div>\r\n @if (upload.file) {\r\n <div class=\"file-size\">{{ getFileSizeDisplay(upload.file) }}</div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (upload.status === 'uploading') {\r\n <div class=\"progress-container\">\r\n <div class=\"progress-bar\">\r\n <div class=\"progress-fill\" [style.width.%]=\"upload.progress\">\r\n </div>\r\n </div>\r\n <span class=\"progress-text\">{{ upload.progress }}%</span>\r\n </div>\r\n }\r\n\r\n <!-- Status Text -->\r\n @if (upload.status !== 'uploading') {\r\n <div class=\"status-text\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <span class=\"text-yellow-600\">Waiting...</span>\r\n }\r\n @case ('completed') {\r\n <span class=\"text-green-600\">Uploaded successfully</span>\r\n }\r\n @case ('error') {\r\n <span class=\"text-red-600\">{{ upload.error || 'Upload failed' }}</span>\r\n }\r\n @case ('cancelled') {\r\n <span class=\"text-gray-600\">Cancelled</span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"upload-actions\">\r\n @switch (upload.status) {\r\n @case ('pending') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('uploading') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"cancelUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"cancel\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('completed') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('error') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"retryUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"refresh\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n @case ('cancelled') {\r\n <button cideEleButton variant=\"text\" size=\"sm\" (click)=\"removeUpload(upload.fileId)\">\r\n <cide-ele-icon name=\"close\" size=\"xs\"></cide-ele-icon>\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Queue Summary -->\r\n <div class=\"queue-summary\">\r\n <div class=\"summary-stats\">\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"schedule\" size=\"xs\"></cide-ele-icon>\r\n {{ pendingUploads().length }} pending\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"cloud_upload\" size=\"xs\"></cide-ele-icon>\r\n {{ activeUploads().length }} uploading\r\n </span>\r\n <span class=\"stat\">\r\n <cide-ele-icon name=\"check_circle\" size=\"xs\"></cide-ele-icon>\r\n {{ completedUploads().length }} completed\r\n </span>\r\n @if (failedUploads().length > 0) {\r\n <span class=\"stat error\">\r\n <cide-ele-icon name=\"error\" size=\"xs\"></cide-ele-icon>\r\n {{ failedUploads().length }} failed\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n\r\n", styles: [".global-file-uploader{@apply tw-w-full tw-max-w-2xl tw-mx-auto tw-p-4;}.global-file-uploader .upload-zone{@apply tw-border-2 tw-border-dashed tw-border-gray-300 tw-rounded-lg tw-p-8 tw-text-center tw-cursor-pointer tw-transition-all tw-duration-200 tw-ease-in-out;@apply hover:tw-border-blue-400 hover:tw-bg-blue-50;}.global-file-uploader .upload-zone.drag-over{@apply tw-border-blue-500 tw-bg-blue-100 tw-scale-105;}.global-file-uploader .upload-zone .upload-zone-content{@apply tw-space-y-4;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-400 tw-mx-auto;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-xl tw-font-semibold tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-sm tw-text-gray-500 tw-m-0;}.global-file-uploader .upload-queue{@apply tw-mt-6 tw-bg-white tw-border tw-border-gray-200 tw-rounded-lg tw-shadow-sm;}.global-file-uploader .upload-queue .queue-header{@apply tw-flex tw-justify-between tw-items-center tw-p-4 tw-border-b tw-border-gray-200;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-lg tw-font-medium tw-text-gray-700 tw-m-0;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-flex tw-space-x-2;}.global-file-uploader .upload-queue .queue-items{@apply tw-max-h-96 tw-overflow-y-auto;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-flex tw-items-center tw-justify-between tw-p-4 tw-border-b tw-border-gray-100 tw-transition-colors tw-duration-200;}.global-file-uploader .upload-queue .queue-items .upload-item:last-child{@apply tw-border-b-0;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-50;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-50;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-flex tw-items-center tw-flex-1 tw-min-w-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .status-icon{@apply tw-mr-3 tw-flex-shrink-0;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details{@apply tw-min-w-0 tw-flex-1;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-sm tw-font-medium tw-text-gray-700 tw-truncate;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-xs tw-text-gray-500 tw-mt-1;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-flex tw-items-center tw-space-x-3 tw-flex-1 tw-max-w-xs tw-mx-4;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-flex-1 tw-h-2 tw-bg-gray-200 tw-rounded-full tw-overflow-hidden;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar .progress-fill{@apply tw-h-full tw-bg-blue-500 tw-transition-all tw-duration-300 tw-ease-out;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-xs tw-font-medium tw-text-gray-600 tw-min-w-12 tw-text-right;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-flex-1 tw-text-sm tw-text-center;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text span{@apply tw-font-medium;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-flex tw-space-x-1 tw-ml-4;}.global-file-uploader .upload-queue .queue-summary{@apply tw-p-4 tw-bg-gray-50 tw-border-t tw-border-gray-200;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-flex tw-flex-wrap tw-gap-4 tw-text-sm tw-text-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat{@apply tw-flex tw-items-center tw-space-x-1;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-600;}@media (max-width: 640px){.global-file-uploader{@apply tw-p-2;}.global-file-uploader .upload-zone{@apply tw-p-6;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-lg;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-xs;}.global-file-uploader .upload-queue .queue-header{@apply tw-p-3 tw-flex-col tw-space-y-2 tw-items-start;}.global-file-uploader .upload-queue .queue-header .queue-actions{@apply tw-w-full tw-justify-end;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-p-3 tw-flex-col tw-space-y-3 tw-items-start;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info{@apply tw-w-full;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container{@apply tw-w-full tw-mx-0;}.global-file-uploader .upload-queue .queue-items .upload-item .status-text{@apply tw-w-full tw-text-left;}.global-file-uploader .upload-queue .queue-items .upload-item .upload-actions{@apply tw-w-full tw-justify-end tw-ml-0;}}@media (prefers-color-scheme: dark){.global-file-uploader .upload-zone{@apply tw-border-gray-600 hover:tw-border-blue-400 hover:tw-bg-gray-800;}.global-file-uploader .upload-zone.drag-over{@apply tw-bg-blue-900;}.global-file-uploader .upload-zone .upload-zone-content .upload-icon{@apply tw-text-gray-500;}.global-file-uploader .upload-zone .upload-zone-content .upload-title{@apply tw-text-gray-200;}.global-file-uploader .upload-zone .upload-zone-content .upload-subtitle{@apply tw-text-gray-400;}.global-file-uploader .upload-queue{@apply tw-bg-gray-800 tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-header .queue-title{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item{@apply tw-border-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item.status-pending{@apply tw-bg-yellow-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-uploading{@apply tw-bg-blue-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-completed{@apply tw-bg-green-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-error{@apply tw-bg-red-900;}.global-file-uploader .upload-queue .queue-items .upload-item.status-cancelled{@apply tw-bg-gray-700;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-name{@apply tw-text-gray-200;}.global-file-uploader .upload-queue .queue-items .upload-item .file-info .file-details .file-size{@apply tw-text-gray-400;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-bar{@apply tw-bg-gray-600;}.global-file-uploader .upload-queue .queue-items .upload-item .progress-container .progress-text{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary{@apply tw-bg-gray-700 tw-border-gray-600;}.global-file-uploader .upload-queue .queue-summary .summary-stats{@apply tw-text-gray-300;}.global-file-uploader .upload-queue .queue-summary .summary-stats .stat.error{@apply tw-text-red-400;}}\n"] }]
4906
+ }], ctorParameters: () => [], propDecorators: { userId: [{
4907
+ type: Input
4908
+ }], multiple: [{
4909
+ type: Input
4910
+ }], accept: [{
4911
+ type: Input
4912
+ }], maxFileSize: [{
4913
+ type: Input
4914
+ }], allowedTypes: [{
4915
+ type: Input
4916
+ }], uploadComplete: [{
4917
+ type: Output
4918
+ }], uploadError: [{
4919
+ type: Output
4920
+ }], uploadCancelled: [{
4921
+ type: Output
4922
+ }], allUploadsComplete: [{
4923
+ type: Output
4924
+ }] } });
4925
+
4926
+ class CideFloatingUploadTriggerDirective {
4927
+ elementRef = inject(ElementRef);
4928
+ floatingUploader = inject(CideEleFloatingFileUploaderComponent);
4929
+ groupId;
4930
+ userId;
4931
+ showIcon = true;
4932
+ filesSelected = new EventEmitter();
4933
+ triggerIcon;
4934
+ ngOnInit() {
4935
+ this.setupTrigger();
4936
+ }
4937
+ ngOnDestroy() {
4938
+ this.removeTrigger();
4939
+ }
4940
+ setupTrigger() {
4941
+ const element = this.elementRef.nativeElement;
4942
+ if (element.type !== 'file') {
4943
+ console.warn('⚠️ [FloatingUploadTrigger] Directive should only be used on file input elements');
4944
+ return;
4945
+ }
4946
+ // Add change event listener
4947
+ element.addEventListener('change', this.onFileChange.bind(this));
4948
+ // Add floating uploader trigger icon if enabled
4949
+ if (this.showIcon) {
4950
+ this.addTriggerIcon();
4951
+ }
4952
+ }
4953
+ removeTrigger() {
4954
+ const element = this.elementRef.nativeElement;
4955
+ element.removeEventListener('change', this.onFileChange.bind(this));
4956
+ if (this.triggerIcon) {
4957
+ this.triggerIcon.remove();
4958
+ }
4959
+ }
4960
+ onFileChange(event) {
4961
+ const target = event.target;
4962
+ const files = target.files;
4963
+ if (files && files.length > 0) {
4964
+ console.log('📁 [FloatingUploadTrigger] Files selected:', files.length);
4965
+ // Show floating uploader
4966
+ this.floatingUploader.showUploader(this.groupId);
4967
+ // Handle files through floating uploader
4968
+ this.floatingUploader.handleExternalFiles(Array.from(files), this.userId, this.groupId);
4969
+ // Emit files selected event
4970
+ this.filesSelected.emit(Array.from(files));
4971
+ }
4972
+ }
4973
+ addTriggerIcon() {
4974
+ const element = this.elementRef.nativeElement;
4975
+ const container = element.parentElement;
4976
+ if (!container)
4977
+ return;
4978
+ // Create trigger icon
4979
+ this.triggerIcon = document.createElement('button');
4980
+ this.triggerIcon.type = 'button';
4981
+ this.triggerIcon.className = 'floating-upload-trigger-icon';
4982
+ this.triggerIcon.innerHTML = `
4983
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
4984
+ <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
4985
+ </svg>
4986
+ `;
4987
+ this.triggerIcon.title = 'View upload progress';
4988
+ this.triggerIcon.style.cssText = `
4989
+ position: absolute;
4990
+ right: 8px;
4991
+ top: 50%;
4992
+ transform: translateY(-50%);
4993
+ background: #3b82f6;
4994
+ color: white;
4995
+ border: none;
4996
+ border-radius: 4px;
4997
+ padding: 4px;
4998
+ cursor: pointer;
4999
+ display: flex;
5000
+ align-items: center;
5001
+ justify-content: center;
5002
+ z-index: 10;
5003
+ `;
5004
+ // Make container relative positioned
5005
+ container.style.position = 'relative';
5006
+ // Add click handler to show floating uploader
5007
+ this.triggerIcon.addEventListener('click', (e) => {
5008
+ e.preventDefault();
5009
+ e.stopPropagation();
5010
+ this.floatingUploader.showUploader(this.groupId);
5011
+ });
5012
+ // Insert icon after the input
5013
+ element.parentNode?.insertBefore(this.triggerIcon, element.nextSibling);
5014
+ }
5015
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
5016
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.7", type: CideFloatingUploadTriggerDirective, isStandalone: true, selector: "[cideFloatingUploadTrigger]", inputs: { groupId: "groupId", userId: "userId", showIcon: "showIcon" }, outputs: { filesSelected: "filesSelected" }, ngImport: i0 });
5017
+ }
5018
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImport: i0, type: CideFloatingUploadTriggerDirective, decorators: [{
5019
+ type: Directive,
5020
+ args: [{
5021
+ selector: '[cideFloatingUploadTrigger]',
5022
+ standalone: true
5023
+ }]
5024
+ }], propDecorators: { groupId: [{
5025
+ type: Input
5026
+ }], userId: [{
5027
+ type: Input
5028
+ }], showIcon: [{
5029
+ type: Input
5030
+ }], filesSelected: [{
5031
+ type: Output
5032
+ }] } });
4827
5033
 
4828
5034
  class CideTextareaComponent {
4829
5035
  label = '';
@@ -9063,5 +9269,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.7", ngImpor
9063
9269
  * Generated bundle index. Do not edit.
9064
9270
  */
9065
9271
 
9066
- export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
9272
+ export { CideCoreFileManagerService, CideEleButtonComponent, CideEleConfirmationModalComponent, CideEleDataGridComponent, CideEleDropdownComponent, CideEleFileImageDirective, CideEleFileInputComponent, CideEleFileManagerService, CideEleFloatingFileUploaderComponent, CideEleGlobalFileUploaderComponent, CideEleGlobalNotificationsComponent, CideEleJsonEditorComponent, CideEleResizerDirective, CideEleSkeletonLoaderComponent, CideEleTabComponent, CideEleToastNotificationComponent, CideElementsService, CideFloatingUploadTriggerDirective, CideIconComponent, CideInputComponent, CideSelectComponent, CideSelectOptionComponent, CideSpinnerComponent, CideTextareaComponent, ConfirmationService, CoreFileManagerInsertUpdatePayload, DEFAULT_GRID_CONFIG, DropdownManagerService, ICoreCyfmSave, MFileManager, NotificationService, TooltipDirective };
9067
9273
  //# sourceMappingURL=cloud-ide-element.mjs.map