From 927dd00af785d3230661a8ff335801542ea17396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Fri, 20 Dec 2024 21:16:37 +0100 Subject: [PATCH 1/7] fix(Forms): add support for `sessionStorageId` in Field.Upload --- .../extensions/forms/Field/Upload/Upload.tsx | 28 +++++++++++++++---- .../Field/Upload/__tests__/Upload.test.tsx | 18 ++++++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) 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..9192dbd5ed4 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,21 @@ 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, ...props, } @@ -128,10 +140,16 @@ 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) + if (Array.isArray(value)) { + setFiles( + 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 + }) + ) } }, [setFiles, value]) @@ -173,7 +191,7 @@ function UploadComponent(props: Props) { handleChange(existingFiles) } }, - [files, setFiles, fileHandler, handleChange] + [setFiles, fileHandler, handleChange] ) const changeHandler = useCallback( 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', () => { From 0645d053408cf3389e258607cf1ba8f421b2c356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Sat, 21 Dec 2024 21:23:50 +0100 Subject: [PATCH 2/7] Refactor to use `toInput` --- .../extensions/forms/Field/Upload/Upload.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) 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 9192dbd5ed4..45128f9c15d 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -98,6 +98,7 @@ function UploadComponent(props: Props) { errorMessages, validateRequired, fromInput, + toInput: transformFiles, ...props, } @@ -140,17 +141,7 @@ function UploadComponent(props: Props) { }, [files]) useEffect(() => { - if (Array.isArray(value)) { - setFiles( - 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 - }) - ) - } + setFiles(value) }, [setFiles, value]) const handleChangeAsync = useCallback( @@ -259,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 +} From 73768c0cba59805e78eb8e09db2c2a49538c1777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Sat, 21 Dec 2024 21:24:27 +0100 Subject: [PATCH 3/7] Add examples --- .../more-fields/Upload/Examples.tsx | 31 ++++++++++++++++++- .../more-fields/Upload/demos.mdx | 6 ++++ .../more-fields/Upload/info.mdx | 6 ++++ .../Field/Upload/stories/Upload.stories.tsx | 26 ++++++++++++++-- 4 files changed, 65 insertions(+), 4 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..1a34a6c3d24 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 blog got lost during the serialization process. 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 ( + + + + + + + + + + + + ) +} From eb86616cee64ef7ba2416c3d2d6cb577a12f562c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Sat, 21 Dec 2024 21:24:33 +0100 Subject: [PATCH 4/7] Fix support to Value.Upload --- .../extensions/forms/Value/Upload/Upload.tsx | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 ( Date: Mon, 23 Dec 2024 06:46:51 +0100 Subject: [PATCH 5/7] chore(Forms): run `fromExternal` in all cases --- .../forms/hooks/useExternalValue.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/dnb-eufemia/src/extensions/forms/hooks/useExternalValue.ts b/packages/dnb-eufemia/src/extensions/forms/hooks/useExternalValue.ts index 0adc11d2de6..a0d22d839c9 100644 --- a/packages/dnb-eufemia/src/extensions/forms/hooks/useExternalValue.ts +++ b/packages/dnb-eufemia/src/extensions/forms/hooks/useExternalValue.ts @@ -36,22 +36,33 @@ export default function useExternalValue(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 + ) } } From 8f17ff885ca7d8fbdc4f563253bfbbda93ca0d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=B8egh?= Date: Mon, 23 Dec 2024 21:51:43 +0100 Subject: [PATCH 6/7] Add test --- .../extensions/forms/Field/Upload/Upload.tsx | 2 +- .../Value/Upload/__tests__/Upload.test.tsx | 80 +++++++++++++++++-- 2 files changed, 76 insertions(+), 6 deletions(-) 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 45128f9c15d..83764b00c2b 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Field/Upload/Upload.tsx @@ -258,7 +258,7 @@ export function transformFiles(value: UploadValue) { } value.map((item) => { - if (item.file && !(item.file instanceof File)) { + if (item?.file && !(item.file instanceof File)) { // To support session storage, we recreated the file blob. item['file'] = new File([], item['name']) } diff --git a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx index b727da994fb..663549f353f 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Value/Upload/__tests__/Upload.test.tsx @@ -1,6 +1,6 @@ import React from 'react' import { screen, render, fireEvent, waitFor } from '@testing-library/react' -import { Value, Form } from '../../..' +import { Value, Form, DataContext, Field } from '../../..' import { createMockFile } from '../../../../../components/upload/__tests__/testHelpers' import { wait } from '../../../../../core/jest/jestSetup' @@ -35,14 +35,23 @@ describe('Value.Upload', () => { ).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( Date: Thu, 26 Dec 2024 21:17:15 +0100 Subject: [PATCH 7/7] Update packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/feature-fields/more-fields/Upload/info.mdx --- .../extensions/forms/feature-fields/more-fields/Upload/info.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1a34a6c3d24..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 @@ -165,4 +165,4 @@ function MyForm() { 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 blog got lost during the serialization process. +But the persisted files only render the file name, and not the file itself. The file blob will be lost during the serialization process.