1717// * LINK- https://github.com/adobe/aem-core-forms-components/blob/master/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/fileinput.html
1818// ******************************************************************************
1919
20- import React , { useCallback , useRef , useState } from 'react' ;
20+ import React , { useCallback , useRef , useState , useEffect } from 'react' ;
2121import { FileObject } from '@aemforms/af-core' ;
2222import { getFileSizeInBytes } from '@aemforms/af-core' ;
2323import { withRuleEngine } from '../utils/withRuleEngine' ;
@@ -41,19 +41,52 @@ const FileUpload = (props: PROPS) => {
4141 properties,
4242 valid
4343 } = props ;
44+ type LocalFile = { uid : string , file : File | FileObject } ;
45+
46+ const generateUid = ( seed ?: string ) => `${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ${ seed ? `-${ seed } ` : '' } ` ;
47+
48+ const getIdentity = ( f : any ) => `${ f ?. name || '' } |${ f ?. size || '' } |${ f ?. lastModified || '' } |${ f ?. type || '' } ` ;
49+
50+ const uidMapRef = React . useRef < Map < string , string > > ( new Map ( ) ) ;
51+
52+ const wrapWithUid = ( items : Array < File | FileObject > | null | undefined ) : LocalFile [ ] => {
53+ const list = items && ( items instanceof Array ? items : [ items ] ) ;
54+ return ( list || [ ] ) . map ( ( f ) => {
55+ const identity = getIdentity ( f as any ) ;
56+ let uid = uidMapRef . current . get ( identity ) ;
57+ if ( ! uid ) {
58+ uid = generateUid ( identity ) ;
59+ uidMapRef . current . set ( identity , uid ) ;
60+ }
61+ return { uid, file : f } ;
62+ } ) ;
63+ } ;
64+
4465 let val = value && ( value instanceof Array ? value : [ value ] ) ;
45- const [ files , setFiles ] = useState < FileObject [ ] > ( val || [ ] ) ;
66+ const [ files , setFiles ] = useState < LocalFile [ ] > ( wrapWithUid ( val as Array < File | FileObject > ) || [ ] ) ;
4667 const [ dragOver , setDragOver ] = useState ( false ) ;
4768
69+ // Sync internal state with external value prop only once (initial mount)
70+ const didInitFromPropsRef = useRef ( false ) ;
71+ useEffect ( ( ) => {
72+ if ( ! didInitFromPropsRef . current ) {
73+ const newVal = value && ( value instanceof Array ? value : [ value ] ) ;
74+ setFiles ( wrapWithUid ( newVal as Array < File | FileObject > ) ) ;
75+ didInitFromPropsRef . current = true ;
76+ }
77+ } , [ value ] ) ;
78+
4879 const maxFileSizeInBytes = getFileSizeInBytes ( maxFileSize ) ;
4980 let multiple = props . type ?. endsWith ( '[]' ) ? { multiple : true } : { } ;
5081
82+ // Dispatch value to the model. When field supports multiple values, send array; otherwise a single item
5183 const fileChangeHandler = useCallback (
52- ( files : Array < File | FileObject > ) => {
84+ ( localFiles : Array < LocalFile > ) => {
85+ const plainFiles = localFiles . map ( ( { file } ) => file ) ;
5386 if ( multiple ) {
54- props . dispatchChange ( files ) ;
87+ props . dispatchChange ( plainFiles ) ;
5588 } else {
56- props . dispatchChange ( files . length > 0 ? files [ 0 ] : null ) ;
89+ props . dispatchChange ( plainFiles . length > 0 ? plainFiles [ 0 ] : null ) ;
5790 }
5891 } ,
5992 [ multiple , props . dispatchChange ]
@@ -69,30 +102,54 @@ const FileUpload = (props: PROPS) => {
69102 setDragOver ( false ) ;
70103 } ;
71104
105+ // Handles file selection via input, drag/drop, or paste
72106 const fileUploadHandler = useCallback ( ( e ) => {
73107 e . preventDefault ( ) ;
74108 const newFiles = Array . from < File > ( e . dataTransfer ?. files || e ?. target ?. files || e . clipboardData ?. files || [ ] ) ;
109+
110+ // Clear the input value to allow re-uploading the same file again
111+ if ( e . target && e . target . type === 'file' ) {
112+ e . target . value = '' ;
113+ }
114+
75115 if ( newFiles ?. length ) {
76116 const validFiles = newFiles . filter ( ( file : File ) => file . size <= maxFileSizeInBytes ) ;
77117 if ( validFiles . length < newFiles . length ) {
78118 // Show constraint message for files with size exceeding the limit
79119 alert ( `${ props . constraintMessages ?. maxFileSize } ` ) ;
80120 }
81- const updatedFiles = [ ...files , ...validFiles ] ;
82- setFiles ( updatedFiles as FileObject [ ] ) ;
121+
122+ // Avoid collapsing same-named files: append new entries without deduping
123+ const wrappedNew = validFiles . map ( ( f ) => ( { uid : generateUid ( `${ f . name } -${ f . size } -${ f . lastModified } ` ) , file : f } ) ) ;
124+ const updatedFiles : LocalFile [ ] = [ ...files , ...wrappedNew ] ;
125+ setFiles ( updatedFiles ) ;
83126 fileChangeHandler ( updatedFiles ) ;
84127 }
128+
85129 setDragOver ( false ) ;
86130 } ,
87131 [ files , fileChangeHandler , maxFileSizeInBytes , props ?. constraintMessages ]
88132) ;
89133
134+ // Removes one file by its unique id and clears the input to allow re-uploading the same file
90135 const removeFile = useCallback (
91- ( index : number ) => {
136+ ( uid : string ) => {
137+ const index = files . findIndex ( ( f ) => f . uid === uid ) ;
138+ if ( index === - 1 ) { return ; }
139+ // remove identity mapping as well to avoid leaks
140+ const toRemove = files [ index ] ;
141+ const identity = getIdentity ( ( toRemove ?. file as any ) ) ;
142+ if ( identity ) {
143+ uidMapRef . current . delete ( identity ) ;
144+ }
92145 const fileList = [ ...files ] ;
93- fileList . splice ( index , 1 ) ;
146+ fileList . splice ( index , 1 ) ;
94147 setFiles ( fileList ) ;
95148 fileChangeHandler ( fileList ) ;
149+ // Clear the input value so the same file can be selected again
150+ if ( fileInputField . current ) {
151+ ( fileInputField . current as HTMLInputElement ) . value = '' ;
152+ }
96153 } ,
97154 [ files , fileChangeHandler ]
98155 ) ;
@@ -156,25 +213,31 @@ const FileUpload = (props: PROPS) => {
156213 </ div >
157214 < ul className = "cmp-adaptiveform-fileinput__filelist" >
158215 { files &&
159- files ?. map ( ( item : FileObject , index ) => (
216+ files ?. map ( ( { file , uid } ) => (
160217 < li
161218 className = "cmp-adaptiveform-fileinput__fileitem"
162- key = { item ?. name }
219+ key = { uid }
163220 >
164221 < span
165222 className = "cmp-adaptiveform-fileinput__filename"
166- aria-label = { item ?. name }
223+ aria-label = { ( file as any ) ?. name }
167224 >
168- { item ?. name }
225+ { ( file as any ) ?. name }
169226 </ span >
170227 < span className = "cmp-adaptiveform-fileinput__fileendcontainer" >
171228 < span className = "cmp-adaptiveform-fileinput__filesize" >
172- { formatBytes ( item ?. size ) }
229+ { formatBytes ( ( file as any ) ?. size ) }
173230 </ span >
174231 < button
175- onClick = { ( ) => removeFile ( index ) }
232+ type = "button"
233+ onClick = { ( e ) => {
234+ // Prevent form submit bubbling when used inside a <form>
235+ e . preventDefault ( ) ;
236+ e . stopPropagation ( ) ;
237+ removeFile ( uid ) ;
238+ } }
176239 className = "cmp-adaptiveform-fileinput__filedelete"
177- role = "button "
240+ aria-label = "Remove file "
178241 >
179242 x
180243 </ button >
@@ -188,4 +251,4 @@ const FileUpload = (props: PROPS) => {
188251 ) ;
189252} ;
190253
191- export default withRuleEngine ( FileUpload ) ;
254+ export default withRuleEngine ( FileUpload ) ;
0 commit comments