Skip to content

Commit e050101

Browse files
committed
feat(upload): Add file upload for projects
1 parent fa24d66 commit e050101

File tree

4 files changed

+234
-49
lines changed

4 files changed

+234
-49
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { css } from '@emotion/react';
2+
import { Typography, useTheme } from '@material-ui/core';
3+
import { Alert, AlertTitle } from '@material-ui/lab';
4+
5+
export interface FileHoverCoverProps {
6+
active: boolean;
7+
}
8+
9+
export const FileHoverCover = ({ active }: FileHoverCoverProps) => {
10+
const theme = useTheme();
11+
12+
return (
13+
<>
14+
<div
15+
css={css`
16+
position: absolute;
17+
left: 0;
18+
right: 0;
19+
top: 0;
20+
bottom: 0;
21+
background-color: ${theme.palette.grey[600]};
22+
opacity: ${active ? '40%' : '0%'};
23+
transition: ${theme.transitions.easing.easeIn} opacity
24+
${theme.transitions.duration.shortest}ms;
25+
`}
26+
/>
27+
28+
<div
29+
css={css`
30+
position: absolute;
31+
left: 0;
32+
right: 0;
33+
top: 50px;
34+
display: flex;
35+
justify-content: center;
36+
align-items: center;
37+
opacity: ${active ? '100%' : '0%'};
38+
transition: ${theme.transitions.easing.easeIn} opacity
39+
${theme.transitions.duration.shortest}ms;
40+
`}
41+
>
42+
<Alert severity="info">
43+
<AlertTitle>
44+
<Typography component="h2" variant="h3">
45+
Upload File to Current Project
46+
</Typography>
47+
</AlertTitle>
48+
<Typography component="p" variant="h4">
49+
Drag and drop files here
50+
</Typography>
51+
</Alert>
52+
</div>
53+
</>
54+
);
55+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { ReactNode } from 'react';
2+
import { useCallback } from 'react';
3+
import type { DropzoneState, FileRejection } from 'react-dropzone';
4+
import { useDropzone } from 'react-dropzone';
5+
import { useQueryClient } from 'react-query';
6+
7+
import { getGetFilesQueryKey } from '@squonk/data-manager-client/file';
8+
import { useAddFileToProject } from '@squonk/data-manager-client/project';
9+
10+
import { css } from '@emotion/react';
11+
import { useSnackbar } from 'notistack';
12+
13+
import { useCurrentProjectId } from '../../hooks/projectHooks';
14+
import { useProjectBreadcrumbs } from '../../hooks/projectPathHooks';
15+
import { useFileExtensions } from '../../hooks/useFileExtensions';
16+
import { getErrorMessage } from '../../utils/orvalError';
17+
import { FileHoverCover } from './FileHoverCover';
18+
19+
export interface ProjectFileUploadProps {
20+
children: (open: DropzoneState['open']) => ReactNode;
21+
}
22+
23+
export const ProjectFileUpload = ({ children }: ProjectFileUploadProps) => {
24+
const allowedFileTypes = useFileExtensions();
25+
26+
const { projectId } = useCurrentProjectId();
27+
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
28+
29+
const breadcrumbs = useProjectBreadcrumbs();
30+
const path = '/' + breadcrumbs.join('/');
31+
32+
const { mutateAsync: uploadProjectFile } = useAddFileToProject();
33+
34+
const queryClient = useQueryClient();
35+
36+
const uploadFile = useCallback(
37+
async (file: File) => {
38+
const key = enqueueSnackbar(`Uploading file ${file.name}`, { autoHideDuration: 10000 });
39+
if (projectId) {
40+
try {
41+
await uploadProjectFile({
42+
projectid: projectId,
43+
data: { as_filename: file.name, file, path },
44+
});
45+
enqueueSnackbar(`${file.name} was uploaded`, { variant: 'success' });
46+
queryClient.invalidateQueries(getGetFilesQueryKey({ project_id: projectId, path }));
47+
} catch (err) {
48+
const error = getErrorMessage(err);
49+
enqueueSnackbar(`The upload of ${file.name} failed: ${error}`, { variant: 'error' });
50+
}
51+
closeSnackbar(key);
52+
}
53+
},
54+
[closeSnackbar, enqueueSnackbar, path, projectId, queryClient, uploadProjectFile],
55+
);
56+
57+
const onDrop = useCallback(
58+
(acceptedFiles: File[], rejections: FileRejection[]) => {
59+
// Upload each valid file and display updates with notistack
60+
for (const file of acceptedFiles) {
61+
uploadFile(file);
62+
}
63+
64+
// Display rejected files and notistack errors
65+
for (const rejection of rejections) {
66+
enqueueSnackbar(`${rejection.file.name} was rejected`, { variant: 'error' });
67+
}
68+
},
69+
[enqueueSnackbar, uploadFile],
70+
);
71+
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
72+
onDrop,
73+
noClick: true,
74+
accept: allowedFileTypes ?? [],
75+
});
76+
77+
return (
78+
<div
79+
css={css`
80+
position: relative;
81+
`}
82+
{...getRootProps()}
83+
>
84+
<FileHoverCover active={isDragActive} />
85+
<input {...getInputProps()} />
86+
{children(open)}
87+
</div>
88+
);
89+
};

