Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Field.Upload): adds support for async and sync fn in fileHandler #4294

Merged
merged 7 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ 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 @@ -35,46 +34,6 @@ 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
Expand Up @@ -13,6 +13,10 @@ breadcrumb:

Change log for the Eufemia Forms extension.

## v10.56.1

- Renamed `asyncFileHandler` to `fileHandler` in [Field.Upload](/uilib/extensions/forms/feature-fields/more-fields/Upload/), to support both async and sync file handling.

## v10.56

- Added inline help button (`help`) to all `Field.*` components as default (with option to open in Dialog).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Flex } from '@dnb/eufemia/src'
import ComponentBox from '../../../../../../../shared/tags/ComponentBox'
import { Field, Form, Tools } from '@dnb/eufemia/src/extensions/forms'
import {
createMockFile,
mockAsyncFileUpload,
} from '../../../../../../../docs/uilib/components/upload/Examples'
import { createMockFile } from '../../../../../../../docs/uilib/components/upload/Examples'
import useUpload from '@dnb/eufemia/src/components/upload/useUpload'
import { UploadValue } from '@dnb/eufemia/src/extensions/forms/Field/Upload'
import { createRequest } from '../../../Form/SubmitIndicator/Examples'

export const BasicUsage = () => {
return (
Expand Down Expand Up @@ -85,7 +84,7 @@ export const WithPath = () => {

export const WithAsyncFileHandler = () => {
return (
<ComponentBox scope={{ mockAsyncFileUpload, useUpload, Tools }}>
<ComponentBox scope={{ createRequest, useUpload, Tools }}>
{() => {
const MyForm = () => {
return (
Expand All @@ -95,7 +94,7 @@ export const WithAsyncFileHandler = () => {
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}
fileHandler={mockAsyncFileUpload}
required
/>
<Form.SubmitButton />
Expand All @@ -105,6 +104,47 @@ export const WithAsyncFileHandler = () => {
)
}

async function mockAsyncFileUpload(
newFiles: UploadValue,
): Promise<UploadValue> {
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(Math.floor(Math.random() * 2000) + 1000) // Simulate a request

try {
const mockResponse = {
ok: false, // Fails virus check
json: async () => ({
server_generated_id:
file.file.name + '_' + crypto.randomUUID(),
}),
}

if (!mockResponse.ok) {
throw new Error('Unable to upload this file')
}

const data = await mockResponse.json()
updatedFiles.push({
...file,
id: data.server_generated_id,
})
} catch (error) {
updatedFiles.push({
...file,
errorMessage: error.message,
})
}
}

return updatedFiles
}

const Output = () => {
const { files } = useUpload('async_upload_context_id')
return <Tools.Log data={files} top />
Expand All @@ -115,3 +155,44 @@ export const WithAsyncFileHandler = () => {
</ComponentBox>
)
}

export const WithSyncFileHandler = () => {
return (
<ComponentBox scope={{ createRequest, useUpload, Tools }}>
{() => {
const MyForm = () => {
return (
<Form.Handler onSubmit={async (form) => console.log(form)}>
<Flex.Stack>
<Field.Upload
id="sync_upload_context_id"
path="/myattachments"
fileHandler={mockSyncFileUpload}
required
/>
<Form.SubmitButton />
</Flex.Stack>
<Output />
</Form.Handler>
)
}

function mockSyncFileUpload(newFiles: UploadValue) {
return newFiles.map((file) => {
if (file.file.name.length > 5) {
file.errorMessage = 'File length is too long'
}
return file
})
}

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

return <MyForm />
}}
</ComponentBox>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,12 @@ import * as Examples from './Examples'

### With asynchronous file handler

The `fileHandler` property supports an asynchronous function, and can be used for handling/validating files asynchronously, like to upload files to a virus checker and display errors based on the outcome:

<Examples.WithAsyncFileHandler />

### With synchronous file handler

The `fileHandler` property supports a synchronous function, and can be used for handling/validating files synchronously, like to check for file names that's too long:

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

## About the `asyncFileHandler` property
## About the `fileHandler` 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.
The `fileHandler` is a handler function that supports both an asynchronous and synchronous function. It takes newly added files as a parameter and returns processed files (a promise when asynchronous).
The component will automatically handle asynchronous 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) {
Expand Down
21 changes: 12 additions & 9 deletions packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export type Props = Omit<
| 'onFileDelete'
| 'skeleton'
> & {
asyncFileHandler?: (newFiles: UploadValue) => Promise<UploadValue>
fileHandler?: (
newFiles: UploadValue
) => UploadValue | Promise<UploadValue>
}

const validateRequired = (
Expand All @@ -62,7 +64,7 @@ const validateRequired = (

const updateFileLoadingState = (
files: UploadValue,
isLoading: boolean
{ isLoading } = { isLoading: true }
tujoworker marked this conversation as resolved.
Show resolved Hide resolved
langz marked this conversation as resolved.
Show resolved Hide resolved
) => {
return files.map((file) => ({ ...file, isLoading }))
}
Expand Down Expand Up @@ -96,7 +98,7 @@ function UploadComponent(props: Props) {
handleChange,
handleFocus,
handleBlur,
asyncFileHandler,
fileHandler,
...rest
} = useFieldProps(preparedProps, {
executeOnChangeRegardlessOfError: true,
Expand Down Expand Up @@ -131,20 +133,21 @@ function UploadComponent(props: Props) {
// Set loading
setFiles([
...fileContext,
...updateFileLoadingState(newFiles, true),
...updateFileLoadingState(newFiles, { isLoading: true }),
])

const uploadedFiles = updateFileLoadingState(
await asyncFileHandler(newFiles),
false
await fileHandler(newFiles),
{ isLoading: false }
)

// Set error, if any
handleChange([...fileContext, ...uploadedFiles])
} else {
handleChange(files)
}
},
[fileContext, asyncFileHandler, setFiles, updateFileLoadingState]
[fileContext, setFiles, fileHandler, handleChange]
)

const changeHandler = useCallback(
Expand All @@ -153,13 +156,13 @@ function UploadComponent(props: Props) {
handleBlur()
handleFocus()

if (asyncFileHandler) {
if (fileHandler) {
handleChangeAsync(files)
} else {
handleChange(files)
}
},
[handleBlur, handleChange, handleFocus, asyncFileHandler, fileContext]
[handleBlur, handleFocus, fileHandler, handleChangeAsync, handleChange]
)

const width = widthProp as FieldBlockWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
import { PropertiesTableProps } from '../../../../shared/types'

export const UploadFieldProperties: PropertiesTableProps = {
asyncFileHandler: {
doc: 'Asynchronous handler function that takes newly added files (`newFiles: UploadValue`) as a parameter and returns a promise containing the processed files (`Promise<UploadValue>`).',
fileHandler: {
doc: 'File handler function that takes newly added files (`newFiles: UploadValue`) as a parameter and returns the processed files. The function can either be synchronous or asynchronous. It returns a promise (`Promise<UploadValue>`) containing the processed files when asynchronous.',
type: 'function',
status: 'optional',
},
Expand Down
Loading
Loading