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
+ )
}
}