components/ProjectTable/ProjectTable.tsx

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { useCallback, useMemo } from 'react';
2+
import type { DropzoneState } from 'react-dropzone';
23
import type { Cell, CellProps, Column, PluginHook } from 'react-table';
34

45
import type { ProjectDetail } from '@squonk/data-manager-client';
56

67
import { css } from '@emotion/react';
7-
import { Breadcrumbs, CircularProgress, Link, Typography, useTheme } from '@material-ui/core';
8+
import {
9+
Breadcrumbs,
10+
CircularProgress,
11+
Grid,
12+
IconButton,
13+
Link,
14+
Typography,
15+
useTheme,
16+
} from '@material-ui/core';
17+
import CloudUploadRoundedIcon from '@material-ui/icons/CloudUploadRounded';
818
import FolderRoundedIcon from '@material-ui/icons/FolderRounded';
919
import fileSize from 'filesize';
1020
import dynamic from 'next/dynamic';
@@ -28,17 +38,21 @@ const FileActions = dynamic<FileActionsProps>(
2838
},
2939
);
3040

31-
export interface ProjectTable {
41+
export interface ProjectTableProps {
3242
/**
3343
* Project detailing the files to be displayed
3444
*/
3545
currentProject: ProjectDetail;
46+
/**
47+
* Functions that returns props for an input element that opens the file selection UI
48+
*/
49+
openUploadDialog: DropzoneState['open'];
3650
}
3751

3852
/**
3953
* Data table displaying a project's files with actions to manage the files.
4054
*/
41-
export const ProjectTable = ({ currentProject }: ProjectTable) => {
55+
export const ProjectTable = ({ currentProject, openUploadDialog }: ProjectTableProps) => {
4256
const theme = useTheme();
4357

4458
const router = useRouter();
@@ -162,29 +176,49 @@ export const ProjectTable = ({ currentProject }: ProjectTable) => {
162176
isError={isError}
163177
isLoading={isLoading}
164178
ToolbarChild={
165-
<Breadcrumbs>
166-
{['root', ...breadcrumbs].map((path, pathIndex) =>
167-
pathIndex < breadcrumbs.length ? (
168-
<NextLink
169-
passHref
170-
href={{
171-
pathname: router.pathname,
172-
query: {
173-
project: currentProject.project_id,
174-
path: breadcrumbs.slice(0, pathIndex),
175-
},
176-
}}
177-
key={`${pathIndex}-${path}`}
178-
>
179-
<Link color="inherit" component="button" variant="body1">
180-
{path}
181-
</Link>
182-
</NextLink>
183-
) : (
184-
<Typography key={`${pathIndex}-${path}`}>{path}</Typography>
185-
),
186-
)}
187-
</Breadcrumbs>
179+
<Grid container>
180+
<Grid
181+
item
182+
css={css`
183+
display: flex;
184+
align-items: center;
185+
`}
186+
>
187+
<Breadcrumbs>
188+
{['root', ...breadcrumbs].map((path, pathIndex) =>
189+
pathIndex < breadcrumbs.length ? (
190+
<NextLink
191+
passHref
192+
href={{
193+
pathname: router.pathname,
194+
query: {
195+
project: currentProject.project_id,
196+
path: breadcrumbs.slice(0, pathIndex),
197+
},
198+
}}
199+
key={`${pathIndex}-${path}`}
200+
>
201+
<Link color="inherit" component="button" variant="body1">
202+
{path}
203+
</Link>
204+
</NextLink>
205+
) : (
206+
<Typography key={`${pathIndex}-${path}`}>{path}</Typography>
207+
),
208+
)}
209+
</Breadcrumbs>
210+
</Grid>
211+
<Grid
212+
item
213+
css={css`
214+
margin-left: auto;
215+
`}
216+
>
217+
<IconButton onClick={openUploadDialog}>
218+
<CloudUploadRoundedIcon />
219+
</IconButton>
220+
</Grid>
221+
</Grid>
188222
}
189223
useActionsColumnPlugin={useActionsColumnPlugin}
190224
/>

pages/project.tsx

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Image from 'next/image';
99
import { CenterLoader } from '../components/CenterLoader';
1010
import Layout from '../components/Layout';
1111
import { ProjectTable } from '../components/ProjectTable';
12+
import { ProjectFileUpload } from '../components/ProjectTable/ProjectFileUpload';
1213
import { OrganisationAutocomplete } from '../components/userContext/OrganisationAutocomplete';
1314
import { ProjectAutocomplete } from '../components/userContext/ProjectAutocomplete';
1415
import { UnitAutocomplete } from '../components/userContext/UnitAutocomplete';
@@ -33,30 +34,36 @@ const Project = () => {
3334
{isLoading ? (
3435
<CenterLoader />
3536
) : currentProject ? (
36-
<Grid
37-
container
38-
css={css`
39-
display: flex;
40-
align-items: center;
41-
`}
42-
>
43-
<Grid item md={6} xs={12}>
44-
<Typography
45-
gutterBottom
46-
component="h1"
47-
css={css`
48-
word-break: break-all;
49-
`}
50-
variant={currentProject.name.length > 16 ? 'h2' : 'h1'}
51-
>
52-
Project: {currentProject.name}
53-
</Typography>
54-
</Grid>
55-
<Grid item md={6} xs={12}>
56-
<ProjectAutocomplete size="medium" />
37+
<>
38+
<Grid
39+
container
40+
css={css`
41+
display: flex;
42+
align-items: center;
43+
`}
44+
>
45+
<Grid item md={6} xs={12}>
46+
<Typography
47+
gutterBottom
48+
component="h1"
49+
css={css`
50+
word-break: break-all;
51+
`}
52+
variant={currentProject.name.length > 16 ? 'h2' : 'h1'}
53+
>
54+
Project: {currentProject.name}
55+
</Typography>
56+
</Grid>
57+
<Grid item md={6} xs={12}>
58+
<ProjectAutocomplete size="medium" />
59+
</Grid>
5760
</Grid>
58-
<ProjectTable currentProject={currentProject} />
59-
</Grid>
61+
<ProjectFileUpload>
62+
{(open) => (
63+
<ProjectTable currentProject={currentProject} openUploadDialog={open} />
64+
)}
65+
</ProjectFileUpload>
66+
</>
6067
) : (
6168
<div
6269
css={css`

0 commit comments

Comments
 (0)