Skip to content

Commit 0a54784

Browse files
authored
Merge pull request #191 from adhocteam/cm-280-delete-files
Cm 280 delete files
2 parents 092f32a + 05f697f commit 0a54784

File tree

11 files changed

+399
-155
lines changed

11 files changed

+399
-155
lines changed

docs/openapi/paths/files.yaml

+26-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,29 @@ post:
1717
application/json:
1818
schema:
1919
type: object
20-
$ref: '../index.yaml#/components/schemas/fileResponse'
20+
$ref: '../index.yaml#/components/schemas/fileResponse'
21+
delete:
22+
tags:
23+
- files
24+
operationId: deleteFile
25+
description: Delete a file from s3
26+
parameters:
27+
- in: path
28+
name: reportId
29+
required: true
30+
schema:
31+
type: number
32+
description: Numeric ID of the file to delete
33+
- in: path
34+
name: fileId
35+
required: true
36+
schema:
37+
type: number
38+
description: Numeric ID of the file to delete
39+
responses:
40+
204:
41+
description: file was successfully deleted
42+
400:
43+
description: request did not contain reportId/fileId in the path
44+
403:
45+
description: User is not authorized to delete this file

frontend/src/components/FileUploader.js

+169-90
Original file line numberDiff line numberDiff line change
@@ -5,54 +5,61 @@
55
// react-dropzone examples all use prop spreading. Disabling the eslint no prop spreading
66
// rules https://github.com/react-dropzone/react-dropzone
77
/* eslint-disable react/jsx-props-no-spreading */
8-
import React, { useState } from 'react';
8+
import React, { useState, useRef, useEffect } from 'react';
99
import PropTypes from 'prop-types';
1010
import { useDropzone } from 'react-dropzone';
1111
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
1212
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';
1517

1618
import './FileUploader.css';
1719

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+
1857
function Dropzone(props) {
1958
const { onChange, id, reportId } = props;
2059
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' });
5663

5764
return (
5865
<div
@@ -102,60 +109,129 @@ export const getStatus = (status) => {
102109
return 'Upload Failed';
103110
};
104111

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" />
148192

149193
</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>
150223

151-
))}
152-
</tbody>
153-
</table>
154-
{ files.length === 0 && (
224+
</tr>
225+
))}
226+
</tbody>
227+
</table>
228+
{ files.length === 0 && (
155229
<p className="files-table--empty">No files uploaded</p>
156-
)}
157-
</div>
158-
);
230+
)}
231+
</div>
232+
);
233+
};
234+
159235
FileTable.propTypes = {
160236
onFileRemoved: PropTypes.func.isRequired,
161237
files: PropTypes.arrayOf(PropTypes.object),
@@ -170,8 +246,11 @@ const FileUploader = ({
170246
onChange([...files, ...newFiles]);
171247
};
172248

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);
175254
};
176255

177256
return (

0 commit comments

Comments
 (0)