Skip to content

Commit

Permalink
feat(Forms): add asyncFileHandler to Field.Upload to support async …
Browse files Browse the repository at this point in the history
…file handling during upload (#4281)

Add support for asynchronously processing/handling files so a virus
checker can be used with the Field.Upload component.

---------

Co-authored-by: Whitney Hiltz <[email protected]>
  • Loading branch information
whitneymarkov and Whitney Hiltz authored Nov 17, 2024
1 parent f9ded3c commit 030a09c
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -34,6 +35,46 @@ const useMockFiles = (setFiles, extend) => {
}, [])
}

export async function mockAsyncFileUpload(
newFiles: UploadValue,
): Promise<UploadValue> {
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 = () => (
<ComponentBox
data-visual-test="upload-file-list"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Flex } from '@dnb/eufemia/src'
import ComponentBox from '../../../../../../../shared/tags/ComponentBox'
import { Field, Form } from '@dnb/eufemia/src/extensions/forms'
import { createMockFile } from '../../../../../../../docs/uilib/components/upload/Examples'
import { Field, Form, Tools } from '@dnb/eufemia/src/extensions/forms'
import {
createMockFile,
mockAsyncFileUpload,
} from '../../../../../../../docs/uilib/components/upload/Examples'
import useUpload from '@dnb/eufemia/src/components/upload/useUpload'

export const BasicUsage = () => {
return (
Expand Down Expand Up @@ -78,3 +82,36 @@ export const WithPath = () => {
</ComponentBox>
)
}

export const WithAsyncFileHandler = () => {
return (
<ComponentBox scope={{ mockAsyncFileUpload, useUpload, Tools }}>
{() => {
const MyForm = () => {
return (
<Form.Handler onSubmit={async (form) => console.log(form)}>
<Flex.Stack>
<Field.Upload
id="async_upload_context_id"
path="/attachments"
labelDescription="Upload multiple files at once to see the upload error message. This demo has been set up so that every other file in a batch will fail."
asyncFileHandler={mockAsyncFileUpload}
required
/>
<Form.SubmitButton />
</Flex.Stack>
<Output />
</Form.Handler>
)
}

const Output = () => {
const { files } = useUpload('async_upload_context_id')
return <Tools.Log data={files} top />
}

return <MyForm />
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ import * as Examples from './Examples'
### Customized

<Examples.Customized />

### With asynchronous file handler

<Examples.WithAsyncFileHandler />
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,36 @@ The `value` property represents an array with an object described above:
```tsx
render(<Field.Upload value={files} />)
```

## 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)
}
```
51 changes: 47 additions & 4 deletions packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export type Props = FieldHelpProps &
| 'fileMaxSize'
| 'onFileDelete'
| 'skeleton'
>
> & {
asyncFileHandler?: (newFiles: UploadValue) => Promise<UploadValue>
}

const validateRequired = (
value: UploadValue,
Expand All @@ -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
Expand Down Expand Up @@ -82,6 +91,7 @@ function UploadComponent(props: Props) {
handleChange,
handleFocus,
handleBlur,
asyncFileHandler,
...rest
} = useFieldProps(preparedProps, {
executeOnChangeRegardlessOfError: true,
Expand All @@ -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
Expand Down

0 comments on commit 030a09c

Please sign in to comment.