diff --git a/site/src/components/FileUpload/FileUpload.test.tsx b/site/src/components/FileUpload/FileUpload.test.tsx new file mode 100644 index 0000000000000..6042d546e909e --- /dev/null +++ b/site/src/components/FileUpload/FileUpload.test.tsx @@ -0,0 +1,41 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { FileUpload } from "./FileUpload"; +import { ThemeProvider } from "contexts/ThemeProvider"; + +test("accepts files with the correct extension", async () => { + const onUpload = jest.fn(); + + render( + + + , + ); + + const dropZone = screen.getByTestId("drop-zone"); + + const tarFile = new File([""], "file.tar"); + fireEvent.drop(dropZone, { + dataTransfer: { files: [tarFile] }, + }); + expect(onUpload).toBeCalledWith(tarFile); + onUpload.mockClear(); + + const zipFile = new File([""], "file.zip"); + fireEvent.drop(dropZone, { + dataTransfer: { files: [zipFile] }, + }); + expect(onUpload).toBeCalledWith(zipFile); + onUpload.mockClear(); + + const unsupportedFile = new File([""], "file.mp4"); + fireEvent.drop(dropZone, { + dataTransfer: { files: [unsupportedFile] }, + }); + expect(onUpload).not.toHaveBeenCalled(); +}); diff --git a/site/src/components/FileUpload/FileUpload.tsx b/site/src/components/FileUpload/FileUpload.tsx index 534fc05a01095..b24cada59cbda 100644 --- a/site/src/components/FileUpload/FileUpload.tsx +++ b/site/src/components/FileUpload/FileUpload.tsx @@ -8,36 +8,6 @@ import RemoveIcon from "@mui/icons-material/DeleteOutline"; import FileIcon from "@mui/icons-material/FolderOutlined"; import { css, type Interpolation, type Theme } from "@emotion/react"; -const useFileDrop = ( - callback: (file: File) => void, - fileTypeRequired?: string, -): { - onDragOver: (e: DragEvent) => void; - onDrop: (e: DragEvent) => void; -} => { - const onDragOver = (e: DragEvent) => { - e.preventDefault(); - }; - - const onDrop = (e: DragEvent) => { - e.preventDefault(); - const file = e.dataTransfer.files[0]; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- file can be undefined - if (!file) { - return; - } - if (fileTypeRequired && file.type !== fileTypeRequired) { - return; - } - callback(file); - }; - - return { - onDragOver, - onDrop, - }; -}; - export interface FileUploadProps { isUploading: boolean; onUpload: (file: File) => void; @@ -46,8 +16,7 @@ export interface FileUploadProps { removeLabel: string; title: string; description?: ReactNode; - extension?: string; - fileTypeRequired?: string; + extensions?: string[]; } export const FileUpload: FC = ({ @@ -58,10 +27,9 @@ export const FileUpload: FC = ({ removeLabel, title, description, - extension, - fileTypeRequired, + extensions, }) => { - const tarDrop = useFileDrop(onUpload, fileTypeRequired); + const fileDrop = useFileDrop(onUpload, extensions); const inputRef = useRef(null); const clickable = useClickable( () => inputRef.current?.click(), @@ -90,9 +58,10 @@ export const FileUpload: FC = ({ return ( <>
{isUploading ? ( @@ -113,7 +82,7 @@ export const FileUpload: FC = ({ data-testid="file-upload" ref={inputRef} css={styles.input} - accept={extension} + accept={extensions?.map((ext) => `.${ext}`).join(",")} onChange={(event) => { const file = event.currentTarget.files?.[0]; if (file) { @@ -125,6 +94,47 @@ export const FileUpload: FC = ({ ); }; +const useFileDrop = ( + callback: (file: File) => void, + extensions?: string[], +): { + onDragOver: (e: DragEvent) => void; + onDrop: (e: DragEvent) => void; +} => { + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const onDrop = (e: DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0] as File | undefined; + + if (!file) { + return; + } + + if (!extensions) { + callback(file); + return; + } + + const extension = file.name.split(".").pop(); + + if (!extension) { + throw new Error(`File has no extension to compare with ${extensions}`); + } + + if (extensions.includes(extension)) { + callback(file); + } + }; + + return { + onDragOver, + onDrop, + }; +}; + const styles = { root: (theme) => css` display: flex; @@ -151,12 +161,16 @@ const styles = { title: { fontSize: 16, + lineHeight: "1", }, description: (theme) => ({ color: theme.palette.text.secondary, textAlign: "center", maxWidth: 400, + fontSize: 14, + lineHeight: "1.5", + marginTop: 4, }), input: { diff --git a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx index 3b532ed536e67..298f9e50aef9c 100644 --- a/site/src/pages/CreateTemplatePage/TemplateUpload.tsx +++ b/site/src/pages/CreateTemplatePage/TemplateUpload.tsx @@ -18,7 +18,7 @@ export const TemplateUpload: FC = ({ }) => { const description = ( <> - The template has to be a .tar file. You can also use our{" "} + The template has to be a .tar or .zip file. You can also use our{" "} = ({ removeLabel="Remove file" title="Upload template" description={description} - extension=".tar" - fileTypeRequired="application/x-tar" + extensions={["tar", "zip"]} /> ); };