@shohojdhara/atomix 0.3.7 → 0.3.8
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.
- package/dist/atomix.css +77 -0
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +77 -0
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js.map +1 -1
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +578 -515
- package/dist/index.esm.js +3157 -2626
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +10496 -9973
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +237 -420
- package/dist/theme.js +1629 -1701
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.stories.tsx +238 -0
- package/src/components/DataTable/DataTable.test.tsx +450 -0
- package/src/components/DataTable/DataTable.tsx +384 -61
- package/src/components/DatePicker/DatePicker.tsx +29 -38
- package/src/components/Upload/Upload.tsx +539 -40
- package/src/lib/composables/useDataTable.ts +355 -15
- package/src/lib/composables/useDatePicker.ts +19 -0
- package/src/lib/constants/components.ts +10 -0
- package/src/lib/theme/adapters/cssVariableMapper.ts +29 -14
- package/src/lib/theme/adapters/index.ts +1 -4
- package/src/lib/theme/config/configLoader.ts +53 -35
- package/src/lib/theme/core/composeTheme.ts +22 -30
- package/src/lib/theme/core/createTheme.ts +49 -26
- package/src/lib/theme/core/index.ts +0 -1
- package/src/lib/theme/generators/generateCSSNested.ts +4 -3
- package/src/lib/theme/generators/generateCSSVariables.ts +24 -16
- package/src/lib/theme/index.ts +10 -17
- package/src/lib/theme/runtime/ThemeApplicator.ts +6 -109
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +3 -3
- package/src/lib/theme/runtime/ThemeProvider.tsx +186 -44
- package/src/lib/theme/runtime/useTheme.ts +1 -1
- package/src/lib/theme/runtime/useThemeTokens.ts +7 -16
- package/src/lib/theme/test/testTheme.ts +2 -1
- package/src/lib/theme/types.ts +14 -14
- package/src/lib/theme/utils/componentTheming.ts +35 -27
- package/src/lib/theme/utils/domUtils.ts +57 -15
- package/src/lib/theme/utils/injectCSS.ts +0 -1
- package/src/lib/theme/utils/themeHelpers.ts +1 -39
- package/src/lib/theme/utils/themeUtils.ts +1 -170
- package/src/lib/types/components.ts +145 -0
- package/src/lib/utils/dataTableExport.ts +143 -0
- package/src/styles/06-components/_components.data-table.scss +95 -0
- package/src/lib/hooks/useThemeTokens.ts +0 -105
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import { UPLOAD } from '../../lib/constants/components';
|
|
3
3
|
|
|
4
4
|
export interface UploadProps {
|
|
@@ -52,6 +52,46 @@ export interface UploadProps {
|
|
|
52
52
|
*/
|
|
53
53
|
icon?: React.ReactNode;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Upload endpoint URL. If not provided, upload will be simulated.
|
|
57
|
+
*/
|
|
58
|
+
uploadEndpoint?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* HTTP method for upload request
|
|
62
|
+
*/
|
|
63
|
+
uploadMethod?: 'POST' | 'PUT' | 'PATCH';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Additional headers to include in upload request
|
|
67
|
+
*/
|
|
68
|
+
uploadHeaders?: Record<string, string>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Additional form data fields to include in upload
|
|
72
|
+
*/
|
|
73
|
+
uploadData?: Record<string, string>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Chunk size in MB for chunked uploads (0 = no chunking)
|
|
77
|
+
*/
|
|
78
|
+
chunkSizeInMB?: number;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of retry attempts for failed uploads
|
|
82
|
+
*/
|
|
83
|
+
maxRetries?: number;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delay in milliseconds between retry attempts
|
|
87
|
+
*/
|
|
88
|
+
retryDelay?: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether to automatically upload files after selection
|
|
92
|
+
*/
|
|
93
|
+
autoUpload?: boolean;
|
|
94
|
+
|
|
55
95
|
/**
|
|
56
96
|
* Called when files are selected
|
|
57
97
|
*/
|
|
@@ -65,13 +105,18 @@ export interface UploadProps {
|
|
|
65
105
|
/**
|
|
66
106
|
* Called when file upload is complete
|
|
67
107
|
*/
|
|
68
|
-
onFileUploadComplete?: (file: File) => void;
|
|
108
|
+
onFileUploadComplete?: (file: File, response?: any) => void;
|
|
69
109
|
|
|
70
110
|
/**
|
|
71
111
|
* Called on file upload errors
|
|
72
112
|
*/
|
|
73
113
|
onFileUploadError?: (file: File, error: string) => void;
|
|
74
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Called when upload is cancelled
|
|
117
|
+
*/
|
|
118
|
+
onUploadCancel?: (file: File) => void;
|
|
119
|
+
|
|
75
120
|
/**
|
|
76
121
|
* Additional CSS class
|
|
77
122
|
*/
|
|
@@ -88,6 +133,18 @@ export interface UploadProps {
|
|
|
88
133
|
*/
|
|
89
134
|
type UploadStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
90
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Upload request state
|
|
138
|
+
*/
|
|
139
|
+
interface UploadRequest {
|
|
140
|
+
xhr: XMLHttpRequest | null;
|
|
141
|
+
abortController: AbortController;
|
|
142
|
+
file: File;
|
|
143
|
+
retryCount: number;
|
|
144
|
+
chunkXhrs?: XMLHttpRequest[];
|
|
145
|
+
intervalId?: NodeJS.Timeout;
|
|
146
|
+
}
|
|
147
|
+
|
|
91
148
|
/**
|
|
92
149
|
* Upload component for file uploads with drag and drop
|
|
93
150
|
*/
|
|
@@ -108,14 +165,25 @@ export const Upload: React.FC<UploadProps> = ({
|
|
|
108
165
|
buttonText = 'Choose File',
|
|
109
166
|
helperText = `Maximum size: ${maxSizeInMB}MB`,
|
|
110
167
|
icon = <i className="icon-lux-cloud-arrow-up-fill"></i>,
|
|
168
|
+
uploadEndpoint,
|
|
169
|
+
uploadMethod = 'POST',
|
|
170
|
+
uploadHeaders = {},
|
|
171
|
+
uploadData = {},
|
|
172
|
+
chunkSizeInMB = 0,
|
|
173
|
+
maxRetries = 3,
|
|
174
|
+
retryDelay = 1000,
|
|
175
|
+
autoUpload = true,
|
|
111
176
|
onFileSelect,
|
|
112
177
|
onFileUpload,
|
|
113
178
|
onFileUploadComplete,
|
|
114
179
|
onFileUploadError,
|
|
180
|
+
onUploadCancel,
|
|
115
181
|
className = '',
|
|
116
182
|
style,
|
|
117
183
|
}) => {
|
|
118
184
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
185
|
+
const uploadRequestRef = useRef<UploadRequest | null>(null);
|
|
186
|
+
const startTimeRef = useRef<number | null>(null);
|
|
119
187
|
|
|
120
188
|
const [status, setStatus] = useState<UploadStatus>('idle');
|
|
121
189
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -206,9 +274,12 @@ export const Upload: React.FC<UploadProps> = ({
|
|
|
206
274
|
|
|
207
275
|
// Process the first valid file
|
|
208
276
|
if (validFiles.length) {
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
277
|
+
const fileToUpload = validFiles[0];
|
|
278
|
+
if (fileToUpload) {
|
|
279
|
+
setCurrentFile(fileToUpload);
|
|
280
|
+
if (autoUpload) {
|
|
281
|
+
uploadFile(fileToUpload);
|
|
282
|
+
}
|
|
212
283
|
}
|
|
213
284
|
}
|
|
214
285
|
};
|
|
@@ -245,44 +316,433 @@ export const Upload: React.FC<UploadProps> = ({
|
|
|
245
316
|
return true;
|
|
246
317
|
};
|
|
247
318
|
|
|
248
|
-
//
|
|
249
|
-
const
|
|
319
|
+
// Calculate time remaining based on upload speed
|
|
320
|
+
const calculateTimeRemaining = useCallback((progress: number, elapsedTime: number): string => {
|
|
321
|
+
if (progress <= 0 || elapsedTime <= 0) return '';
|
|
322
|
+
|
|
323
|
+
const estimatedTotalTime = (elapsedTime / progress) * 100;
|
|
324
|
+
const remainingTime = estimatedTotalTime - elapsedTime;
|
|
325
|
+
|
|
326
|
+
if (remainingTime <= 0) return 'Almost done...';
|
|
327
|
+
|
|
328
|
+
const seconds = Math.ceil(remainingTime / 1000);
|
|
329
|
+
if (seconds < 60) {
|
|
330
|
+
return `${seconds} second${seconds !== 1 ? 's' : ''} left`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const minutes = Math.floor(seconds / 60);
|
|
334
|
+
const remainingSeconds = seconds % 60;
|
|
335
|
+
return `${minutes}m ${remainingSeconds}s left`;
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
// Upload file chunk
|
|
339
|
+
const uploadChunk = useCallback((
|
|
340
|
+
file: File,
|
|
341
|
+
chunkIndex: number,
|
|
342
|
+
totalChunks: number,
|
|
343
|
+
chunkSize: number,
|
|
344
|
+
chunkXhrs: XMLHttpRequest[]
|
|
345
|
+
): Promise<any> => {
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
const start = chunkIndex * chunkSize;
|
|
348
|
+
const end = Math.min(start + chunkSize, file.size);
|
|
349
|
+
const chunk = file.slice(start, end);
|
|
350
|
+
|
|
351
|
+
const formData = new FormData();
|
|
352
|
+
formData.append('file', chunk, file.name);
|
|
353
|
+
formData.append('chunkIndex', chunkIndex.toString());
|
|
354
|
+
formData.append('totalChunks', totalChunks.toString());
|
|
355
|
+
formData.append('fileName', file.name);
|
|
356
|
+
formData.append('fileSize', file.size.toString());
|
|
357
|
+
|
|
358
|
+
// Add additional form data
|
|
359
|
+
Object.entries(uploadData).forEach(([key, value]) => {
|
|
360
|
+
formData.append(key, value);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const xhr = new XMLHttpRequest();
|
|
364
|
+
|
|
365
|
+
// Store XHR for cancellation
|
|
366
|
+
chunkXhrs[chunkIndex] = xhr;
|
|
367
|
+
|
|
368
|
+
// Set up progress tracking for this chunk
|
|
369
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
370
|
+
if (e.lengthComputable && uploadRequestRef.current) {
|
|
371
|
+
const chunkProgress = (e.loaded / e.total) * 100;
|
|
372
|
+
const overallProgress = ((chunkIndex * chunkSize + e.loaded) / file.size) * 100;
|
|
373
|
+
|
|
374
|
+
setUploadProgress(Math.min(overallProgress, 100));
|
|
375
|
+
|
|
376
|
+
const elapsedTime = startTimeRef.current ? Date.now() - startTimeRef.current : 0;
|
|
377
|
+
const timeRemaining = calculateTimeRemaining(overallProgress, elapsedTime);
|
|
378
|
+
setTimeLeft(timeRemaining);
|
|
379
|
+
|
|
380
|
+
if (onFileUpload) {
|
|
381
|
+
onFileUpload(file, overallProgress);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
xhr.addEventListener('load', () => {
|
|
387
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
388
|
+
try {
|
|
389
|
+
const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
|
|
390
|
+
resolve(response);
|
|
391
|
+
} catch {
|
|
392
|
+
resolve({});
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`));
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
xhr.addEventListener('error', () => {
|
|
400
|
+
reject(new Error('Network error occurred during upload'));
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
xhr.addEventListener('abort', () => {
|
|
404
|
+
reject(new Error('Upload was cancelled'));
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
xhr.open(uploadMethod, uploadEndpoint!);
|
|
408
|
+
|
|
409
|
+
// Set headers
|
|
410
|
+
Object.entries(uploadHeaders).forEach(([key, value]) => {
|
|
411
|
+
xhr.setRequestHeader(key, value);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
xhr.send(formData);
|
|
415
|
+
});
|
|
416
|
+
}, [uploadEndpoint, uploadMethod, uploadHeaders, uploadData, onFileUpload, calculateTimeRemaining]);
|
|
417
|
+
|
|
418
|
+
// Upload file (with chunking support)
|
|
419
|
+
const uploadFile = useCallback(async (file: File, retryCount: number = 0) => {
|
|
420
|
+
// If no endpoint is provided, simulate upload for backward compatibility
|
|
421
|
+
if (!uploadEndpoint) {
|
|
422
|
+
setStatus('loading');
|
|
423
|
+
setUploadProgress(0);
|
|
424
|
+
startTimeRef.current = Date.now();
|
|
425
|
+
|
|
426
|
+
// Simulate progress updates
|
|
427
|
+
let progress = 0;
|
|
428
|
+
const interval = setInterval(() => {
|
|
429
|
+
progress += 5;
|
|
430
|
+
|
|
431
|
+
if (progress < 100) {
|
|
432
|
+
setUploadProgress(progress);
|
|
433
|
+
const elapsedTime = Date.now() - (startTimeRef.current || Date.now());
|
|
434
|
+
const timeRemaining = calculateTimeRemaining(progress, elapsedTime);
|
|
435
|
+
setTimeLeft(timeRemaining);
|
|
436
|
+
|
|
437
|
+
if (onFileUpload) {
|
|
438
|
+
onFileUpload(file, progress);
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
clearInterval(interval);
|
|
442
|
+
setStatus('success');
|
|
443
|
+
setSuccessMessage('Upload successful');
|
|
444
|
+
startTimeRef.current = null;
|
|
445
|
+
|
|
446
|
+
if (onFileUploadComplete) {
|
|
447
|
+
onFileUploadComplete(file);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
uploadRequestRef.current = null;
|
|
451
|
+
}
|
|
452
|
+
}, 500);
|
|
453
|
+
|
|
454
|
+
// Store interval for cleanup
|
|
455
|
+
uploadRequestRef.current = {
|
|
456
|
+
xhr: null,
|
|
457
|
+
abortController: new AbortController(),
|
|
458
|
+
file,
|
|
459
|
+
retryCount,
|
|
460
|
+
intervalId: interval,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
250
466
|
setStatus('loading');
|
|
251
467
|
setUploadProgress(0);
|
|
468
|
+
setErrorMessage(null);
|
|
469
|
+
setSuccessMessage(null);
|
|
470
|
+
startTimeRef.current = Date.now();
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const chunkSize = chunkSizeInMB > 0 ? chunkSizeInMB * 1024 * 1024 : file.size;
|
|
474
|
+
const totalChunks = chunkSizeInMB > 0 ? Math.ceil(file.size / chunkSize) : 1;
|
|
475
|
+
|
|
476
|
+
if (totalChunks > 1) {
|
|
477
|
+
// Chunked upload
|
|
478
|
+
const chunkXhrs: XMLHttpRequest[] = [];
|
|
479
|
+
|
|
480
|
+
// Initialize upload request with chunk array
|
|
481
|
+
uploadRequestRef.current = {
|
|
482
|
+
xhr: null,
|
|
483
|
+
abortController: new AbortController(),
|
|
484
|
+
file,
|
|
485
|
+
retryCount,
|
|
486
|
+
chunkXhrs,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const chunkResults = [];
|
|
491
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
492
|
+
// Check if upload was cancelled
|
|
493
|
+
if (!uploadRequestRef.current) {
|
|
494
|
+
throw new Error('Upload was cancelled');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const result = await uploadChunk(file, i, totalChunks, chunkSize, chunkXhrs);
|
|
498
|
+
chunkResults.push(result);
|
|
499
|
+
}
|
|
252
500
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
501
|
+
setStatus('success');
|
|
502
|
+
setSuccessMessage('Upload successful');
|
|
503
|
+
setUploadProgress(100);
|
|
504
|
+
setTimeLeft(null);
|
|
505
|
+
startTimeRef.current = null;
|
|
257
506
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
507
|
+
if (onFileUploadComplete) {
|
|
508
|
+
onFileUploadComplete(file, chunkResults);
|
|
509
|
+
}
|
|
261
510
|
|
|
262
|
-
|
|
263
|
-
|
|
511
|
+
uploadRequestRef.current = null;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
// Abort all remaining chunks
|
|
514
|
+
chunkXhrs.forEach(xhr => {
|
|
515
|
+
if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
|
|
516
|
+
xhr.abort();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
throw error;
|
|
264
520
|
}
|
|
265
521
|
} else {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
522
|
+
// Single upload - wrap in Promise for proper error handling
|
|
523
|
+
await new Promise<void>((resolve, reject) => {
|
|
524
|
+
const formData = new FormData();
|
|
525
|
+
formData.append('file', file);
|
|
526
|
+
|
|
527
|
+
// Add additional form data
|
|
528
|
+
Object.entries(uploadData).forEach(([key, value]) => {
|
|
529
|
+
formData.append(key, value);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const xhr = new XMLHttpRequest();
|
|
533
|
+
const abortController = new AbortController();
|
|
534
|
+
|
|
535
|
+
// Set up progress tracking
|
|
536
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
537
|
+
if (e.lengthComputable) {
|
|
538
|
+
const progress = (e.loaded / e.total) * 100;
|
|
539
|
+
setUploadProgress(progress);
|
|
540
|
+
|
|
541
|
+
const elapsedTime = startTimeRef.current ? Date.now() - startTimeRef.current : 0;
|
|
542
|
+
const timeRemaining = calculateTimeRemaining(progress, elapsedTime);
|
|
543
|
+
setTimeLeft(timeRemaining);
|
|
544
|
+
|
|
545
|
+
if (onFileUpload) {
|
|
546
|
+
onFileUpload(file, progress);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
xhr.addEventListener('load', () => {
|
|
552
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
553
|
+
setStatus('success');
|
|
554
|
+
setSuccessMessage('Upload successful');
|
|
555
|
+
setUploadProgress(100);
|
|
556
|
+
setTimeLeft(null);
|
|
557
|
+
startTimeRef.current = null;
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const response = xhr.responseText ? JSON.parse(xhr.responseText) : {};
|
|
561
|
+
if (onFileUploadComplete) {
|
|
562
|
+
onFileUploadComplete(file, response);
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
if (onFileUploadComplete) {
|
|
566
|
+
onFileUploadComplete(file);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
uploadRequestRef.current = null;
|
|
571
|
+
resolve();
|
|
572
|
+
} else {
|
|
573
|
+
reject(new Error(`Upload failed with status ${xhr.status}: ${xhr.statusText}`));
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
xhr.addEventListener('error', () => {
|
|
578
|
+
reject(new Error('Network error occurred during upload'));
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
xhr.addEventListener('abort', () => {
|
|
582
|
+
setStatus('idle');
|
|
583
|
+
setUploadProgress(0);
|
|
584
|
+
setTimeLeft(null);
|
|
585
|
+
startTimeRef.current = null;
|
|
586
|
+
uploadRequestRef.current = null;
|
|
587
|
+
|
|
588
|
+
if (onUploadCancel) {
|
|
589
|
+
onUploadCancel(file);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
reject(new Error('Upload was cancelled'));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
xhr.open(uploadMethod, uploadEndpoint);
|
|
596
|
+
|
|
597
|
+
// Set headers
|
|
598
|
+
Object.entries(uploadHeaders).forEach(([key, value]) => {
|
|
599
|
+
xhr.setRequestHeader(key, value);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Store request for cancellation
|
|
603
|
+
uploadRequestRef.current = {
|
|
604
|
+
xhr,
|
|
605
|
+
abortController,
|
|
606
|
+
file,
|
|
607
|
+
retryCount,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
xhr.send(formData);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const errorMessage = error instanceof Error ? error.message : 'Upload failed';
|
|
615
|
+
|
|
616
|
+
// Retry logic
|
|
617
|
+
if (retryCount < maxRetries && !errorMessage.includes('cancelled')) {
|
|
618
|
+
setStatus('loading');
|
|
619
|
+
setErrorMessage(`Upload failed. Retrying... (${retryCount + 1}/${maxRetries})`);
|
|
620
|
+
|
|
621
|
+
setTimeout(() => {
|
|
622
|
+
uploadFile(file, retryCount + 1);
|
|
623
|
+
}, retryDelay);
|
|
624
|
+
} else {
|
|
625
|
+
setStatus('error');
|
|
626
|
+
setErrorMessage(errorMessage);
|
|
627
|
+
setUploadProgress(0);
|
|
628
|
+
setTimeLeft(null);
|
|
629
|
+
startTimeRef.current = null;
|
|
630
|
+
uploadRequestRef.current = null;
|
|
631
|
+
|
|
632
|
+
if (onFileUploadError) {
|
|
633
|
+
onFileUploadError(file, errorMessage);
|
|
272
634
|
}
|
|
273
635
|
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
636
|
+
}
|
|
637
|
+
}, [
|
|
638
|
+
uploadEndpoint,
|
|
639
|
+
uploadMethod,
|
|
640
|
+
uploadHeaders,
|
|
641
|
+
uploadData,
|
|
642
|
+
chunkSizeInMB,
|
|
643
|
+
maxRetries,
|
|
644
|
+
retryDelay,
|
|
645
|
+
onFileUpload,
|
|
646
|
+
onFileUploadComplete,
|
|
647
|
+
onFileUploadError,
|
|
648
|
+
onUploadCancel,
|
|
649
|
+
uploadChunk,
|
|
650
|
+
calculateTimeRemaining,
|
|
651
|
+
]);
|
|
276
652
|
|
|
277
653
|
// Reset upload
|
|
278
|
-
const resetUpload = () => {
|
|
654
|
+
const resetUpload = useCallback(() => {
|
|
655
|
+
// Cancel any ongoing upload
|
|
656
|
+
if (uploadRequestRef.current) {
|
|
657
|
+
// Cancel single XHR request
|
|
658
|
+
if (uploadRequestRef.current.xhr) {
|
|
659
|
+
uploadRequestRef.current.xhr.abort();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Cancel all chunk XHR requests
|
|
663
|
+
if (uploadRequestRef.current.chunkXhrs) {
|
|
664
|
+
uploadRequestRef.current.chunkXhrs.forEach(xhr => {
|
|
665
|
+
if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
|
|
666
|
+
xhr.abort();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Clear interval if it exists
|
|
672
|
+
if (uploadRequestRef.current.intervalId) {
|
|
673
|
+
clearInterval(uploadRequestRef.current.intervalId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
uploadRequestRef.current = null;
|
|
677
|
+
}
|
|
678
|
+
|
|
279
679
|
setStatus('idle');
|
|
280
680
|
setCurrentFile(null);
|
|
281
681
|
setUploadProgress(0);
|
|
282
682
|
setTimeLeft(null);
|
|
283
683
|
setErrorMessage(null);
|
|
284
684
|
setSuccessMessage(null);
|
|
285
|
-
|
|
685
|
+
startTimeRef.current = null;
|
|
686
|
+
}, []);
|
|
687
|
+
|
|
688
|
+
// Cancel upload
|
|
689
|
+
const cancelUpload = useCallback(() => {
|
|
690
|
+
if (uploadRequestRef.current) {
|
|
691
|
+
// Cancel single XHR request
|
|
692
|
+
if (uploadRequestRef.current.xhr) {
|
|
693
|
+
uploadRequestRef.current.xhr.abort();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Cancel all chunk XHR requests
|
|
697
|
+
if (uploadRequestRef.current.chunkXhrs) {
|
|
698
|
+
uploadRequestRef.current.chunkXhrs.forEach(xhr => {
|
|
699
|
+
if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
|
|
700
|
+
xhr.abort();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Clear interval if it exists (for simulated uploads)
|
|
706
|
+
if (uploadRequestRef.current.intervalId) {
|
|
707
|
+
clearInterval(uploadRequestRef.current.intervalId);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
uploadRequestRef.current.abortController.abort();
|
|
711
|
+
|
|
712
|
+
if (onUploadCancel && uploadRequestRef.current.file) {
|
|
713
|
+
onUploadCancel(uploadRequestRef.current.file);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
uploadRequestRef.current = null;
|
|
718
|
+
resetUpload();
|
|
719
|
+
}, [onUploadCancel, resetUpload]);
|
|
720
|
+
|
|
721
|
+
// Cleanup on unmount
|
|
722
|
+
useEffect(() => {
|
|
723
|
+
return () => {
|
|
724
|
+
if (uploadRequestRef.current) {
|
|
725
|
+
// Cancel single XHR request
|
|
726
|
+
if (uploadRequestRef.current.xhr) {
|
|
727
|
+
uploadRequestRef.current.xhr.abort();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Cancel all chunk XHR requests
|
|
731
|
+
if (uploadRequestRef.current.chunkXhrs) {
|
|
732
|
+
uploadRequestRef.current.chunkXhrs.forEach(xhr => {
|
|
733
|
+
if (xhr && xhr.readyState !== XMLHttpRequest.DONE) {
|
|
734
|
+
xhr.abort();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Clear interval if it exists
|
|
740
|
+
if (uploadRequestRef.current.intervalId) {
|
|
741
|
+
clearInterval(uploadRequestRef.current.intervalId);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}, []);
|
|
286
746
|
|
|
287
747
|
// Build CSS classes
|
|
288
748
|
const uploadClasses = [
|
|
@@ -367,20 +827,59 @@ export const Upload: React.FC<UploadProps> = ({
|
|
|
367
827
|
|
|
368
828
|
{(status === 'loading' || status === 'error' || status === 'success') && (
|
|
369
829
|
<div className="c-upload__loader-control">
|
|
370
|
-
|
|
371
|
-
<
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
830
|
+
{status === 'loading' && (
|
|
831
|
+
<div className="c-upload__loader-bar">
|
|
832
|
+
<svg>
|
|
833
|
+
<circle cx="10" cy="10" r="10"></circle>
|
|
834
|
+
<circle cx="10" cy="10" r="10"></circle>
|
|
835
|
+
</svg>
|
|
836
|
+
</div>
|
|
837
|
+
)}
|
|
838
|
+
{status === 'loading' && (
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
className="c-upload__loader-cancel"
|
|
842
|
+
onClick={cancelUpload}
|
|
843
|
+
aria-label="Cancel upload"
|
|
844
|
+
>
|
|
845
|
+
<i className="icon-lux-x"></i>
|
|
846
|
+
</button>
|
|
847
|
+
)}
|
|
848
|
+
{(status === 'error' || status === 'success') && (
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
className="c-upload__loader-close"
|
|
852
|
+
onClick={resetUpload}
|
|
853
|
+
aria-label="Close upload progress"
|
|
854
|
+
>
|
|
855
|
+
<i className="icon-lux-x"></i>
|
|
856
|
+
</button>
|
|
857
|
+
)}
|
|
858
|
+
</div>
|
|
859
|
+
)}
|
|
860
|
+
|
|
861
|
+
{errorMessage && status === 'error' && (
|
|
862
|
+
<div className="c-upload__error-message">
|
|
863
|
+
{errorMessage}
|
|
864
|
+
{uploadRequestRef.current && uploadRequestRef.current.retryCount < maxRetries && (
|
|
865
|
+
<button
|
|
866
|
+
type="button"
|
|
867
|
+
className="c-upload__retry-btn"
|
|
868
|
+
onClick={() => {
|
|
869
|
+
if (currentFile) {
|
|
870
|
+
uploadFile(currentFile, uploadRequestRef.current?.retryCount || 0);
|
|
871
|
+
}
|
|
872
|
+
}}
|
|
873
|
+
>
|
|
874
|
+
Retry
|
|
875
|
+
</button>
|
|
876
|
+
)}
|
|
877
|
+
</div>
|
|
878
|
+
)}
|
|
879
|
+
|
|
880
|
+
{successMessage && status === 'success' && (
|
|
881
|
+
<div className="c-upload__success-message">
|
|
882
|
+
{successMessage}
|
|
384
883
|
</div>
|
|
385
884
|
)}
|
|
386
885
|
</div>
|