From 2153fa937db0794e0bc4633c772915bd8b8b9109 Mon Sep 17 00:00:00 2001 From: -l Date: Thu, 5 Dec 2024 13:36:22 +0100 Subject: [PATCH 1/5] fix(Field.Upload): improves multiple async uploads --- .../extensions/forms/Field/Upload/Upload.tsx | 33 ++++++-- .../Field/Upload/__tests__/Upload.test.tsx | 78 +++++++++++++++++++ .../Field/Upload/stories/Upload.stories.tsx | 57 ++++++++++++++ 3 files changed, 160 insertions(+), 8 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx index 08dae3728d1..af47de91273 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import classnames from 'classnames' import FieldBlock, { Props as FieldBlockProps, @@ -117,6 +117,12 @@ function UploadComponent(props: Props) { const { files: fileContext, setFiles } = useUpload(id) + const filesRef = useRef(null) + + useEffect(() => { + filesRef.current = fileContext + }, [fileContext]) + useEffect(() => { // Files stored in session storage will not have a property (due to serialization). const hasInvalidFiles = value?.some(({ file }) => !file?.name) @@ -126,10 +132,10 @@ function UploadComponent(props: Props) { }, [setFiles, value]) const handleChangeAsync = useCallback( - async (files: UploadValue) => { + async (existingFiles: UploadValue) => { // Filter out existing files const existingFileIds = fileContext?.map((file) => file.id) || [] - const newFiles = files.filter( + const newFiles = existingFiles.filter( (file) => !existingFileIds.includes(file.id) ) @@ -140,15 +146,26 @@ function UploadComponent(props: Props) { ...updateFileLoadingState(newFiles, { isLoading: true }), ]) - const uploadedFiles = updateFileLoadingState( - await fileHandler(newFiles), - { isLoading: false } + const incomingFiles = await fileHandler(newFiles) + + const uploadedFiles = updateFileLoadingState(incomingFiles, { + isLoading: false, + }) + + const indexOfFirstNewFile = filesRef.current.findIndex( + ({ id }) => id === newFiles[0].id ) + const updatedFiles = [ + ...filesRef.current.slice(0, indexOfFirstNewFile), + ...uploadedFiles, + ...filesRef.current.slice(indexOfFirstNewFile + newFiles.length), + ] + // Set error, if any - handleChange([...fileContext, ...uploadedFiles]) + handleChange(updatedFiles) } else { - handleChange(files) + handleChange(existingFiles) } }, [fileContext, setFiles, fileHandler, handleChange] diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx index 97488363022..44bf3c49250 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx @@ -1154,6 +1154,84 @@ describe('Field.Upload', () => { }) }) + it.only('should add new files from fileHandler with async function with multiple actions', async () => { + const newFile1 = createMockFile( + 'fileName-new-1.png', + 100, + 'image/png' + ) + const newFile2 = createMockFile( + 'fileName-new-2.png', + 100, + 'image/png' + ) + const newFile3 = createMockFile( + 'fileName-new-3.png', + 100, + 'image/png' + ) + const files = { newFile1, newFile2, newFile3 } + + const asyncValidatorResolvingWithSuccess = (id) => + new Promise((resolve) => + setTimeout( + () => + resolve([ + { + file: files[`newFile${id}`], + id: 'server_generated_id_' + id, + exists: false, + }, + ]), + 1 + ) + ) + + const asyncFileHandlerFnSuccess = jest + .fn(asyncValidatorResolvingWithSuccess) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(1)) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(2)) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(3)) + + render() + + const element = getRootElement() + + await waitFor(() => { + fireEvent.drop(element, { + dataTransfer: { + files: [newFile1], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [newFile2], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [newFile3], + }, + }) + + expect( + document.querySelectorAll('.dnb-upload__file-cell').length + ).toBe(3) + + expect( + screen.queryByText('fileName-new-1.png') + ).toBeInTheDocument() + expect( + screen.queryByText('fileName-new-2.png') + ).toBeInTheDocument() + expect( + screen.queryByText('fileName-new-3.png') + ).toBeInTheDocument() + }) + }) + it('should not add existing file using fileHandler with async function', async () => { const file = createMockFile('fileName.png', 100, 'image/png') diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx index 1c52a1c6557..1595a55a204 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/stories/Upload.stories.tsx @@ -96,3 +96,60 @@ export const WithAsyncFileHandler = () => { ) } + +export const AsyncEverything = () => { + const acceptedFileTypes = ['jpg', 'pdf', 'png'] + + async function mockAsyncFileRemoval({ fileItem }) { + const request = createRequest() + console.log('making API request to remove: ' + fileItem.file.name) + await request(3000) // Simulate a request + } + + async function mockAsyncFileUpload( + newFiles: UploadValue + ): Promise { + const updatedFiles: UploadValue = [] + + for (const [, file] of Object.entries(newFiles)) { + const formData = new FormData() + formData.append('file', file.file, file.file.name) + + const request = createRequest() + await request(3000) // Simulate a request + + const mockResponse = { + ok: false, // Fails virus check + json: async () => ({ + server_generated_id: + 'server_generated_id' + + '_' + + file.file.name + + '_' + + crypto.randomUUID(), + }), + } + + const data = await mockResponse.json() + updatedFiles.push({ + ...file, + id: data.server_generated_id, + }) + } + + return updatedFiles + } + + return ( + console.log(form)}> + + + + + ) +} From eec9b68576e003f719a30f3f5723605126c0dce4 Mon Sep 17 00:00:00 2001 From: Anders Date: Thu, 5 Dec 2024 13:38:47 +0100 Subject: [PATCH 2/5] Update packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx --- .../src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx index 44bf3c49250..61b0939cd4a 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx @@ -1154,7 +1154,7 @@ describe('Field.Upload', () => { }) }) - it.only('should add new files from fileHandler with async function with multiple actions', async () => { + it('should add new files from fileHandler with async function with multiple actions', async () => { const newFile1 = createMockFile( 'fileName-new-1.png', 100, From 9d76bb79f2cf9468d5867b215f7342de3449980b Mon Sep 17 00:00:00 2001 From: Anders Date: Thu, 5 Dec 2024 20:23:30 +0100 Subject: [PATCH 3/5] Update packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tobias Høegh --- .../dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx index af47de91273..71b754e9aa6 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -117,7 +117,7 @@ function UploadComponent(props: Props) { const { files: fileContext, setFiles } = useUpload(id) - const filesRef = useRef(null) + const filesRef = useRef>() useEffect(() => { filesRef.current = fileContext From 1c7cc70aaf1b59057c3c8619a2a0499885ababcd Mon Sep 17 00:00:00 2001 From: -l Date: Fri, 6 Dec 2024 11:32:32 +0100 Subject: [PATCH 4/5] updates test --- .../Field/Upload/__tests__/Upload.test.tsx | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx index 61b0939cd4a..e526293dfb6 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx @@ -1155,22 +1155,18 @@ describe('Field.Upload', () => { }) it('should add new files from fileHandler with async function with multiple actions', async () => { - const newFile1 = createMockFile( - 'fileName-new-1.png', - 100, - 'image/png' - ) - const newFile2 = createMockFile( - 'fileName-new-2.png', - 100, - 'image/png' - ) - const newFile3 = createMockFile( - 'fileName-new-3.png', - 100, - 'image/png' - ) - const files = { newFile1, newFile2, newFile3 } + const newFile = (fileId) => { + return createMockFile(`${fileId}.png`, 100, 'image/png') + } + + const files = [ + newFile(0), + newFile(1), + newFile(2), + newFile(3), + newFile(4), + newFile(5), + ] const asyncValidatorResolvingWithSuccess = (id) => new Promise((resolve) => @@ -1178,7 +1174,7 @@ describe('Field.Upload', () => { () => resolve([ { - file: files[`newFile${id}`], + file: files[id], id: 'server_generated_id_' + id, exists: false, }, @@ -1187,11 +1183,15 @@ describe('Field.Upload', () => { ) ) + const asyncValidatorNeverResolving = () => + new Promise(() => {}) + const asyncFileHandlerFnSuccess = jest .fn(asyncValidatorResolvingWithSuccess) - .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(1)) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(0)) + .mockReturnValueOnce(asyncValidatorNeverResolving()) .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(2)) - .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(3)) + .mockReturnValueOnce(asyncValidatorNeverResolving()) render() @@ -1200,35 +1200,42 @@ describe('Field.Upload', () => { await waitFor(() => { fireEvent.drop(element, { dataTransfer: { - files: [newFile1], + files: [files[0]], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [files[1], files[3], files[4]], }, }) fireEvent.drop(element, { dataTransfer: { - files: [newFile2], + files: [files[2]], }, }) fireEvent.drop(element, { dataTransfer: { - files: [newFile3], + files: [files[5]], }, }) expect( document.querySelectorAll('.dnb-upload__file-cell').length - ).toBe(3) + ).toBe(6) + + expect(screen.queryByText('0.png')).toBeInTheDocument() + expect(screen.queryByText('1.png')).not.toBeInTheDocument() + expect(screen.queryByText('2.png')).toBeInTheDocument() + expect(screen.queryByText('3.png')).not.toBeInTheDocument() + expect(screen.queryByText('4.png')).not.toBeInTheDocument() + expect(screen.queryByText('5.png')).not.toBeInTheDocument() expect( - screen.queryByText('fileName-new-1.png') - ).toBeInTheDocument() - expect( - screen.queryByText('fileName-new-2.png') - ).toBeInTheDocument() - expect( - screen.queryByText('fileName-new-3.png') - ).toBeInTheDocument() + document.querySelectorAll('.dnb-progress-indicator').length + ).toBe(4) }) }) From 5fd43d96c630c1707f9d8ab80215ad8eff508678 Mon Sep 17 00:00:00 2001 From: Anders Date: Fri, 6 Dec 2024 11:39:10 +0100 Subject: [PATCH 5/5] Update packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx --- .../src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx index e526293dfb6..a2a6a8d9879 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/__tests__/Upload.test.tsx @@ -1184,7 +1184,7 @@ describe('Field.Upload', () => { ) const asyncValidatorNeverResolving = () => - new Promise(() => {}) + new Promise(() => undefined) const asyncFileHandlerFnSuccess = jest .fn(asyncValidatorResolvingWithSuccess)