Skip to content

Commit ee7c96d

Browse files
armaang1729Armaan Gupta
andauthored
fixed the fileUpload component and also added the test cases (#159)
Co-authored-by: Armaan Gupta <armaang@Armaans-MacBook-Pro.local>
1 parent 5f65bc9 commit ee7c96d

File tree

2 files changed

+146
-14
lines changed

2 files changed

+146
-14
lines changed

packages/react-vanilla-components/__tests__/components/FileUpload.test.tsx

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,107 @@ describe("File Upload", () => {
311311
expect(input).toHaveLength(1);
312312
expect(input[0]).toHaveAttribute('aria-describedby', `${f.id}__longdescription ${f.id}__shortdescription`)
313313
})
314-
});
314+
315+
test("should allow re-uploading same file after removal", async () => {
316+
const f = {
317+
...field,
318+
type: "file",
319+
};
320+
const { renderResponse } = await helper(f);
321+
322+
// Get the file input element directly (like other tests do)
323+
const input = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__widget");
324+
325+
// 1. Upload initial file
326+
const file = new File(["(⌐□_□)"], "test-document.pdf", { type: "application/pdf" });
327+
userEvent.upload(input[0] as HTMLInputElement, file);
328+
329+
// Verify file was uploaded
330+
expect(renderResponse.queryByText("test-document.pdf")).toBeTruthy();
331+
332+
// 2. Remove the file using the remove button
333+
const removeButton = renderResponse.getByLabelText("Remove file");
334+
userEvent.click(removeButton);
335+
336+
// Verify file was removed
337+
expect(renderResponse.queryByText("test-document.pdf")).toBeFalsy();
338+
339+
// 3. Re-upload the same file (this tests the bug fix)
340+
const newFile = new File(["(⌐□_□)"], "test-document.pdf", { type: "application/pdf" });
341+
userEvent.upload(input[0] as HTMLInputElement, newFile);
342+
343+
// 4. Verify the same file appears correctly after re-upload
344+
expect(renderResponse.queryByText("test-document.pdf")).toBeTruthy();
345+
});
346+
347+
test("should handle duplicate filenames correctly without removing wrong file", async () => {
348+
const f = {
349+
...field,
350+
type: "file[]",
351+
};
352+
const { renderResponse } = await helper(f);
353+
354+
const input = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__widget");
355+
356+
// Upload first document
357+
const largerContent = Array(100).fill('(⌐□_□)').join('');
358+
const document1 = new File([largerContent], "document.pdf", { type: "application/pdf" });
359+
userEvent.upload(input[0] as HTMLInputElement, document1);
360+
361+
// Upload image file to create separation between duplicate names
362+
const imageFile = new File(["image data"], "image.jpg", { type: "image/jpeg" });
363+
userEvent.upload(input[0] as HTMLInputElement, imageFile);
364+
365+
// Upload second document with same filename but different content
366+
const smallerContent = '(⌐□_□)';
367+
const document2 = new File([smallerContent], "document.pdf", { type: "application/pdf" });
368+
userEvent.upload(input[0] as HTMLInputElement, document2);
369+
370+
// Verify all files are present
371+
const fileItems = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__fileitem");
372+
expect(fileItems).toHaveLength(3);
373+
374+
const fileSizes = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__filesize");
375+
expect(fileSizes).toHaveLength(3);
376+
const sizeTexts = Array.from(fileSizes).map(el => el.textContent);
377+
expect(sizeTexts.filter(size => size !== sizeTexts[0])).toHaveLength(2);
378+
379+
expect(renderResponse.queryByText("image.jpg")).toBeTruthy();
380+
381+
const removeButtons = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__filedelete");
382+
expect(removeButtons).toHaveLength(3);
383+
384+
// Remove the first uploaded document
385+
userEvent.click(removeButtons[0] as HTMLButtonElement);
386+
387+
// Verify exactly 2 files remain
388+
const remainingFileItems = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__fileitem");
389+
expect(remainingFileItems).toHaveLength(2);
390+
391+
const remainingFileSizes = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__filesize");
392+
const remainingSizeTexts = Array.from(remainingFileSizes).map(el => el.textContent);
393+
expect(remainingSizeTexts).toHaveLength(2);
394+
395+
expect(renderResponse.queryByText("image.jpg")).toBeTruthy();
396+
expect(renderResponse.queryByText("document.pdf")).toBeTruthy();
397+
398+
// Remove the image file to verify the second document remains
399+
const updatedRemoveButtons = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__filedelete");
400+
expect(updatedRemoveButtons).toHaveLength(2);
401+
402+
const imageRemoveButton = Array.from(updatedRemoveButtons).find((button) => {
403+
const fileItem = button.closest('.cmp-adaptiveform-fileinput__fileitem');
404+
const fileName = fileItem?.querySelector('.cmp-adaptiveform-fileinput__filename')?.textContent;
405+
return fileName === 'image.jpg';
406+
});
407+
userEvent.click(imageRemoveButton as HTMLButtonElement);
408+
409+
// Verify only the second document remains
410+
const finalFileItems = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__fileitem");
411+
expect(finalFileItems).toHaveLength(1);
412+
413+
const finalFileSizes = renderResponse.container.getElementsByClassName("cmp-adaptiveform-fileinput__filesize");
414+
expect(finalFileSizes).toHaveLength(1);
415+
expect(renderResponse.queryByText("image.jpg")).toBeFalsy();
416+
});
417+
});

