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..71b754e9aa6 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>() + + 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..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 @@ -1154,6 +1154,91 @@ describe('Field.Upload', () => { }) }) + it('should add new files from fileHandler with async function with multiple actions', async () => { + 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) => + setTimeout( + () => + resolve([ + { + file: files[id], + id: 'server_generated_id_' + id, + exists: false, + }, + ]), + 1 + ) + ) + + const asyncValidatorNeverResolving = () => + new Promise(() => undefined) + + const asyncFileHandlerFnSuccess = jest + .fn(asyncValidatorResolvingWithSuccess) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(0)) + .mockReturnValueOnce(asyncValidatorNeverResolving()) + .mockReturnValueOnce(asyncValidatorResolvingWithSuccess(2)) + .mockReturnValueOnce(asyncValidatorNeverResolving()) + + render() + + const element = getRootElement() + + await waitFor(() => { + fireEvent.drop(element, { + dataTransfer: { + files: [files[0]], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [files[1], files[3], files[4]], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [files[2]], + }, + }) + + fireEvent.drop(element, { + dataTransfer: { + files: [files[5]], + }, + }) + + expect( + document.querySelectorAll('.dnb-upload__file-cell').length + ).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( + document.querySelectorAll('.dnb-progress-indicator').length + ).toBe(4) + }) + }) + 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)}> + + + + + ) +}