@adamosuiteservices/ui 2.17.1 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/components/ui/file-upload/file-upload-v2.d.ts +35 -0
- package/dist/components/ui/file-upload/file-upload.d.ts +5 -1
- package/dist/components/ui/file-upload/index.d.ts +1 -0
- package/dist/file-upload.cjs +11 -4
- package/dist/file-upload.js +521 -183
- package/dist/styles.css +1 -1
- package/docs/ai-guide.md +7 -7
- package/docs/components/ui/file-upload-v2.md +1113 -0
- package/docs/components/ui/file-upload.md +208 -0
- package/llm.txt +8 -5
- package/package.json +1 -1
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
# File Upload V2 Component
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Enhanced file upload component with **unique file identification**, **metadata support**, and **server integration** capabilities. Built on the FileUpload component foundation, FileUploadV2 adds automatic ID generation for each file, making it ideal for scenarios requiring server-side file tracking, asynchronous uploads, and delete operations.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ✅ All features from FileUpload component
|
|
10
|
+
- ✅ Automatic unique ID generation for each file
|
|
11
|
+
- ✅ Custom ID generator support
|
|
12
|
+
- ✅ Metadata support for storing additional file information
|
|
13
|
+
- ✅ Server-side file tracking with IDs
|
|
14
|
+
- ✅ Pre-loaded files with server IDs
|
|
15
|
+
- ✅ Callbacks with file references for API integration
|
|
16
|
+
- ✅ Full TypeScript with FileWithMetadata type
|
|
17
|
+
- ✅ Perfect for async upload/delete operations
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
Use **FileUploadV2** when you need:
|
|
22
|
+
|
|
23
|
+
- Server-side file tracking with unique identifiers
|
|
24
|
+
- Asynchronous file uploads with progress tracking
|
|
25
|
+
- Delete operations requiring file IDs
|
|
26
|
+
- Pre-loading existing files from server
|
|
27
|
+
- Storing metadata alongside files
|
|
28
|
+
- Complex file management workflows
|
|
29
|
+
|
|
30
|
+
Use **FileUpload** (original) when you need:
|
|
31
|
+
|
|
32
|
+
- Simple file selection without server integration
|
|
33
|
+
- One-time form submission with files
|
|
34
|
+
- No need for file identification or tracking
|
|
35
|
+
|
|
36
|
+
## Import
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { FileUploadV2 } from "@adamosuiteservices/ui/file-upload";
|
|
40
|
+
import type {
|
|
41
|
+
FileUploadV2Props,
|
|
42
|
+
FileWithMetadata,
|
|
43
|
+
FileUploadV2Labels,
|
|
44
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Basic usage
|
|
48
|
+
|
|
49
|
+
### Single file with automatic IDs
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useState } from "react";
|
|
53
|
+
import {
|
|
54
|
+
FileUploadV2,
|
|
55
|
+
FileWithMetadata,
|
|
56
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
57
|
+
|
|
58
|
+
function MyForm() {
|
|
59
|
+
const [file, setFile] = useState<FileWithMetadata | null>(null);
|
|
60
|
+
|
|
61
|
+
return <FileUploadV2 selectedFile={file} onFileSelect={setFile} />;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Multiple files with automatic IDs
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useState } from "react";
|
|
69
|
+
import {
|
|
70
|
+
FileUploadV2,
|
|
71
|
+
FileWithMetadata,
|
|
72
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
73
|
+
|
|
74
|
+
function MyForm() {
|
|
75
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<FileUploadV2 selectedFiles={files} onFilesSelect={setFiles} multiple />
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### With server files (pre-loaded)
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useState, useEffect } from "react";
|
|
87
|
+
import {
|
|
88
|
+
FileUploadV2,
|
|
89
|
+
FileWithMetadata,
|
|
90
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
91
|
+
|
|
92
|
+
function MyForm() {
|
|
93
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
// Load existing files from server
|
|
97
|
+
const serverFiles: FileWithMetadata[] = [
|
|
98
|
+
{
|
|
99
|
+
id: "server-file-123",
|
|
100
|
+
file: new File(["content"], "report.xlsx", {
|
|
101
|
+
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
102
|
+
}),
|
|
103
|
+
metadata: {
|
|
104
|
+
uploadedAt: "2024-01-15T10:30:00Z",
|
|
105
|
+
uploadedBy: "user@example.com",
|
|
106
|
+
serverUrl: "https://api.example.com/files/server-file-123",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
setFiles(serverFiles);
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<FileUploadV2 selectedFiles={files} onFilesSelect={setFiles} multiple />
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Props
|
|
120
|
+
|
|
121
|
+
### FileWithMetadata type
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
type FileWithMetadata = {
|
|
125
|
+
id: string;
|
|
126
|
+
file: File;
|
|
127
|
+
metadata?: Record<string, unknown>;
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- **id**: Unique identifier for the file (auto-generated or provided)
|
|
132
|
+
- **file**: Native browser File object
|
|
133
|
+
- **metadata**: Optional additional data (upload timestamp, server URL, etc.)
|
|
134
|
+
|
|
135
|
+
### Simple mode (single file)
|
|
136
|
+
|
|
137
|
+
#### selectedFile
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
selectedFile?: FileWithMetadata | null
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
State of the currently selected file with ID and metadata. Use `null` when there is no file.
|
|
144
|
+
|
|
145
|
+
#### onFileSelect
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
onFileSelect?: (file: FileWithMetadata | null) => void
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Callback invoked when the user selects or removes a file. Receives the file with ID and metadata, or `null` when removed.
|
|
152
|
+
|
|
153
|
+
#### onFileAdd
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
onFileAdd?: (file: FileWithMetadata) => void
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Callback invoked when a new file is added. Receives the file with auto-generated ID. This fires in addition to `onFileSelect`.
|
|
160
|
+
|
|
161
|
+
**Use case**: Start upload immediately when file is added.
|
|
162
|
+
|
|
163
|
+
**Example**:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const handleFileAdd = async (fileWithMetadata: FileWithMetadata) => {
|
|
167
|
+
console.log("File added with ID:", fileWithMetadata.id);
|
|
168
|
+
|
|
169
|
+
const formData = new FormData();
|
|
170
|
+
formData.append("file", fileWithMetadata.file);
|
|
171
|
+
formData.append("fileId", fileWithMetadata.id);
|
|
172
|
+
|
|
173
|
+
await fetch("/api/upload", {
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: formData,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
<FileUploadV2
|
|
180
|
+
selectedFile={file}
|
|
181
|
+
onFileSelect={setFile}
|
|
182
|
+
onFileAdd={handleFileAdd}
|
|
183
|
+
/>;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### onFileRemove
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
onFileRemove?: (file: FileWithMetadata) => void
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Callback invoked when a file is removed. Receives the file with its ID, enabling server-side deletion.
|
|
193
|
+
|
|
194
|
+
**Use case**: Call API to delete file from server using the file ID.
|
|
195
|
+
|
|
196
|
+
**Example**:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const handleFileRemove = async (fileWithMetadata: FileWithMetadata) => {
|
|
200
|
+
console.log("File removed with ID:", fileWithMetadata.id);
|
|
201
|
+
|
|
202
|
+
// Only delete from server if it has been uploaded
|
|
203
|
+
if (fileWithMetadata.metadata?.serverUrl) {
|
|
204
|
+
await fetch(`/api/files/${fileWithMetadata.id}`, {
|
|
205
|
+
method: "DELETE",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
<FileUploadV2
|
|
211
|
+
selectedFile={file}
|
|
212
|
+
onFileSelect={setFile}
|
|
213
|
+
onFileRemove={handleFileRemove}
|
|
214
|
+
/>;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Multiple mode
|
|
218
|
+
|
|
219
|
+
#### selectedFiles
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
selectedFiles?: FileWithMetadata[]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Array of selected files with IDs and metadata.
|
|
226
|
+
|
|
227
|
+
#### onFilesSelect
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
onFilesSelect?: (files: FileWithMetadata[]) => void
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Callback invoked when the user selects, adds, or removes files.
|
|
234
|
+
|
|
235
|
+
#### onFilesAdd
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
onFilesAdd?: (files: FileWithMetadata[]) => void
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Callback invoked when new files are added. Receives array of files with auto-generated IDs. This fires in addition to `onFilesSelect`.
|
|
242
|
+
|
|
243
|
+
**Example**:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
const handleFilesAdd = async (filesWithMetadata: FileWithMetadata[]) => {
|
|
247
|
+
console.log(
|
|
248
|
+
"Files added:",
|
|
249
|
+
filesWithMetadata.map((f) => f.id),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Upload each file
|
|
253
|
+
await Promise.all(
|
|
254
|
+
filesWithMetadata.map(async (fwm) => {
|
|
255
|
+
const formData = new FormData();
|
|
256
|
+
formData.append("file", fwm.file);
|
|
257
|
+
formData.append("fileId", fwm.id);
|
|
258
|
+
|
|
259
|
+
return fetch("/api/upload", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: formData,
|
|
262
|
+
});
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
<FileUploadV2
|
|
268
|
+
selectedFiles={files}
|
|
269
|
+
onFilesSelect={setFiles}
|
|
270
|
+
onFilesAdd={handleFilesAdd}
|
|
271
|
+
multiple
|
|
272
|
+
/>;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### onFilesRemove
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
onFilesRemove?: (files: FileWithMetadata[]) => void
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Callback invoked when files are removed. Receives array of removed files with their IDs.
|
|
282
|
+
|
|
283
|
+
**Example**:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
const handleFilesRemove = async (removedFiles: FileWithMetadata[]) => {
|
|
287
|
+
console.log(
|
|
288
|
+
"Files removed:",
|
|
289
|
+
removedFiles.map((f) => f.id),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Delete from server
|
|
293
|
+
await Promise.all(
|
|
294
|
+
removedFiles
|
|
295
|
+
.filter((f) => f.metadata?.serverUrl)
|
|
296
|
+
.map((f) => fetch(`/api/files/${f.id}`, { method: "DELETE" })),
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
<FileUploadV2
|
|
301
|
+
selectedFiles={files}
|
|
302
|
+
onFilesSelect={setFiles}
|
|
303
|
+
onFilesRemove={handleFilesRemove}
|
|
304
|
+
multiple
|
|
305
|
+
/>;
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### multiple
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
multiple?: boolean
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Enables multiple file mode. Default: `false`
|
|
315
|
+
|
|
316
|
+
#### maxFiles
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
maxFiles?: number
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Maximum number of files allowed in multiple mode. Default: `10`
|
|
323
|
+
|
|
324
|
+
#### filesPosition
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
filesPosition?: "above" | "below"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Position where selected files appear in multiple mode. Default: `"below"`
|
|
331
|
+
|
|
332
|
+
### ID generation
|
|
333
|
+
|
|
334
|
+
#### generateId
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
generateId?: (file: File) => string
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Custom function to generate unique IDs for files. If not provided, uses default generator.
|
|
341
|
+
|
|
342
|
+
**Default generator format**: `{timestamp}-{random}-{filename}`
|
|
343
|
+
|
|
344
|
+
Example: `1704892800000-x7k2p9q3m-report.xlsx`
|
|
345
|
+
|
|
346
|
+
**Use case**: Use custom ID format for integration with existing systems.
|
|
347
|
+
|
|
348
|
+
**Example**:
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
const customIdGenerator = (file: File) => {
|
|
352
|
+
return `custom-${Date.now()}-${file.name}`;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
<FileUploadV2
|
|
356
|
+
selectedFile={file}
|
|
357
|
+
onFileSelect={setFile}
|
|
358
|
+
generateId={customIdGenerator}
|
|
359
|
+
/>;
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Example with UUID**:
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import { v4 as uuidv4 } from "uuid";
|
|
366
|
+
|
|
367
|
+
<FileUploadV2
|
|
368
|
+
selectedFile={file}
|
|
369
|
+
onFileSelect={setFile}
|
|
370
|
+
generateId={(file) => uuidv4()}
|
|
371
|
+
/>;
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Common props
|
|
375
|
+
|
|
376
|
+
All props from the original FileUpload component are supported:
|
|
377
|
+
|
|
378
|
+
#### acceptedExtensions
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
acceptedExtensions?: string[]
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Array of allowed file extensions. Default: `[".xls", ".xlsx", ".numbers"]`
|
|
385
|
+
|
|
386
|
+
#### maxSizeInMB
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
maxSizeInMB?: number
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Maximum file size in megabytes. Default: `50`
|
|
393
|
+
|
|
394
|
+
#### labels
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
labels?: FileUploadV2Labels
|
|
398
|
+
|
|
399
|
+
type FileUploadV2Labels = {
|
|
400
|
+
dragDrop?: string
|
|
401
|
+
selectFile?: string
|
|
402
|
+
fileRequirements?: string
|
|
403
|
+
filesSelected?: (count: number) => string
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Customizable labels for internationalization.
|
|
408
|
+
|
|
409
|
+
#### aria-invalid / invalid
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
"aria-invalid"?: boolean
|
|
413
|
+
invalid?: boolean
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Marks the component as invalid. Default: `false`
|
|
417
|
+
|
|
418
|
+
#### disabled
|
|
419
|
+
|
|
420
|
+
```tsx
|
|
421
|
+
disabled?: boolean
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Disables the component. Default: `false`
|
|
425
|
+
|
|
426
|
+
#### input
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
input?: ComponentProps<"input">
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Custom props for the internal `<input type="file">` element.
|
|
433
|
+
|
|
434
|
+
#### onInvalidFile
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
onInvalidFile?: (file: FileWithMetadata, reason: "extension" | "size") => void
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Callback invoked when a file does not pass validation.
|
|
441
|
+
|
|
442
|
+
## Examples
|
|
443
|
+
|
|
444
|
+
### With Field components
|
|
445
|
+
|
|
446
|
+
```tsx
|
|
447
|
+
import {
|
|
448
|
+
Field,
|
|
449
|
+
FieldLabel,
|
|
450
|
+
FieldDescription,
|
|
451
|
+
FieldError,
|
|
452
|
+
FieldGroup,
|
|
453
|
+
} from "@adamosuiteservices/ui/field";
|
|
454
|
+
import {
|
|
455
|
+
FileUploadV2,
|
|
456
|
+
FileWithMetadata,
|
|
457
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
458
|
+
|
|
459
|
+
function FileUploadForm() {
|
|
460
|
+
const [file, setFile] = useState<FileWithMetadata | null>(null);
|
|
461
|
+
const [error, setError] = useState<string>("");
|
|
462
|
+
|
|
463
|
+
const handleInvalidFile = (
|
|
464
|
+
file: FileWithMetadata,
|
|
465
|
+
reason: "extension" | "size",
|
|
466
|
+
) => {
|
|
467
|
+
if (reason === "extension") {
|
|
468
|
+
setError(`File "${file.file.name}" has an invalid extension.`);
|
|
469
|
+
} else if (reason === "size") {
|
|
470
|
+
setError(`File "${file.file.name}" exceeds the maximum size limit.`);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<FieldGroup className="adm:max-w-xl">
|
|
476
|
+
<Field>
|
|
477
|
+
<FieldLabel>Upload document (PDF or DOCX only, max 2MB)</FieldLabel>
|
|
478
|
+
<FileUploadV2
|
|
479
|
+
selectedFile={file}
|
|
480
|
+
onFileSelect={(newFile) => {
|
|
481
|
+
setFile(newFile);
|
|
482
|
+
if (newFile) setError("");
|
|
483
|
+
}}
|
|
484
|
+
onInvalidFile={handleInvalidFile}
|
|
485
|
+
aria-invalid={!!error}
|
|
486
|
+
acceptedExtensions={[".pdf", ".docx"]}
|
|
487
|
+
maxSizeInMB={2}
|
|
488
|
+
/>
|
|
489
|
+
{error ? (
|
|
490
|
+
<FieldError>{error}</FieldError>
|
|
491
|
+
) : (
|
|
492
|
+
<FieldDescription>
|
|
493
|
+
Supported formats: PDF, DOCX. Maximum 2MB.
|
|
494
|
+
</FieldDescription>
|
|
495
|
+
)}
|
|
496
|
+
</Field>
|
|
497
|
+
</FieldGroup>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### With API integration (async upload)
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
import { useState } from "react";
|
|
506
|
+
import {
|
|
507
|
+
FileUploadV2,
|
|
508
|
+
FileWithMetadata,
|
|
509
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
510
|
+
import { Button } from "@adamosuiteservices/ui/button";
|
|
511
|
+
import { Typography } from "@adamosuiteservices/ui/typography";
|
|
512
|
+
|
|
513
|
+
function AsyncUploadExample() {
|
|
514
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
515
|
+
const [uploadStatus, setUploadStatus] = useState<
|
|
516
|
+
Record<string, "uploading" | "success" | "error">
|
|
517
|
+
>({});
|
|
518
|
+
|
|
519
|
+
const handleFilesAdd = async (addedFiles: FileWithMetadata[]) => {
|
|
520
|
+
// Mark files as uploading
|
|
521
|
+
const newStatus = { ...uploadStatus };
|
|
522
|
+
addedFiles.forEach((f) => {
|
|
523
|
+
newStatus[f.id] = "uploading";
|
|
524
|
+
});
|
|
525
|
+
setUploadStatus(newStatus);
|
|
526
|
+
|
|
527
|
+
// Upload each file
|
|
528
|
+
await Promise.all(
|
|
529
|
+
addedFiles.map(async (fileWithMetadata) => {
|
|
530
|
+
try {
|
|
531
|
+
const formData = new FormData();
|
|
532
|
+
formData.append("file", fileWithMetadata.file);
|
|
533
|
+
formData.append("fileId", fileWithMetadata.id);
|
|
534
|
+
|
|
535
|
+
const response = await fetch("/api/upload", {
|
|
536
|
+
method: "POST",
|
|
537
|
+
body: formData,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
if (response.ok) {
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
|
|
543
|
+
// Update file with server metadata
|
|
544
|
+
setFiles((prev) =>
|
|
545
|
+
prev.map((f) =>
|
|
546
|
+
f.id === fileWithMetadata.id
|
|
547
|
+
? {
|
|
548
|
+
...f,
|
|
549
|
+
metadata: {
|
|
550
|
+
...f.metadata,
|
|
551
|
+
serverUrl: data.url,
|
|
552
|
+
uploadedAt: new Date().toISOString(),
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
: f,
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
setUploadStatus((prev) => ({
|
|
560
|
+
...prev,
|
|
561
|
+
[fileWithMetadata.id]: "success",
|
|
562
|
+
}));
|
|
563
|
+
} else {
|
|
564
|
+
setUploadStatus((prev) => ({
|
|
565
|
+
...prev,
|
|
566
|
+
[fileWithMetadata.id]: "error",
|
|
567
|
+
}));
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error("Upload failed:", error);
|
|
571
|
+
setUploadStatus((prev) => ({
|
|
572
|
+
...prev,
|
|
573
|
+
[fileWithMetadata.id]: "error",
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const handleFilesRemove = async (removedFiles: FileWithMetadata[]) => {
|
|
581
|
+
// Delete from server if they were uploaded
|
|
582
|
+
await Promise.all(
|
|
583
|
+
removedFiles
|
|
584
|
+
.filter((f) => f.metadata?.serverUrl)
|
|
585
|
+
.map(async (f) => {
|
|
586
|
+
try {
|
|
587
|
+
await fetch(`/api/files/${f.id}`, { method: "DELETE" });
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error("Delete failed:", error);
|
|
590
|
+
}
|
|
591
|
+
}),
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Clean up upload status
|
|
595
|
+
setUploadStatus((prev) => {
|
|
596
|
+
const newStatus = { ...prev };
|
|
597
|
+
removedFiles.forEach((f) => delete newStatus[f.id]);
|
|
598
|
+
return newStatus;
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<div className="adm:max-w-xl adm:space-y-4">
|
|
604
|
+
<FileUploadV2
|
|
605
|
+
selectedFiles={files}
|
|
606
|
+
onFilesSelect={setFiles}
|
|
607
|
+
onFilesAdd={handleFilesAdd}
|
|
608
|
+
onFilesRemove={handleFilesRemove}
|
|
609
|
+
multiple
|
|
610
|
+
/>
|
|
611
|
+
|
|
612
|
+
{files.length > 0 && (
|
|
613
|
+
<div className="adm:space-y-2">
|
|
614
|
+
<Typography className="adm:font-medium">Upload status:</Typography>
|
|
615
|
+
{files.map((f) => (
|
|
616
|
+
<div key={f.id} className="adm:flex adm:items-center adm:gap-2">
|
|
617
|
+
<Typography className="adm:text-sm">{f.file.name}</Typography>
|
|
618
|
+
<Typography
|
|
619
|
+
className="adm:text-sm"
|
|
620
|
+
color={
|
|
621
|
+
uploadStatus[f.id] === "success"
|
|
622
|
+
? "success"
|
|
623
|
+
: uploadStatus[f.id] === "error"
|
|
624
|
+
? "destructive"
|
|
625
|
+
: "muted"
|
|
626
|
+
}
|
|
627
|
+
>
|
|
628
|
+
{uploadStatus[f.id] === "uploading" && "Uploading..."}
|
|
629
|
+
{uploadStatus[f.id] === "success" && "✓ Uploaded"}
|
|
630
|
+
{uploadStatus[f.id] === "error" && "✗ Failed"}
|
|
631
|
+
</Typography>
|
|
632
|
+
</div>
|
|
633
|
+
))}
|
|
634
|
+
</div>
|
|
635
|
+
)}
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### With pre-loaded server files
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
import { useState, useEffect } from "react";
|
|
645
|
+
import {
|
|
646
|
+
FileUploadV2,
|
|
647
|
+
FileWithMetadata,
|
|
648
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
649
|
+
|
|
650
|
+
function PreloadedFilesExample() {
|
|
651
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
652
|
+
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
// Simulate fetching files from server
|
|
655
|
+
const loadServerFiles = async () => {
|
|
656
|
+
const response = await fetch("/api/files");
|
|
657
|
+
const serverFiles = await response.json();
|
|
658
|
+
|
|
659
|
+
// Convert server files to FileWithMetadata
|
|
660
|
+
const filesWithMetadata: FileWithMetadata[] = serverFiles.map(
|
|
661
|
+
(sf: any) => ({
|
|
662
|
+
id: sf.id,
|
|
663
|
+
file: new File([""], sf.name, { type: sf.mimeType }),
|
|
664
|
+
metadata: {
|
|
665
|
+
serverUrl: sf.url,
|
|
666
|
+
uploadedAt: sf.uploadedAt,
|
|
667
|
+
size: sf.size,
|
|
668
|
+
},
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
setFiles(filesWithMetadata);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
loadServerFiles();
|
|
676
|
+
}, []);
|
|
677
|
+
|
|
678
|
+
const handleFilesRemove = async (removedFiles: FileWithMetadata[]) => {
|
|
679
|
+
// Delete from server
|
|
680
|
+
await Promise.all(
|
|
681
|
+
removedFiles.map((f) =>
|
|
682
|
+
fetch(`/api/files/${f.id}`, { method: "DELETE" }),
|
|
683
|
+
),
|
|
684
|
+
);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<FileUploadV2
|
|
689
|
+
selectedFiles={files}
|
|
690
|
+
onFilesSelect={setFiles}
|
|
691
|
+
onFilesRemove={handleFilesRemove}
|
|
692
|
+
multiple
|
|
693
|
+
className="adm:max-w-xl"
|
|
694
|
+
/>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### With custom ID generator
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
import { useState } from "react";
|
|
703
|
+
import {
|
|
704
|
+
FileUploadV2,
|
|
705
|
+
FileWithMetadata,
|
|
706
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
707
|
+
import { v4 as uuidv4 } from "uuid";
|
|
708
|
+
|
|
709
|
+
function CustomIdExample() {
|
|
710
|
+
const [file, setFile] = useState<FileWithMetadata | null>(null);
|
|
711
|
+
|
|
712
|
+
// Use UUID for file IDs
|
|
713
|
+
const generateUUID = () => uuidv4();
|
|
714
|
+
|
|
715
|
+
return (
|
|
716
|
+
<FileUploadV2
|
|
717
|
+
selectedFile={file}
|
|
718
|
+
onFileSelect={setFile}
|
|
719
|
+
generateId={generateUUID}
|
|
720
|
+
className="adm:max-w-xl"
|
|
721
|
+
/>
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### With form integration and validation
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
import { useState } from "react";
|
|
730
|
+
import {
|
|
731
|
+
FileUploadV2,
|
|
732
|
+
FileWithMetadata,
|
|
733
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
734
|
+
import { Field, FieldLabel, FieldError } from "@adamosuiteservices/ui/field";
|
|
735
|
+
import { Button } from "@adamosuiteservices/ui/button";
|
|
736
|
+
|
|
737
|
+
function FormIntegrationExample() {
|
|
738
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
739
|
+
const [error, setError] = useState<string>("");
|
|
740
|
+
const [uploadedFileIds, setUploadedFileIds] = useState<string[]>([]);
|
|
741
|
+
|
|
742
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
743
|
+
e.preventDefault();
|
|
744
|
+
|
|
745
|
+
if (files.length === 0) {
|
|
746
|
+
setError("Please select at least one file");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
setError("");
|
|
751
|
+
|
|
752
|
+
// Upload files that haven't been uploaded yet
|
|
753
|
+
const filesToUpload = files.filter((f) => !uploadedFileIds.includes(f.id));
|
|
754
|
+
|
|
755
|
+
if (filesToUpload.length > 0) {
|
|
756
|
+
try {
|
|
757
|
+
await Promise.all(
|
|
758
|
+
filesToUpload.map(async (f) => {
|
|
759
|
+
const formData = new FormData();
|
|
760
|
+
formData.append("file", f.file);
|
|
761
|
+
formData.append("fileId", f.id);
|
|
762
|
+
|
|
763
|
+
await fetch("/api/upload", {
|
|
764
|
+
method: "POST",
|
|
765
|
+
body: formData,
|
|
766
|
+
});
|
|
767
|
+
}),
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
setUploadedFileIds((prev) => [
|
|
771
|
+
...prev,
|
|
772
|
+
...filesToUpload.map((f) => f.id),
|
|
773
|
+
]);
|
|
774
|
+
alert("Files uploaded successfully!");
|
|
775
|
+
} catch (error) {
|
|
776
|
+
setError("Upload failed. Please try again.");
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
alert("All files already uploaded!");
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
return (
|
|
784
|
+
<form onSubmit={handleSubmit} className="adm:max-w-xl adm:space-y-4">
|
|
785
|
+
<Field>
|
|
786
|
+
<FieldLabel>Upload documents</FieldLabel>
|
|
787
|
+
<FileUploadV2
|
|
788
|
+
selectedFiles={files}
|
|
789
|
+
onFilesSelect={(newFiles) => {
|
|
790
|
+
setFiles(newFiles);
|
|
791
|
+
setError("");
|
|
792
|
+
}}
|
|
793
|
+
aria-invalid={!!error}
|
|
794
|
+
multiple
|
|
795
|
+
/>
|
|
796
|
+
{error && <FieldError>{error}</FieldError>}
|
|
797
|
+
</Field>
|
|
798
|
+
|
|
799
|
+
<Button type="submit">Submit form</Button>
|
|
800
|
+
</form>
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### With event tracking
|
|
806
|
+
|
|
807
|
+
```tsx
|
|
808
|
+
import { useState } from "react";
|
|
809
|
+
import {
|
|
810
|
+
FileUploadV2,
|
|
811
|
+
FileWithMetadata,
|
|
812
|
+
} from "@adamosuiteservices/ui/file-upload";
|
|
813
|
+
import { Typography } from "@adamosuiteservices/ui/typography";
|
|
814
|
+
|
|
815
|
+
function EventTrackingExample() {
|
|
816
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
817
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
818
|
+
|
|
819
|
+
const logEvent = (message: string) => {
|
|
820
|
+
setEvents((prev) => [
|
|
821
|
+
...prev,
|
|
822
|
+
`[${new Date().toLocaleTimeString()}] ${message}`,
|
|
823
|
+
]);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const handleFilesAdd = (addedFiles: FileWithMetadata[]) => {
|
|
827
|
+
logEvent(
|
|
828
|
+
`Added ${addedFiles.length} file(s): ${addedFiles.map((f) => `${f.file.name} (ID: ${f.id})`).join(", ")}`,
|
|
829
|
+
);
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
const handleFilesRemove = (removedFiles: FileWithMetadata[]) => {
|
|
833
|
+
logEvent(
|
|
834
|
+
`Removed ${removedFiles.length} file(s): ${removedFiles.map((f) => `${f.file.name} (ID: ${f.id})`).join(", ")}`,
|
|
835
|
+
);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
return (
|
|
839
|
+
<div className="adm:max-w-xl adm:space-y-4">
|
|
840
|
+
<FileUploadV2
|
|
841
|
+
selectedFiles={files}
|
|
842
|
+
onFilesSelect={setFiles}
|
|
843
|
+
onFilesAdd={handleFilesAdd}
|
|
844
|
+
onFilesRemove={handleFilesRemove}
|
|
845
|
+
multiple
|
|
846
|
+
/>
|
|
847
|
+
|
|
848
|
+
{events.length > 0 && (
|
|
849
|
+
<div className="adm:space-y-2">
|
|
850
|
+
<Typography className="adm:font-medium">Event log:</Typography>
|
|
851
|
+
<div className="adm:max-h-40 adm:overflow-y-auto adm:rounded-lg adm:border adm:bg-muted adm:p-3">
|
|
852
|
+
{events.map((event, index) => (
|
|
853
|
+
<Typography
|
|
854
|
+
key={index}
|
|
855
|
+
className="adm:text-sm adm:font-mono"
|
|
856
|
+
color="muted"
|
|
857
|
+
>
|
|
858
|
+
{event}
|
|
859
|
+
</Typography>
|
|
860
|
+
))}
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
)}
|
|
864
|
+
</div>
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
## Comparison with FileUpload
|
|
870
|
+
|
|
871
|
+
| Feature | FileUpload (original) | FileUploadV2 |
|
|
872
|
+
| ---------------------- | --------------------- | ---------------------- |
|
|
873
|
+
| File type | `File` | `FileWithMetadata` |
|
|
874
|
+
| Unique IDs | ❌ No | ✅ Yes (auto) |
|
|
875
|
+
| Custom ID generator | ❌ No | ✅ Yes (generateId) |
|
|
876
|
+
| Metadata support | ❌ No | ✅ Yes (optional) |
|
|
877
|
+
| Server integration | ⚠️ Manual | ✅ Built-in with IDs |
|
|
878
|
+
| Pre-loaded files | ⚠️ No IDs | ✅ With server IDs |
|
|
879
|
+
| Delete API calls | ⚠️ No file reference | ✅ Uses file.id |
|
|
880
|
+
| Async upload tracking | ⚠️ Manual tracking | ✅ Track by file.id |
|
|
881
|
+
| Callbacks with IDs | ❌ No | ✅ Yes |
|
|
882
|
+
| Use case | Simple forms | Server integration |
|
|
883
|
+
| Backward compatibility | ✅ Original | ❌ Different file type |
|
|
884
|
+
|
|
885
|
+
## TypeScript types
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
export type FileWithMetadata = {
|
|
889
|
+
id: string;
|
|
890
|
+
file: File;
|
|
891
|
+
metadata?: Record<string, unknown>;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
export type FileUploadV2Labels = {
|
|
895
|
+
dragDrop?: string;
|
|
896
|
+
selectFile?: string;
|
|
897
|
+
fileRequirements?: string;
|
|
898
|
+
filesSelected?: (count: number) => string;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
export type FileUploadV2Props = ComponentProps<"div"> &
|
|
902
|
+
Readonly<{
|
|
903
|
+
// Simple mode
|
|
904
|
+
selectedFile?: FileWithMetadata | null;
|
|
905
|
+
onFileSelect?: (file: FileWithMetadata | null) => void;
|
|
906
|
+
|
|
907
|
+
// Multiple mode
|
|
908
|
+
selectedFiles?: FileWithMetadata[];
|
|
909
|
+
onFilesSelect?: (files: FileWithMetadata[]) => void;
|
|
910
|
+
multiple?: boolean;
|
|
911
|
+
maxFiles?: number;
|
|
912
|
+
filesPosition?: "above" | "below";
|
|
913
|
+
|
|
914
|
+
// Callbacks
|
|
915
|
+
onFileAdd?: (file: FileWithMetadata) => void;
|
|
916
|
+
onFileRemove?: (file: FileWithMetadata) => void;
|
|
917
|
+
onFilesAdd?: (files: FileWithMetadata[]) => void;
|
|
918
|
+
onFilesRemove?: (files: FileWithMetadata[]) => void;
|
|
919
|
+
|
|
920
|
+
// ID generation
|
|
921
|
+
generateId?: (file: File) => string;
|
|
922
|
+
|
|
923
|
+
// Validation
|
|
924
|
+
onInvalidFile?: (
|
|
925
|
+
file: FileWithMetadata,
|
|
926
|
+
reason: "extension" | "size",
|
|
927
|
+
) => void;
|
|
928
|
+
invalid?: boolean;
|
|
929
|
+
"aria-invalid"?: boolean;
|
|
930
|
+
|
|
931
|
+
// States
|
|
932
|
+
disabled?: boolean;
|
|
933
|
+
|
|
934
|
+
// Common
|
|
935
|
+
acceptedExtensions?: string[];
|
|
936
|
+
maxSizeInMB?: number;
|
|
937
|
+
labels?: FileUploadV2Labels;
|
|
938
|
+
input?: ComponentProps<"input">;
|
|
939
|
+
}>;
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
## Default ID generation
|
|
943
|
+
|
|
944
|
+
The default ID generator creates unique IDs using the following format:
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
const defaultGenerateId = (file: File) => {
|
|
948
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}-${file.name}`;
|
|
949
|
+
};
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Format**: `{timestamp}-{random}-{filename}`
|
|
953
|
+
|
|
954
|
+
**Example**: `1704892800000-x7k2p9q3m-report.xlsx`
|
|
955
|
+
|
|
956
|
+
**Components**:
|
|
957
|
+
|
|
958
|
+
- **timestamp**: Current timestamp in milliseconds
|
|
959
|
+
- **random**: Random alphanumeric string (9 characters)
|
|
960
|
+
- **filename**: Original file name
|
|
961
|
+
|
|
962
|
+
This ensures uniqueness even when uploading the same file multiple times.
|
|
963
|
+
|
|
964
|
+
## Backend integration
|
|
965
|
+
|
|
966
|
+
### Upload with file ID
|
|
967
|
+
|
|
968
|
+
```tsx
|
|
969
|
+
const uploadFile = async (fileWithMetadata: FileWithMetadata) => {
|
|
970
|
+
const formData = new FormData();
|
|
971
|
+
formData.append("file", fileWithMetadata.file);
|
|
972
|
+
formData.append("fileId", fileWithMetadata.id);
|
|
973
|
+
formData.append(
|
|
974
|
+
"metadata",
|
|
975
|
+
JSON.stringify({
|
|
976
|
+
uploadedAt: new Date().toISOString(),
|
|
977
|
+
originalName: fileWithMetadata.file.name,
|
|
978
|
+
}),
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const response = await fetch("/api/upload", {
|
|
982
|
+
method: "POST",
|
|
983
|
+
body: formData,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
return response.json();
|
|
987
|
+
};
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
### Delete from server
|
|
991
|
+
|
|
992
|
+
```tsx
|
|
993
|
+
const deleteFile = async (fileWithMetadata: FileWithMetadata) => {
|
|
994
|
+
await fetch(`/api/files/${fileWithMetadata.id}`, {
|
|
995
|
+
method: "DELETE",
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### Update file metadata after upload
|
|
1001
|
+
|
|
1002
|
+
```tsx
|
|
1003
|
+
const handleFileAdd = async (fileWithMetadata: FileWithMetadata) => {
|
|
1004
|
+
const response = await uploadFile(fileWithMetadata);
|
|
1005
|
+
|
|
1006
|
+
// Update file with server metadata
|
|
1007
|
+
setFiles((prev) =>
|
|
1008
|
+
prev.map((f) =>
|
|
1009
|
+
f.id === fileWithMetadata.id
|
|
1010
|
+
? {
|
|
1011
|
+
...f,
|
|
1012
|
+
metadata: {
|
|
1013
|
+
serverUrl: response.url,
|
|
1014
|
+
uploadedAt: response.uploadedAt,
|
|
1015
|
+
},
|
|
1016
|
+
}
|
|
1017
|
+
: f,
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
};
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
## Best practices
|
|
1024
|
+
|
|
1025
|
+
### Do
|
|
1026
|
+
|
|
1027
|
+
```tsx
|
|
1028
|
+
// Use FileUploadV2 when you need server-side tracking
|
|
1029
|
+
<FileUploadV2 selectedFile={file} onFileSelect={setFile} />;
|
|
1030
|
+
|
|
1031
|
+
// Always handle file removal for uploaded files
|
|
1032
|
+
const handleFileRemove = async (file: FileWithMetadata) => {
|
|
1033
|
+
if (file.metadata?.serverUrl) {
|
|
1034
|
+
await deleteFromServer(file.id);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
// Store server metadata after successful upload
|
|
1039
|
+
const handleFileAdd = async (file: FileWithMetadata) => {
|
|
1040
|
+
const response = await uploadFile(file);
|
|
1041
|
+
updateFileMetadata(file.id, { serverUrl: response.url });
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
// Use custom ID generator for specific requirements
|
|
1045
|
+
<FileUploadV2
|
|
1046
|
+
selectedFile={file}
|
|
1047
|
+
onFileSelect={setFile}
|
|
1048
|
+
generateId={(file) => uuidv4()}
|
|
1049
|
+
/>;
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### Don't
|
|
1053
|
+
|
|
1054
|
+
```tsx
|
|
1055
|
+
// Don't use FileUploadV2 for simple forms without server integration
|
|
1056
|
+
// Use FileUpload instead
|
|
1057
|
+
<FileUploadV2 selectedFile={file} onFileSelect={setFile} /> // Overkill
|
|
1058
|
+
|
|
1059
|
+
// Don't modify file.id after creation
|
|
1060
|
+
file.id = "new-id"; // ❌ IDs should be immutable
|
|
1061
|
+
|
|
1062
|
+
// Don't forget to clean up server files on remove
|
|
1063
|
+
const handleFileRemove = (file: FileWithMetadata) => {
|
|
1064
|
+
// ❌ Missing server deletion
|
|
1065
|
+
console.log("Removed:", file.id);
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
// Don't store sensitive data in metadata (it's client-side)
|
|
1069
|
+
metadata: {
|
|
1070
|
+
password: "secret123", // ❌ Insecure
|
|
1071
|
+
apiKey: "key", // ❌ Insecure
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
## Notes
|
|
1076
|
+
|
|
1077
|
+
- The component uses the same UI as FileUpload but works with `FileWithMetadata` instead of `File`
|
|
1078
|
+
- IDs are generated automatically when files are added (drag & drop or file selection)
|
|
1079
|
+
- IDs are guaranteed unique within the component instance
|
|
1080
|
+
- For server integration, use the file ID to track uploads and deletions
|
|
1081
|
+
- Metadata is optional and can store any additional information
|
|
1082
|
+
- Pre-loaded files from server should include their server-assigned IDs
|
|
1083
|
+
- The component is fully controlled - the parent manages file state
|
|
1084
|
+
- Use `generateId` prop for custom ID generation strategies
|
|
1085
|
+
- All validation features from FileUpload are preserved
|
|
1086
|
+
|
|
1087
|
+
## Migration from FileUpload
|
|
1088
|
+
|
|
1089
|
+
If you're using FileUpload and need file IDs:
|
|
1090
|
+
|
|
1091
|
+
**Before** (FileUpload):
|
|
1092
|
+
|
|
1093
|
+
```tsx
|
|
1094
|
+
const [files, setFiles] = useState<File[]>([]);
|
|
1095
|
+
|
|
1096
|
+
<FileUpload selectedFiles={files} onFilesSelect={setFiles} multiple />;
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
**After** (FileUploadV2):
|
|
1100
|
+
|
|
1101
|
+
```tsx
|
|
1102
|
+
const [files, setFiles] = useState<FileWithMetadata[]>([]);
|
|
1103
|
+
|
|
1104
|
+
<FileUploadV2 selectedFiles={files} onFilesSelect={setFiles} multiple />;
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Key changes**:
|
|
1108
|
+
|
|
1109
|
+
1. Change type from `File[]` to `FileWithMetadata[]`
|
|
1110
|
+
2. Update component from `FileUpload` to `FileUploadV2`
|
|
1111
|
+
3. Access the native File object via `fileWithMetadata.file`
|
|
1112
|
+
4. Use `fileWithMetadata.id` for server operations
|
|
1113
|
+
5. Store server data in `fileWithMetadata.metadata`
|