packages/react-vanilla-components/src/components/FileUpload.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,29 @@ const FileUpload = (props: PROPS) => {
4141
properties,
4242
valid
4343
} = props;
44+
type LocalFile = { uid: string, file: File | FileObject };
45+
// Generate a simple unique id for each file instance
46+
const generateUid = () => `${Date.now()}-${Math.random().toString(36).slice(2)}`;
47+
48+
const wrapWithUid = (items: Array<File | FileObject> | null | undefined): LocalFile[] => {
49+
const list = items && (items instanceof Array ? items : [items]);
50+
return (list || []).map((f) => ({
51+
uid: generateUid(),
52+
file: f
53+
}));
54+
};
55+
4456
let val = value && (value instanceof Array ? value : [value]);
45-
const [files, setFiles] = useState<FileObject[]>(val || []);
57+
const [files, setFiles] = useState<LocalFile[]>(wrapWithUid(val as Array<File | FileObject>) || []);
4658
const [ dragOver, setDragOver ] = useState(false);
4759

4860
const maxFileSizeInBytes = getFileSizeInBytes(maxFileSize);
4961
let multiple = props.type?.endsWith('[]') ? { multiple: true } : {};
5062

63+
// Dispatch value to the model. When field supports multiple values, send array; otherwise a single item
5164
const fileChangeHandler = useCallback(
52-
(files: Array<File | FileObject>) => {
65+
(localFiles: Array<LocalFile>) => {
66+
const files = localFiles.map(({ file }) => file);
5367
if (multiple) {
5468
props.dispatchChange(files);
5569
} else {
@@ -69,30 +83,44 @@ const FileUpload = (props: PROPS) => {
6983
setDragOver(false);
7084
};
7185

86+
// Handles file selection via input, drag/drop, or paste
7287
const fileUploadHandler = useCallback((e) => {
7388
e.preventDefault();
7489
const newFiles = Array.from<File>(e.dataTransfer?.files || e?.target?.files || e.clipboardData?.files || []);
90+
// Clear the input value to allow re-uploading the same file again
91+
if (e.target && e.target.type === 'file') {
92+
e.target.value = '';
93+
}
7594
if (newFiles?.length) {
7695
const validFiles = newFiles.filter((file: File) => file.size <= maxFileSizeInBytes);
7796
if (validFiles.length < newFiles.length) {
7897
// Show constraint message for files with size exceeding the limit
7998
alert(`${props.constraintMessages?.maxFileSize}`);
8099
}
81-
const updatedFiles = [...files, ...validFiles];
82-
setFiles(updatedFiles as FileObject[]);
100+
// Create new file entries with unique UIDs
101+
const wrappedNew = validFiles.map((f) => ({ uid: generateUid(), file: f }));
102+
const updatedFiles: LocalFile[] = [...files, ...wrappedNew];
103+
setFiles(updatedFiles);
83104
fileChangeHandler(updatedFiles);
84105
}
85106
setDragOver(false);
86107
},
87108
[files, fileChangeHandler, maxFileSizeInBytes, props?.constraintMessages]
88109
);
89110

111+
// Removes one file by its unique id and clears the input to allow re-uploading the same file
90112
const removeFile = useCallback(
91-
(index: number) => {
92-
const fileList = [...files];
113+
(uid: string) => {
114+
const fileList = [...files];
115+
const index = files.findIndex((f) => f.uid === uid);
116+
if (index === -1) {return;}
93117
fileList.splice(index,1);
94118
setFiles(fileList);
95119
fileChangeHandler(fileList);
120+
// Clear the input value so the same file can be selected again
121+
if (fileInputField.current) {
122+
(fileInputField.current as HTMLInputElement).value = '';
123+
}
96124
},
97125
[files, fileChangeHandler]
98126
);
@@ -156,25 +184,26 @@ const FileUpload = (props: PROPS) => {
156184
</div>
157185
<ul className="cmp-adaptiveform-fileinput__filelist">
158186
{files &&
159-
files?.map((item: FileObject, index) => (
187+
files?.map(({ file, uid }) => (
160188
<li
161189
className="cmp-adaptiveform-fileinput__fileitem"
162-
key={item?.name}
190+
key={uid}
163191
>
164192
<span
165193
className="cmp-adaptiveform-fileinput__filename"
166-
aria-label={item?.name}
194+
aria-label={(file)?.name}
167195
>
168-
{item?.name}
196+
{(file)?.name}
169197
</span>
170198
<span className="cmp-adaptiveform-fileinput__fileendcontainer">
171199
<span className="cmp-adaptiveform-fileinput__filesize">
172-
{formatBytes(item?.size)}
200+
{formatBytes((file)?.size)}
173201
</span>
174202
<button
175-
onClick={() => removeFile(index)}
203+
type="button"
204+
onClick={() => removeFile(uid)}
176205
className="cmp-adaptiveform-fileinput__filedelete"
177-
role="button"
206+
aria-label="Remove file"
178207
>
179208
x
180209
</button>

0 commit comments

Comments
 (0)