Skip to content

Commit 468cd78

Browse files
authored
Merge pull request #4742 from cloud-gov/file-storage-upload-2274
feat: add file upload component and refactor fetch to work with formData (#2274)
2 parents 655b45d + 86c18e4 commit 468cd78

14 files changed

+1356
-778
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ test-server: ## Run server tests
7070
test-all: ## Run all tests
7171
docker compose --env-file ./services/local/docker.env run --rm app yarn test
7272

73+
test-watch:
74+
docker compose --env-file ./services/local/docker.env run --rm app yarn test:rtl --watch --runInBand --silent=false
75+
7376
everything: # When you switch to a new branch and need to rebuild everything
7477
docker compose -f ./docker-compose.yml -f ./docker-compose.uaa.yml --env-file ./services/local/docker.env down
7578
make rebuild

frontend/hooks/useFileStorage.js

+127-36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2-
import { useRef, useEffect } from 'react';
2+
import { useRef, useEffect, useState } from 'react';
33
import federalist from '@util/federalistApi';
44
import { REFETCH_INTERVAL } from './utils';
55

@@ -10,55 +10,139 @@ const INITIAL_DATA = {
1010
totalItems: 0,
1111
};
1212

13+
const TIMEOUT_DURATION = 5000;
14+
1315
export default function useFileStorage(
1416
fileStorageId,
15-
path = '',
16-
sortKey = null,
17-
sortOrder = null,
17+
path = '/',
18+
sortKey = 'updatedAt',
19+
sortOrder = 'desc',
1820
page = 1,
1921
) {
2022
const previousData = useRef();
2123
const queryClient = useQueryClient();
2224

23-
const { data, isLoading, isFetching, isPending, isPlaceholderData, error } = useQuery({
24-
queryKey: ['fileStorage', fileStorageId, path, sortKey, sortOrder, page],
25-
queryFn: () =>
26-
federalist.fetchPublicFiles(fileStorageId, path, sortKey, sortOrder, page),
27-
refetchInterval: REFETCH_INTERVAL,
28-
refetchIntervalInBackground: false,
29-
enabled: !!fileStorageId,
30-
keepPreviousData: true,
31-
staleTime: 2000,
32-
placeholderData: previousData.current || INITIAL_DATA,
33-
onError: (err) => {
34-
// using an empty string so that we don't end up with "undefined" at the end
35-
throw new Error('Failed to fetch public files ' + (err?.message || ''));
36-
},
37-
});
25+
const [uploadError, setUploadError] = useState(null);
26+
const [uploadSuccess, setUploadSuccess] = useState(null);
27+
const [deleteError, setDeleteError] = useState(null);
28+
const [deleteSuccess, setDeleteSuccess] = useState(null);
29+
const [createFolderError, setCreateFolderError] = useState(null);
30+
const [createFolderSuccess, setCreateFolderSuccess] = useState(null);
31+
const uploadTimeout = useRef(null);
32+
const deleteTimeout = useRef(null);
33+
const createFolderTimeout = useRef(null);
34+
35+
const handleSuccess = (setSuccess, setError, timeoutRef, message) => {
36+
setError(null);
37+
setSuccess(() => {
38+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
39+
timeoutRef.current = setTimeout(() => setSuccess(null), TIMEOUT_DURATION);
40+
return message;
41+
});
42+
queryClient.invalidateQueries({
43+
queryKey: ['fileStorage', fileStorageId, path, sortKey, sortOrder, page],
44+
});
45+
};
46+
47+
const handleError = (setError, setSuccess, timeoutRef, errorMessage) => {
48+
setSuccess(null);
49+
setError(() => {
50+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
51+
timeoutRef.current = setTimeout(() => setError(null), TIMEOUT_DURATION);
52+
return errorMessage;
53+
});
54+
};
3855

56+
const { data, isLoading, isFetching, isPending, isPlaceholderData, isError, error } =
57+
useQuery({
58+
queryKey: ['fileStorage', fileStorageId, path, sortKey, sortOrder, page],
59+
queryFn: () =>
60+
federalist.fetchPublicFiles(fileStorageId, path, sortKey, sortOrder, page),
61+
refetchInterval: REFETCH_INTERVAL,
62+
refetchIntervalInBackground: false,
63+
enabled: !!fileStorageId,
64+
keepPreviousData: true,
65+
staleTime: 2000,
66+
placeholderData: previousData.current || INITIAL_DATA,
67+
onError: (err) => {
68+
// using an empty string so that we don't end up with "undefined" at the end
69+
throw new Error('Failed to fetch public files ' + (err?.message || ''));
70+
},
71+
});
3972
useEffect(() => {
4073
if (data !== undefined) {
4174
previousData.current = data;
4275
}
4376
}, [data]);
4477

78+
useEffect(() => {
79+
return () => {
80+
if (uploadTimeout.current) clearTimeout(uploadTimeout.current);
81+
if (deleteTimeout.current) clearTimeout(deleteTimeout.current);
82+
if (createFolderTimeout.current) clearTimeout(createFolderTimeout.current);
83+
};
84+
}, []);
85+
4586
const deleteMutation = useMutation({
46-
mutationFn: (item) => federalist.deletePublicItem(fileStorageId, item.id),
47-
onSuccess: () => {
48-
// Invalidate the query to refetch the file list after deletion.
49-
return queryClient.invalidateQueries({
50-
queryKey: ['fileStorage', fileStorageId, path, sortKey, sortOrder, page],
51-
});
52-
},
53-
onError: (err) => {
54-
// using an empty string so that we don't end up with "undefined" at the end
55-
throw new Error('Failed to delete file ' + (err?.message || ''));
87+
mutationFn: ({ item }) => federalist.deletePublicItem(fileStorageId, item.id),
88+
onSuccess: () =>
89+
handleSuccess(
90+
setDeleteSuccess,
91+
setDeleteError,
92+
deleteTimeout,
93+
'File deleted successfully.',
94+
),
95+
onError: (err) =>
96+
handleError(
97+
setDeleteError,
98+
setDeleteSuccess,
99+
deleteTimeout,
100+
err?.message || 'Failed to delete file.',
101+
),
102+
});
103+
104+
const uploadMutation = useMutation({
105+
mutationFn: ({ parent = '/', file }) =>
106+
federalist.uploadPublicFile(fileStorageId, parent, file),
107+
onSuccess: () =>
108+
handleSuccess(
109+
setUploadSuccess,
110+
setUploadError,
111+
uploadTimeout,
112+
'File uploaded successfully.',
113+
),
114+
onError: (err, { file }) => {
115+
const errorMessage = err?.message || 'Upload failed.';
116+
const formattedMessage = errorMessage.includes('already exists')
117+
? `A file named "${file.name}" already exists in this folder.`
118+
: errorMessage;
119+
handleError(setUploadError, setUploadSuccess, uploadTimeout, formattedMessage);
56120
},
57121
});
58122

59-
async function deleteItem(item) {
60-
return deleteMutation.mutate(item);
61-
}
123+
const createFolderMutation = useMutation({
124+
mutationFn: ({ parent = '/', name }) =>
125+
federalist.createPublicDirectory(fileStorageId, parent, name),
126+
onSuccess: () =>
127+
handleSuccess(
128+
setCreateFolderSuccess,
129+
setCreateFolderError,
130+
createFolderTimeout,
131+
'Folder created successfully.',
132+
),
133+
onError: (err, { name }) => {
134+
const errorMessage = err?.message || 'Could not create folder.';
135+
const formattedMessage = errorMessage.includes('already exists')
136+
? `A folder named "${name}" already exists in this folder.`
137+
: errorMessage;
138+
handleError(
139+
setCreateFolderError,
140+
setCreateFolderSuccess,
141+
createFolderTimeout,
142+
formattedMessage,
143+
);
144+
},
145+
});
62146

63147
return {
64148
...data,
@@ -67,9 +151,16 @@ export default function useFileStorage(
67151
isFetching,
68152
isPending,
69153
isPlaceholderData,
70-
error,
71-
deleteItem,
72-
deleteError: deleteMutation.error,
73-
deleteSuccess: deleteMutation.isSuccess,
154+
isError,
155+
defaultError: error,
156+
deleteItem: (item) => deleteMutation.mutateAsync({ item }),
157+
deleteError,
158+
deleteSuccess,
159+
uploadFile: (parent, file) => uploadMutation.mutateAsync({ parent, file }),
160+
uploadError,
161+
uploadSuccess,
162+
createFolder: (parent, name) => createFolderMutation.mutateAsync({ parent, name }),
163+
createFolderError,
164+
createFolderSuccess,
74165
};
75166
}

0 commit comments

Comments
 (0)