diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx index ed3ce133a2d..349c14ec012 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/upload/Examples.tsx @@ -12,6 +12,7 @@ import { Section, Upload, } from '@dnb/eufemia/src' +import { UploadValue } from '@dnb/eufemia/src/extensions/forms/Field/Upload' export function createMockFile(name: string, size: number, type: string) { const file = new File([], name, { type }) @@ -34,6 +35,46 @@ const useMockFiles = (setFiles, extend) => { }, []) } +export async function mockAsyncFileUpload( + newFiles: UploadValue, +): Promise { + const promises = newFiles.map(async (file, index) => { + const formData = new FormData() + formData.append('file', file.file, file.file.name) + + await new Promise((resolve) => + setTimeout(resolve, Math.floor(Math.random() * 2000) + 1000), + ) + + const mockResponse = { + ok: (index + 2) % 2 === 0, // Every other request will fail + json: async () => ({ + server_generated_id: `${file.file.name}_${crypto.randomUUID()}`, + }), + } + + return await Promise.resolve(mockResponse) + .then((res) => { + if (res.ok) return res.json() + throw new Error('Unable to upload this file') + }) + .then((data) => { + return { + ...file, + id: data.server_generated_id, + } + }) + .catch((error) => { + return { + ...file, + errorMessage: error.message, + } + }) + }) + + return await Promise.all(promises) +} + export const UploadPrefilledFileList = () => ( { return ( @@ -78,3 +82,36 @@ export const WithPath = () => { ) } + +export const WithAsyncFileHandler = () => { + return ( + + {() => { + const MyForm = () => { + return ( + console.log(form)}> + + + + + + + ) + } + + const Output = () => { + const { files } = useUpload('async_upload_context_id') + return + } + + return + }} + + ) +} diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/demos.mdx index 58b2f44838d..d8cce0a083d 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/demos.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/demos.mdx @@ -25,3 +25,7 @@ import * as Examples from './Examples' ### Customized + +### With asynchronous file handler + + diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/info.mdx index 58eeb8a6b50..2c5ac985757 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/info.mdx @@ -58,3 +58,36 @@ The `value` property represents an array with an object described above: ```tsx render() ``` + +## About the `asyncFileHandler` property + +The `asyncFileHandler` is an asynchronous handler function that takes newly added files as a parameter and returns a promise containing the processed files. The component will automatically handle loading states during the upload process. This feature is useful for tasks like uploading files to a virus checker, which returns a new file ID if the file passes the check. To indicate a failed upload, set the `errorMessage` on the specific file object with the desired message to display next to the file in the upload list. + +```js +async function virusCheck(newFiles) { + const promises = newFiles.map(async (file) => { + const formData = new FormData() + formData.append('file', file.file, file.file.name) + + return await fetch('/', { method: 'POST', body: formData }) + .then((response) => { + if (response.ok) return response.json() + throw new Error('Unable to upload this file') + }) + .then((data) => { + return { + ...file, + id: data.server_generated_id, + } + }) + .catch((error) => { + return { + ...file, + errorMessage: error.message, + } + }) + }) + + return await Promise.all(promises) +} +``` 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 f6447fb4be1..38fb9cf1510 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -34,7 +34,9 @@ export type Props = FieldHelpProps & | 'fileMaxSize' | 'onFileDelete' | 'skeleton' - > + > & { + asyncFileHandler?: (newFiles: UploadValue) => Promise + } const validateRequired = ( value: UploadValue, @@ -53,6 +55,13 @@ const validateRequired = ( return undefined } +const updateFileLoadingState = ( + files: UploadValue, + isLoading: boolean +) => { + return files.map((file) => ({ ...file, isLoading })) +} + function UploadComponent(props: Props) { const sharedTr = useSharedTranslation().Upload const formsTr = useFormsTranslation().Upload @@ -82,6 +91,7 @@ function UploadComponent(props: Props) { handleChange, handleFocus, handleBlur, + asyncFileHandler, ...rest } = useFieldProps(preparedProps, { executeOnChangeRegardlessOfError: true, @@ -98,20 +108,53 @@ function UploadComponent(props: Props) { onFileDelete, } = rest - const { setFiles } = useUpload(id) + const { files: fileContext, setFiles } = useUpload(id) useEffect(() => { setFiles(value) }, [setFiles, value]) + const handleChangeAsync = useCallback( + async (files: UploadValue) => { + // Filter out existing files + const existingFileIds = fileContext?.map((file) => file.id) || [] + const newFiles = files.filter( + (file) => !existingFileIds.includes(file.id) + ) + + if (newFiles.length > 0) { + // Set loading + setFiles([ + ...fileContext, + ...updateFileLoadingState(newFiles, true), + ]) + + const uploadedFiles = updateFileLoadingState( + await asyncFileHandler(newFiles), + false + ) + + handleChange([...fileContext, ...uploadedFiles]) + } else { + handleChange(files) + } + }, + [fileContext, asyncFileHandler, setFiles, updateFileLoadingState] + ) + const changeHandler = useCallback( ({ files }: { files: UploadValue }) => { // Prevents the form-status from showing up handleBlur() handleFocus() - handleChange(files) + + if (asyncFileHandler) { + handleChangeAsync(files) + } else { + handleChange(files) + } }, - [handleBlur, handleChange, handleFocus] + [handleBlur, handleChange, handleFocus, asyncFileHandler, fileContext] ) const width = widthProp as FieldBlockWidth