From a8dfc5299b9b0446704c93af8b7e2d67e5dafaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Thu, 26 Dec 2024 21:34:04 +0100 Subject: [PATCH] fix(Forms): add support for `sessionStorageId` in Field.Upload (#4424) In this PR #4339 we did not render the stored files. This time, we do support rendering the file names stored in the session storage. The test dedicated to session storage got aligned. --------- Co-authored-by: Anders --- .../more-fields/Upload/Examples.tsx | 31 ++++++- .../more-fields/Upload/demos.mdx | 6 ++ .../more-fields/Upload/info.mdx | 6 ++ .../extensions/forms/Field/Upload/Upload.tsx | 39 +++++++-- .../Field/Upload/__tests__/Upload.test.tsx | 18 ++++- .../Field/Upload/stories/Upload.stories.tsx | 26 +++++- .../extensions/forms/Value/Upload/Upload.tsx | 23 +++++- .../Value/Upload/__tests__/Upload.test.tsx | 80 +++++++++++++++++-- .../forms/hooks/useExternalValue.ts | 19 ++++- 9 files changed, 222 insertions(+), 26 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/Examples.tsx index fb2211242e4..90b707634d9 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/Examples.tsx @@ -1,6 +1,11 @@ import { Flex } from '@dnb/eufemia/src' import ComponentBox from '../../../../../../../shared/tags/ComponentBox' -import { Field, Form, Tools } from '@dnb/eufemia/src/extensions/forms' +import { + Field, + Form, + Tools, + Value, +} from '@dnb/eufemia/src/extensions/forms' 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' @@ -252,3 +257,27 @@ export const WithAsyncOnFileClick = () => { ) } + +export function SessionStorage() { + 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 370f8503667..647435f1a7d 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 @@ -26,6 +26,12 @@ import * as Examples from './Examples' +### Session storage support + +The `sessionStorageId` property can be used to store the files in the session storage so they persist between page reloads. + + + ### 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: 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 8aa4948e6c6..290a2d807ce 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 @@ -160,3 +160,9 @@ function MyForm() { ) } ``` + +### Persist files in session storage + +The `sessionStorageId` property can be used to store the files in the session storage so they persist between page reloads. + +But the persisted files only render the file name, and not the file itself. The file blob will be lost during the serialization process. 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 b36bdcc99db..83764b00c2b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -83,9 +83,22 @@ function UploadComponent(props: Props) { [formsTr.errorRequired] ) + const fromInput = useCallback((value: UploadValue) => { + value.forEach((item, index) => { + value[index] = item + + // Store the name in the value, to support session storage (serialization) + value[index]['name'] = item['name'] || item.file?.name + }) + + return value + }, []) + const preparedProps = { errorMessages, validateRequired, + fromInput, + toInput: transformFiles, ...props, } @@ -128,11 +141,7 @@ function UploadComponent(props: Props) { }, [files]) useEffect(() => { - // Files stored in session storage will not have a property (due to serialization). - const hasInvalidFiles = value?.some(({ file }) => !file?.name) - if (!hasInvalidFiles) { - setFiles(value) - } + setFiles(value) }, [setFiles, value]) const handleChangeAsync = useCallback( @@ -173,7 +182,7 @@ function UploadComponent(props: Props) { handleChange(existingFiles) } }, - [files, setFiles, fileHandler, handleChange] + [setFiles, fileHandler, handleChange] ) const changeHandler = useCallback( @@ -241,3 +250,21 @@ function UploadComponent(props: Props) { export default UploadComponent UploadComponent._supportsSpacingProps = true + +export function transformFiles(value: UploadValue) { + if (Array.isArray(value)) { + if (value.length === 0) { + return undefined + } + + value.map((item) => { + if (item?.file && !(item.file instanceof File)) { + // To support session storage, we recreated the file blob. + item['file'] = new File([], item['name']) + } + return item + }) + } + + return value +} 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 c4a14a91047..37a3ed8f3c3 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 @@ -207,11 +207,13 @@ describe('Field.Upload', () => { file: file1, id: expect.any(String), exists: expect.any(Boolean), + name: 'fileName-1.png', }, { file: file2, id: expect.any(String), exists: expect.any(Boolean), + name: 'fileName-2.png', }, ], }, @@ -256,6 +258,7 @@ describe('Field.Upload', () => { file: file1, exists: false, id: expect.anything(), + name: 'fileName-1.png', }, { errorMessage: nbShared.Upload.errorLargeFile.replace( @@ -265,6 +268,7 @@ describe('Field.Upload', () => { file: file2, exists: false, id: expect.anything(), + name: 'fileName-2.png', }, ], }, @@ -276,6 +280,7 @@ describe('Field.Upload', () => { file: file1, exists: false, id: expect.anything(), + name: 'fileName-1.png', }, { errorMessage: nbShared.Upload.errorLargeFile.replace( @@ -285,6 +290,7 @@ describe('Field.Upload', () => { file: file2, exists: false, id: expect.anything(), + name: 'fileName-2.png', }, ]) @@ -310,6 +316,7 @@ describe('Field.Upload', () => { file: file1, exists: false, id: expect.anything(), + name: 'fileName-1.png', }, ], }, @@ -353,6 +360,7 @@ describe('Field.Upload', () => { exists: false, file: file1, id: expect.any(String), + name: 'fileName-1.png', }), ], }, @@ -399,6 +407,7 @@ describe('Field.Upload', () => { exists: false, file: file1, id: expect.any(String), + name: 'fileName-1.png', }), ], }, @@ -468,6 +477,7 @@ describe('Field.Upload', () => { file: file1, exists: false, id: expect.anything(), + name: 'fileName-1.png', }, ], }, @@ -734,6 +744,7 @@ describe('Field.Upload', () => { file: file1, exists: false, id: expect.anything(), + name: 'fileName-1.png', }, ], }, @@ -1475,7 +1486,7 @@ describe('Field.Upload', () => { }) }) - it('should not set files from session storage if they are invalid', async () => { + it('should recreate files from session storage', async () => { const file = createMockFile('fileName.png', 100, 'image/png') const { unmount } = render( @@ -1517,15 +1528,16 @@ describe('Field.Upload', () => { expect(dataContext.internalDataRef.current.myFiles).toEqual([ { exists: false, - file: {}, + file: new File([], 'fileName.png'), id: expect.any(String), + name: 'fileName.png', }, ]) const [title] = Array.from(document.querySelectorAll('p')) expect(title).toHaveTextContent(nbShared.Upload.title) expect( document.querySelectorAll('.dnb-upload__file-cell').length - ).toBe(0) + ).toBe(1) }) describe('transformIn and transformOut', () => { 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 131fd1afffb..d1ab3dbcd3f 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 @@ -1,4 +1,4 @@ -import { Field, Form, Tools } from '../../..' +import { Field, Form, Tools, Value } from '../../..' import { Flex } from '../../../../../components' import { UploadFileNative } from '../../../../../components/Upload' import { createRequest } from '../../../Form/Handler/stories/FormHandler.stories' @@ -216,8 +216,6 @@ export function TransformInAndOut() { > ) } + +export function SessionStorage() { + return ( + + + + + + + + + + + + ) +} diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx index d12447ce4a5..a87761196a0 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/Upload.tsx @@ -9,7 +9,10 @@ import ListFormat, { import type { UploadFile } from '../../../../components/upload/types' import { getFileIcon } from '../../../../components/upload/UploadFileListCell' import { BYTES_IN_A_MEGA_BYTE } from '../../../../components/upload/UploadVerify' -import { Props as FieldUploadProps } from '../../Field/Upload/Upload' +import { + Props as FieldUploadProps, + transformFiles, +} from '../../Field/Upload/Upload' import { format } from '../../../../components/number-format/NumberUtils' import { UploadFileLink } from '../../../../components/upload/UploadFileListLink' import { isAsync } from '../../../../shared/helpers/isAsync' @@ -21,8 +24,12 @@ export type Props = ValueProps> & } function Upload(props: Props) { + const preparedProps = { + fromExternal: transformFiles, + ...props, + } + const { - path, value, format, className, @@ -32,7 +39,7 @@ function Upload(props: Props) { displaySize = false, onFileClick, ...rest - } = useValueProps(props) + } = useValueProps(preparedProps) const list = useMemo(() => { const valueToUse = @@ -62,7 +69,15 @@ function Upload(props: Props) { /> ) } - }, [path, value, variant, listType]) + }, [ + value, + download, + displaySize, + onFileClick, + format, + variant, + listType, + ]) return ( { ).toHaveTextContent('foo.png, bar.png og baz.png') }) - it('renders empty array of file values', () => { + it('does not render empty array of file values', () => { render() expect( - document.querySelector( - '.dnb-forms-value-upload .dnb-forms-value-block__content' - ) + document.querySelector('.dnb-forms-value-upload') + ).not.toBeInTheDocument() + }) + + it('renders when value is empty but showEmpty is true', () => { + render() + + expect( + document.querySelector('.dnb-forms-value-upload') ).toHaveTextContent('') + expect( + document.querySelector('.dnb-forms-value-block__content') + ).not.toBeInTheDocument() }) it('renders array of falsy values', () => { @@ -55,6 +64,67 @@ describe('Value.Upload', () => { ).toHaveTextContent('') }) + it('should recreate files from session storage', async () => { + const file = createMockFile('fileName.png', 100, 'image/png') + + const { unmount } = render( + + + + + ) + + expect( + document.querySelector('.dnb-forms-value-upload') + ).not.toBeInTheDocument() + + const element = document.querySelector('.dnb-upload') + + await waitFor(() => + fireEvent.drop(element, { + dataTransfer: { + files: [file], + }, + }) + ) + + expect( + document.querySelector( + '.dnb-forms-value-upload .dnb-forms-value-block__content' + ) + ).toHaveTextContent('fileName.png') + + let dataContext = null + + // Don't rerender, but render again to make sure the files are not set + unmount() + render( + + + + {(context) => { + dataContext = context + return null + }} + + + ) + + expect(dataContext.internalDataRef.current.myFiles).toEqual([ + { + exists: false, + file: new File([], 'fileName.png'), + id: expect.any(String), + name: 'fileName.png', + }, + ]) + expect( + document.querySelector( + '.dnb-forms-value-upload .dnb-forms-value-block__content' + ) + ).toHaveTextContent('fileName.png') + }) + it('renders custom format', () => { render( (props: Props) { if (inIterate && itemPath) { // This field is inside an iterate, and has a pointer from the base of the element being iterated if (itemPath === '/') { - return iterateElementValue ?? emptyValue + return ( + transformers?.current?.fromExternal?.( + iterateElementValue as Value + ) ?? emptyValue + ) } if (pointer.has(iterateElementValue, itemPath)) { - return pointer.get(iterateElementValue, itemPath) ?? emptyValue + return ( + transformers?.current?.fromExternal?.( + pointer.get(iterateElementValue, itemPath) as Value + ) ?? emptyValue + ) } } if (data && path) { // There is a surrounding data context and a path for where in the source to find the data if (path === '/') { - return data ?? emptyValue + return transformers?.current?.fromExternal?.(data) ?? emptyValue } if (pointer.has(data, path)) { - return pointer.get(data, path) ?? emptyValue + return ( + transformers?.current?.fromExternal?.(pointer.get(data, path)) ?? + emptyValue + ) } }