Skip to content

Commit 4346583

Browse files
authored
Fix/attach show size (#2741)
* fix: add file size to file rows * feat: display total file size in attachments widget * chore: add tests * fix: update file normalization and adjust total size display in tests * chore: formatting * chore: formatting * fix: rename file size attribute for consistency in attachments * fix: normalize file_size values to ensure consistent number format across attachments * fix: display file size in attachment list and update tests for size labels
1 parent 1022a87 commit 4346583

13 files changed

Lines changed: 312 additions & 50 deletions

File tree

app/assets/javascripts/attachments/AttachFilesModal.js

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
22
// Render inline as an expanding panel instead of a portal/modal
33
import { ACCEPT_ATTRIBUTE } from "./useAttachments";
44
import { getAttachmentTranslations } from "./localization";
5+
import { formatFileSize } from "./fileSize";
56

67
const DEFAULT_COPY = getAttachmentTranslations("en");
78

@@ -274,33 +275,47 @@ export const AttachFilesModal = ({
274275
</div>
275276
{selectedFiles.length > 0 && (
276277
<ul className="space-y-2 mb-4" data-testid="pending-files-list">
277-
{selectedFiles.map((pendingFile) => (
278-
<li
279-
key={pendingFile.id}
280-
className="border border-gray-300 p-3 flex justify-between items-start align-top"
281-
>
282-
<div className="min-w-0 pr-4">
283-
<span
284-
className="attachment-file-name-truncate mb-0 block"
285-
title={pendingFile.file.name}
286-
>
287-
{pendingFile.file.name}
288-
</span>
289-
</div>
290-
<button
291-
className="link text-red-700 self-start"
292-
type="button"
293-
data-testid="attachments-pending-remove"
294-
aria-label={`Remove ${pendingFile.file.name}`}
295-
onClick={() => onRemovePending(pendingFile.id)}
278+
{selectedFiles.map((pendingFile) => {
279+
const selectedFileSize = formatFileSize(pendingFile.file.size);
280+
281+
return (
282+
<li
283+
key={pendingFile.id}
284+
className="border border-gray-300 p-3 flex justify-between items-start align-top"
296285
>
297-
<span className="font-bold underline">{copy.remove}</span>
298-
<span className="text-[24px]" aria-hidden="true">
299-
&nbsp;×
300-
</span>
301-
</button>
302-
</li>
303-
))}
286+
<div className="min-w-0 pr-4">
287+
<p className="min-w-0 mb-0">
288+
<span
289+
className="attachment-file-name-truncate"
290+
title={pendingFile.file.name}
291+
>
292+
{pendingFile.file.name}
293+
</span>
294+
{selectedFileSize ? (
295+
<span
296+
className="attachment-size"
297+
data-testid="attachment-file-size"
298+
>
299+
{` (${selectedFileSize})`}
300+
</span>
301+
) : null}
302+
</p>
303+
</div>
304+
<button
305+
className="link text-red-700 self-start"
306+
type="button"
307+
data-testid="attachments-pending-remove"
308+
aria-label={`Remove ${pendingFile.file.name}`}
309+
onClick={() => onRemovePending(pendingFile.id)}
310+
>
311+
<span className="font-bold underline">{copy.remove}</span>
312+
<span className="text-[24px]" aria-hidden="true">
313+
&nbsp;×
314+
</span>
315+
</button>
316+
</li>
317+
);
318+
})}
304319
</ul>
305320
)}
306321

app/assets/javascripts/attachments/AttachedFileRow.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useRef } from "react";
22
import { ATTACHMENT_STATUSES } from "./useAttachments";
33
import { getAttachmentTranslations } from "./localization";
4+
import { formatFileSize } from "./fileSize";
45

56
const DEFAULT_COPY = getAttachmentTranslations("en");
67

@@ -33,6 +34,7 @@ export const AttachedFileRow = ({
3334
? `${downloadEndpoint}/${encodeURIComponent(file.id)}`
3435
: null;
3536
const canDownload = !isInProgress && !isMalware && Boolean(downloadHref);
37+
const fileSizeLabel = formatFileSize(file.file_size);
3638

3739
const handleDownload = async (e) => {
3840
e.preventDefault();
@@ -127,7 +129,17 @@ export const AttachedFileRow = ({
127129
data-testid="attachment-row-spinner"
128130
></div>
129131
) : null}
130-
<p className="min-w-0 mb-0">{fileNameNode}</p>
132+
<p className="min-w-0 mb-0">
133+
{fileNameNode}
134+
{fileSizeLabel ? (
135+
<span
136+
className="attachment-size"
137+
data-testid="attachment-file-size"
138+
>
139+
{` (${fileSizeLabel})`}
140+
</span>
141+
) : null}
142+
</p>
131143
</div>
132144
</div>
133145
<button

app/assets/javascripts/attachments/AttachmentsWidget.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useAttachments,
77
validateFiles,
88
} from "./useAttachments";
9+
import { formatFileSize, sumAttachmentFileSizes } from "./fileSize";
910
import { getAttachmentTranslations } from "./localization";
1011

1112
export const AttachmentsWidget = ({
@@ -130,6 +131,15 @@ export const AttachmentsWidget = ({
130131
() => summarizeStatuses(files, copy),
131132
[files, copy],
132133
);
134+
const totalFileSizeLabel = useMemo(() => {
135+
const countableFiles = files.filter(
136+
(file) =>
137+
file.status !== "virus_scan_failed" && file.status !== "deleted",
138+
);
139+
const totalBytes = sumAttachmentFileSizes(countableFiles);
140+
141+
return totalBytes > 0 ? formatFileSize(totalBytes) : null;
142+
}, [files]);
133143

134144
const handleAttach = async (selectedFiles) => {
135145
const result = validateFiles(selectedFiles, files, copy);
@@ -157,7 +167,17 @@ export const AttachmentsWidget = ({
157167

158168
return (
159169
<section className="mb-16" data-testid="attachments-widget">
160-
<h2 className="heading-medium">{copy.attachedFilesHeading}</h2>
170+
<h2 className="heading-medium" data-testid="attachments-heading">
171+
{copy.attachedFilesHeading}
172+
{totalFileSizeLabel ? (
173+
<span
174+
className="hint text-xs inline ml-2"
175+
data-testid="attachments-total-size"
176+
>
177+
({totalFileSizeLabel})
178+
</span>
179+
) : null}
180+
</h2>
161181

162182
{downloadError && (
163183
<div
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const toFiniteFileSize = (value) => {
2+
const parsed = Number(value);
3+
4+
if (!Number.isFinite(parsed) || parsed < 0) {
5+
return null;
6+
}
7+
8+
return parsed;
9+
};
10+
11+
export const formatFileSize = (sizeInBytes) => {
12+
const normalizedSize = toFiniteFileSize(sizeInBytes);
13+
14+
if (normalizedSize === null) {
15+
return null;
16+
}
17+
18+
if (normalizedSize < 1024) {
19+
return `${normalizedSize} B`;
20+
}
21+
22+
if (normalizedSize < 1024 * 1024) {
23+
return `${(normalizedSize / 1024).toFixed(1)} KB`;
24+
}
25+
26+
return `${(normalizedSize / (1024 * 1024)).toFixed(1)} MB`;
27+
};
28+
29+
export const sumAttachmentFileSizes = (files = []) =>
30+
files.reduce((sum, file) => {
31+
const fileSize = toFiniteFileSize(file?.file_size);
32+
33+
if (fileSize === null) {
34+
return sum;
35+
}
36+
37+
return sum + fileSize;
38+
}, 0);

app/assets/javascripts/attachments/useAttachments.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ const getExtension = (fileName) => {
4646
return fileName.slice(lastDot).toLowerCase();
4747
};
4848

49+
const toFiniteFileSize = (value) => {
50+
const parsed = Number(value);
51+
if (!Number.isFinite(parsed) || parsed < 0) {
52+
return undefined;
53+
}
54+
55+
return parsed;
56+
};
57+
4958
export const validateFiles = (
5059
selectedFiles,
5160
existingFiles,
@@ -60,7 +69,7 @@ export const validateFiles = (
6069
}
6170

6271
const existingTotalBytes = existingFiles.reduce(
63-
(sum, file) => sum + (file.size || 0),
72+
(sum, file) => sum + (toFiniteFileSize(file.file_size) || 0),
6473
0,
6574
);
6675
const selectedTotalBytes = selectedFiles.reduce(
@@ -109,11 +118,6 @@ const nextId = () => {
109118
const parseApiStatus = (status, fallbackStatus) =>
110119
VALID_ATTACHMENT_STATUSES.has(status) ? status : fallbackStatus;
111120

112-
const normalizeFile = (file) => ({
113-
...file,
114-
status: parseApiStatus(file.status, ATTACHMENT_STATUSES.DELETED),
115-
});
116-
117121
export const summarizeStatuses = (files, copy = DEFAULT_COPY) => {
118122
const counts = {
119123
scanning: 0,
@@ -160,7 +164,12 @@ export const useAttachments = (
160164
copy = DEFAULT_COPY,
161165
fetchFileStatus = null,
162166
) => {
163-
const [files, setFiles] = useState(() => initialFiles.map(normalizeFile));
167+
const [files, setFiles] = useState(() =>
168+
initialFiles.map((file) => ({
169+
...file,
170+
file_size: toFiniteFileSize(file.file_size),
171+
})),
172+
);
164173
const timeoutIdsRef = useRef([]);
165174
const pollTimeoutIdsRef = useRef(new Map());
166175
const isMountedRef = useRef(true);
@@ -297,7 +306,9 @@ export const useAttachments = (
297306
id: fileId,
298307
name:
299308
sourceFile?.name || itemData?.name || `attachment-${nextId()}`,
300-
size: sourceFile?.size || itemData?.file_size || 0,
309+
file_size: toFiniteFileSize(
310+
sourceFile?.size ?? itemData?.file_size,
311+
),
301312
status: parseApiStatus(
302313
itemData?.status,
303314
ATTACHMENT_STATUSES.PENDING_VIRUS_SCAN,

app/assets/stylesheets/tailwind/components/attachments.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,8 @@
6161
transform: translateY(0);
6262
overflow: visible;
6363
}
64+
65+
.attachment-size {
66+
@apply text-gray-700;
67+
font-size: 18px;
68+
}

app/templates/partials/template-attachments-list.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@
2929
data-testid="attachment-row-spinner"
3030
></div>
3131
{% endif %}
32-
<span class="attachment-file-name-truncate">{{ attachment.filename }}</span>
32+
<p class="min-w-0 mb-0">
33+
<span class="attachment-file-name-truncate">{{ attachment.filename }}</span>
34+
{% if attachment.file_size is not none %}
35+
<span class="attachment-size" data-testid="attachment-file-size">
36+
({{ attachment.file_size | filesizeformat }})
37+
</span>
38+
{% endif %}
39+
</p>
3340
</div>
3441
</li>
3542
{% endfor %}

app/templates/views/storybook/attachments.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ <h3 class="heading-small mb-4">Mixed in-progress and attached</h3>
3232
{
3333
"id": "Filename.pdf",
3434
"name": "Filename.pdf",
35-
"size": 320000,
35+
"file_size": 320000,
3636
"status": "pending_virus_scan"
3737
},
3838
{
3939
"id": "Sometimes_you_need_a_very_very_very_very_long_file_name_final_v1_final_final_3_hello.pdf",
4040
"name": "Sometimes_you_need_a_very_very_very_very_long_file_name_final_v1_final_final_3_hello.pdf",
41-
"size": 170000,
41+
"file_size": 170000,
4242
"status": "uploaded"
4343
}
4444
],
@@ -62,13 +62,13 @@ <h3 class="heading-small mb-4">Unsafe file state</h3>
6262
{
6363
"id": "document_with_malware.pdf",
6464
"name": "document_with_malware.pdf",
65-
"size": 470000,
65+
"file_size": 470000,
6666
"status": "virus_scan_failed"
6767
},
6868
{
6969
"id": "safe_permit.pdf",
7070
"name": "safe_permit.pdf",
71-
"size": 260000,
71+
"file_size": 260000,
7272
"status": "uploaded"
7373
}
7474
],
@@ -93,7 +93,7 @@ <h3 class="heading-small mb-4">Interactive status simulation</h3>
9393
{
9494
"id": "already_attached_notice.pdf",
9595
"name": "already_attached_notice.pdf",
96-
"size": 220000,
96+
"file_size": 220000,
9797
"status": "uploaded"
9898
}
9999
],

tests/app/main/views/test_send.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ def test_check_messages_ok_shows_template_attachments_and_scanning_warning(
808808
assert page.select_one("[data-testid='template-attachments-warning']") is not None
809809
assert "guide.pdf" in attachments_section.text
810810
assert "pending.pdf" in attachments_section.text
811+
size_labels = attachments_section.select("[data-testid='attachment-file-size']")
812+
assert len(size_labels) == 2
811813

812814

813815
def test_check_messages_ok_hides_attachment_warning_when_all_uploaded(

0 commit comments

Comments
 (0)