5
5
// react-dropzone examples all use prop spreading. Disabling the eslint no prop spreading
6
6
// rules https://github.com/react-dropzone/react-dropzone
7
7
/* eslint-disable react/jsx-props-no-spreading */
8
- import React , { useState } from 'react' ;
8
+ import React , { useState , useRef , useEffect } from 'react' ;
9
9
import PropTypes from 'prop-types' ;
10
10
import { useDropzone } from 'react-dropzone' ;
11
11
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
12
12
import { faTrash } from '@fortawesome/free-solid-svg-icons' ;
13
- import { Button , Alert } from '@trussworks/react-uswds' ;
14
- import uploadFile from '../fetchers/File' ;
13
+ import {
14
+ Button , Alert , Modal , connectModal ,
15
+ } from '@trussworks/react-uswds' ;
16
+ import { uploadFile , deleteFile } from '../fetchers/File' ;
15
17
16
18
import './FileUploader.css' ;
17
19
20
+ export const upload = async ( file , reportId , attachmentType , setErrorMessage ) => {
21
+ let res ;
22
+ try {
23
+ const data = new FormData ( ) ;
24
+ data . append ( 'reportId' , reportId ) ;
25
+ data . append ( 'attachmentType' , attachmentType ) ;
26
+ data . append ( 'file' , file ) ;
27
+ res = await uploadFile ( data ) ;
28
+ } catch ( error ) {
29
+ setErrorMessage ( `${ file . name } failed to upload` ) ;
30
+ // eslint-disable-next-line no-console
31
+ console . log ( error ) ;
32
+ return null ;
33
+ }
34
+ setErrorMessage ( null ) ;
35
+ return {
36
+ id : res . id , originalFileName : file . name , fileSize : file . size , status : 'UPLOADED' ,
37
+ } ;
38
+ } ;
39
+
40
+ export const handleDrop = async ( e , reportId , id , onChange , setErrorMessage ) => {
41
+ if ( reportId === 'new' ) {
42
+ setErrorMessage ( 'Cannot save attachments without a Grantee or Non-Grantee selected' ) ;
43
+ return ;
44
+ }
45
+ let attachmentType ;
46
+ if ( id === 'attachments' ) {
47
+ attachmentType = 'ATTACHMENT' ;
48
+ } else if ( id === 'otherResources' ) {
49
+ attachmentType = 'RESOURCE' ;
50
+ }
51
+ const newFiles = e . map ( ( file ) => upload ( file , reportId , attachmentType , setErrorMessage ) ) ;
52
+ Promise . all ( newFiles ) . then ( ( values ) => {
53
+ onChange ( values ) ;
54
+ } ) ;
55
+ } ;
56
+
18
57
function Dropzone ( props ) {
19
58
const { onChange, id, reportId } = props ;
20
59
const [ errorMessage , setErrorMessage ] = useState ( ) ;
21
- const onDrop = async ( e ) => {
22
- if ( props . reportId === 'new' ) {
23
- setErrorMessage ( 'Cannot save attachments without a Grantee or Non-Grantee selected' ) ;
24
- return ;
25
- }
26
- let attachmentType ;
27
- if ( id === 'attachments' ) {
28
- attachmentType = 'ATTACHMENT' ;
29
- } else if ( id === 'otherResources' ) {
30
- attachmentType = 'RESOURCE' ;
31
- }
32
- const upload = async ( file ) => {
33
- try {
34
- const data = new FormData ( ) ;
35
- data . append ( 'reportId' , reportId ) ;
36
- data . append ( 'attachmentType' , attachmentType ) ;
37
- data . append ( 'file' , file ) ;
38
- await uploadFile ( data ) ;
39
- } catch ( error ) {
40
- setErrorMessage ( `${ file . name } failed to upload` ) ;
41
- // eslint-disable-next-line no-console
42
- console . log ( error ) ;
43
- return null ;
44
- }
45
- setErrorMessage ( null ) ;
46
- return {
47
- key : file . name , originalFileName : file . name , fileSize : file . size , status : 'UPLOADED' ,
48
- } ;
49
- } ;
50
- const newFiles = e . map ( ( file ) => upload ( file ) ) ;
51
- Promise . all ( newFiles ) . then ( ( values ) => {
52
- onChange ( values ) ;
53
- } ) ;
54
- } ;
55
- const { getRootProps, getInputProps } = useDropzone ( { onDrop } ) ;
60
+ const onDrop = ( e ) => handleDrop ( e , reportId , id , onChange , setErrorMessage ) ;
61
+
62
+ const { getRootProps, getInputProps } = useDropzone ( { onDrop, accept : 'image/*, .pdf, .docx, .xlsx, .pptx, .doc, .xls, .ppt, .zip' } ) ;
56
63
57
64
return (
58
65
< div
@@ -102,60 +109,129 @@ export const getStatus = (status) => {
102
109
return 'Upload Failed' ;
103
110
} ;
104
111
105
- const FileTable = ( { onFileRemoved, files } ) => (
106
- < div className = "files-table--container margin-top-2" >
107
- < table className = "files-table" >
108
- < thead className = "files-table--thead" bgcolor = "#F8F8F8" >
109
- < tr >
110
- < th width = "50%" >
111
- Name
112
- </ th >
113
- < th width = "20%" >
114
- Size
115
- </ th >
116
- < th width = "20%" >
117
- Status
118
- </ th >
119
- < th width = "10%" aria-label = "remove file" />
120
-
121
- </ tr >
122
- </ thead >
123
- < tbody >
124
- { files . map ( ( file , index ) => (
125
- < tr key = { file . key } id = { `files-table-row-${ index } ` } >
126
- < td className = "files-table--file-name" >
127
- { file . originalFileName }
128
- </ td >
129
- < td >
130
- { `${ ( file . fileSize / 1000 ) . toFixed ( 1 ) } KB` }
131
- </ td >
132
- < td >
133
- { getStatus ( file . status ) }
134
- </ td >
135
- < td >
136
- < Button
137
- role = "button"
138
- className = "smart-hub--file-tag-button"
139
- unstyled
140
- aria-label = "remove file"
141
- onClick = { ( ) => { onFileRemoved ( index ) ; } }
142
- >
143
- < span className = "fa-sm" >
144
- < FontAwesomeIcon color = "black" icon = { faTrash } />
145
- </ span >
146
- </ Button >
147
- </ td >
112
+ const DeleteFileModal = ( {
113
+ onFileRemoved, files, index, closeModal,
114
+ } ) => {
115
+ const deleteModal = useRef ( null ) ;
116
+ const onClose = ( ) => {
117
+ onFileRemoved ( index )
118
+ . then ( closeModal ( ) ) ;
119
+ } ;
120
+ useEffect ( ( ) => {
121
+ deleteModal . current . querySelector ( 'button' ) . focus ( ) ;
122
+ } ) ;
123
+ return (
124
+ < div role = "dialog" aria-modal = "true" ref = { deleteModal } >
125
+ < Modal
126
+ title = { < h2 > Delete File</ h2 > }
127
+ actions = { (
128
+ < >
129
+ < Button type = "button" onClick = { closeModal } >
130
+ Cancel
131
+ </ Button >
132
+ < Button type = "button" secondary onClick = { onClose } >
133
+ Delete
134
+ </ Button >
135
+ </ >
136
+ ) }
137
+ >
138
+ < p >
139
+ Are you sure you want to delete
140
+ { ' ' }
141
+ { files [ index ] . originalFileName }
142
+ { ' ' }
143
+ ?
144
+ </ p >
145
+ < p > This action cannot be undone.</ p >
146
+ </ Modal >
147
+ </ div >
148
+ ) ;
149
+ } ;
150
+
151
+ DeleteFileModal . propTypes = {
152
+ onFileRemoved : PropTypes . func . isRequired ,
153
+ closeModal : PropTypes . func . isRequired ,
154
+ index : PropTypes . number . isRequired ,
155
+ files : PropTypes . arrayOf ( PropTypes . object ) . isRequired ,
156
+ } ;
157
+
158
+ const ConnectedDeleteFileModal = connectModal ( DeleteFileModal ) ;
159
+
160
+ const FileTable = ( { onFileRemoved, files } ) => {
161
+ const [ index , setIndex ] = useState ( null ) ;
162
+ const [ isOpen , setIsOpen ] = useState ( false ) ;
163
+ const closeModal = ( ) => setIsOpen ( false ) ;
164
+
165
+ const handleDelete = ( newIndex ) => {
166
+ setIndex ( newIndex ) ;
167
+ setIsOpen ( true ) ;
168
+ } ;
169
+
170
+ return (
171
+ < div className = "files-table--container margin-top-2" >
172
+ < ConnectedDeleteFileModal
173
+ onFileRemoved = { onFileRemoved }
174
+ files = { files }
175
+ index = { index }
176
+ isOpen = { isOpen }
177
+ closeModal = { closeModal }
178
+ />
179
+ < table className = "files-table" >
180
+ < thead className = "files-table--thead" bgcolor = "#F8F8F8" >
181
+ < tr >
182
+ < th width = "50%" >
183
+ Name
184
+ </ th >
185
+ < th width = "20%" >
186
+ Size
187
+ </ th >
188
+ < th width = "20%" >
189
+ Status
190
+ </ th >
191
+ < th width = "10%" aria-label = "remove file" />
148
192
149
193
</ tr >
194
+ </ thead >
195
+ < tbody >
196
+ { files . map ( ( file , currentIndex ) => (
197
+ < tr key = { `file-${ file . id } ` } id = { `files-table-row-${ currentIndex } ` } >
198
+ < td className = "files-table--file-name" >
199
+ { file . originalFileName }
200
+ </ td >
201
+ < td >
202
+ { `${ ( file . fileSize / 1000 ) . toFixed ( 1 ) } KB` }
203
+ </ td >
204
+ < td >
205
+ { getStatus ( file . status ) }
206
+ </ td >
207
+ < td >
208
+ < Button
209
+ role = "button"
210
+ className = "smart-hub--file-tag-button"
211
+ unstyled
212
+ aria-label = "remove file"
213
+ onClick = { ( e ) => {
214
+ e . preventDefault ( ) ;
215
+ handleDelete ( currentIndex ) ;
216
+ } }
217
+ >
218
+ < span className = "fa-sm" >
219
+ < FontAwesomeIcon color = "black" icon = { faTrash } />
220
+ </ span >
221
+ </ Button >
222
+ </ td >
150
223
151
- ) ) }
152
- </ tbody >
153
- </ table >
154
- { files . length === 0 && (
224
+ </ tr >
225
+ ) ) }
226
+ </ tbody >
227
+ </ table >
228
+ { files . length === 0 && (
155
229
< p className = "files-table--empty" > No files uploaded</ p >
156
- ) }
157
- </ div >
158
- ) ;
230
+ ) }
231
+ </ div >
232
+ ) ;
233
+ } ;
234
+
159
235
FileTable . propTypes = {
160
236
onFileRemoved : PropTypes . func . isRequired ,
161
237
files : PropTypes . arrayOf ( PropTypes . object ) ,
@@ -170,8 +246,11 @@ const FileUploader = ({
170
246
onChange ( [ ...files , ...newFiles ] ) ;
171
247
} ;
172
248
173
- const onFileRemoved = ( removedFileIndex ) => {
174
- onChange ( files . filter ( ( f , index ) => ( index !== removedFileIndex ) ) ) ;
249
+ const onFileRemoved = async ( removedFileIndex ) => {
250
+ const file = files [ removedFileIndex ] ;
251
+ const remainingFiles = files . filter ( ( f ) => f . id !== file . id ) ;
252
+ onChange ( remainingFiles ) ;
253
+ await deleteFile ( file . id , reportId ) ;
175
254
} ;
176
255
177
256
return (
0 commit comments