Skip to content

Commit

Permalink
feat: Upload files (#65)
Browse files Browse the repository at this point in the history
* feat: UploadFiles

* fix: File type and INewFile

* style: Format file and add missing comma

* fix: Dialog show on click

Still in progress, i have this response

msg
:
"There was an error in the upload."
success
:
false

* refactor: file type

* feat: Upload Files

* fix: multiple upload

* fix: lint

* fix:lint

* fix: fragment and button disable

* feat(ui): Animate loader

---------

Co-authored-by: Pedro Andrés Chaparro Quintero <[email protected]>
Co-authored-by: Pedro Andrés Chaparro Quintero <[email protected]>
  • Loading branch information
3 people authored Oct 27, 2023
1 parent 66a5e23 commit c5138ac
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 1 deletion.
34 changes: 34 additions & 0 deletions cypress/e2e/files/upload-files.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { faker } from "@faker-js/faker";

describe("File Upload Tests", () => {
const username = faker.internet.userName();
const password = faker.internet.password({ length: 8 });

it("Uploads a file", () => {
cy.visit("/register");
cy.get("input[name=username]").type(username);
cy.get("input[name=password]").type(password);
cy.get("input[name=confirmPassword]").type(password);
cy.get("button").contains("Submit").click();
cy.url().should("include", "/files");

// Open the modal to upload a file
cy.get("button").contains("Upload File").click();

cy.get("div[role=dialog]").should("be.visible");
cy.get("div[role=dialog]").should("contain", "Select the files to upload");

// Select the file input element
cy.get("input[type=file]").selectFile({
contents: Cypress.Buffer.from("file contents"),
fileName: "file.txt",
lastModified: Date.now()
});

// Submit the form
cy.get("button").contains("Upload files").click();
cy.get("div[role=dialog]").should("not.exist");
// Verify that the file is listed in the table
cy.get("div").should("contain", "file.txt");
});
});
3 changes: 3 additions & 0 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export function Sidebar() {
<button
className="flex w-full items-center justify-center gap-1.5 rounded-full bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
aria-label="Upload files"
onClick={() => {
openDialog(AVAILABLE_DIALOGS.UPLOAD_FILE, null);
}}
>
<FilePlus className="min-w-6 min-h-6" />
<span className="hidden md:inline">Upload File</span>
Expand Down
6 changes: 5 additions & 1 deletion src/context/FilesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface FilesContext {
areFilesLoading: boolean;
files: File[];
addFile: (file: File) => void;
addFiles: (file: File[]) => void;
renameFile: (uuid: string, name: string) => void;
removeFile: (uuid: string) => void;
}
Expand All @@ -18,6 +19,7 @@ const initialValues: FilesContext = {
areFilesLoading: false,
files: [],
addFile: () => {},
addFiles: () => {},
removeFile: () => {},
renameFile: () => {}
};
Expand All @@ -32,7 +34,8 @@ export const FilesContextProvider = ({
const [params, _] = useSearchParams();
const currentDirectory = params.get("directory");

const { areLoading, files, addFile, removeFile, renameFile } = useUserFiles();
const { areLoading, files, addFile, addFiles, removeFile, renameFile } =
useUserFiles();

return (
<FilesContext.Provider
Expand All @@ -41,6 +44,7 @@ export const FilesContextProvider = ({
areFilesLoading: areLoading,
files: files,
addFile: addFile,
addFiles: addFiles,
removeFile: removeFile,
renameFile: renameFile
}}
Expand Down
3 changes: 3 additions & 0 deletions src/context/FilesDialogsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { File } from "../types/entities";
export enum AVAILABLE_DIALOGS {
"CREATE_FOLDER" = "CREATE_FOLDER",
"RENAME_FILE" = "RENAME_FILE",
"UPLOAD_FILE" = "UPLOAD_FILE",
"DELETE_FILE" = "DELETE_FILE",
"ACCESS_MANAGEMENT" = "ACCESS_MANAGEMENT",
"MOVE_FILE" = "MOVE_FILE"
Expand All @@ -20,6 +21,7 @@ const defaultValues: FilesDialogsContext = {
dialogsVisibilityState: {
[AVAILABLE_DIALOGS.CREATE_FOLDER]: false,
[AVAILABLE_DIALOGS.RENAME_FILE]: false,
[AVAILABLE_DIALOGS.UPLOAD_FILE]: false,
[AVAILABLE_DIALOGS.ACCESS_MANAGEMENT]: false,
[AVAILABLE_DIALOGS.MOVE_FILE]: false,
[AVAILABLE_DIALOGS.DELETE_FILE]: false
Expand All @@ -42,6 +44,7 @@ export const FilesDialogsContextProvider = ({
>({
[AVAILABLE_DIALOGS.CREATE_FOLDER]: false,
[AVAILABLE_DIALOGS.RENAME_FILE]: false,
[AVAILABLE_DIALOGS.UPLOAD_FILE]: false,
[AVAILABLE_DIALOGS.DELETE_FILE]: false,
[AVAILABLE_DIALOGS.ACCESS_MANAGEMENT]: false,
[AVAILABLE_DIALOGS.MOVE_FILE]: false
Expand Down
5 changes: 5 additions & 0 deletions src/hooks/useUserFiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const useUserFiles = () => {
setFiles([...files, dir]);
};

const addFiles = (newFiles: File[]) => {
setFiles((prevFiles) => [...prevFiles, ...newFiles]);
};

const removeFile = (uuid: string) => {
setFiles((curr) => curr.filter((file) => file.uuid !== uuid));
};
Expand All @@ -64,6 +68,7 @@ export const useUserFiles = () => {
areLoading,
files,
addFile,
addFiles,
removeFile,
renameFile
};
Expand Down
2 changes: 2 additions & 0 deletions src/screens/FilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useContext } from "react";
import {
CreateFolderDialog,
EditNameDialog,
UploadFileDialog,
AccessManagementDialog,
DeleteFileDialog,
MoveFileDialog
Expand Down Expand Up @@ -55,6 +56,7 @@ export function FilePage() {
)}
</div>
</main>
<UploadFileDialog />
<CreateFolderDialog />
{showRenameDialog && <EditNameDialog />}
{showAccessDialog && <AccessManagementDialog />}
Expand Down
1 change: 1 addition & 0 deletions src/screens/dialogs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { CreateFolderDialog } from "./create-folder-dialog";
export { UploadFileDialog } from "./upload-file-dialog";
export { EditNameDialog } from "./rename-file-dialog";
export { AccessManagementDialog } from "./access-file-dialog";
export { DeleteFileDialog } from "./delete-file-dialog";
Expand Down
98 changes: 98 additions & 0 deletions src/screens/dialogs/upload-file-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useContext, useRef, useState } from "react";
import toast from "react-hot-toast";
import { uploadfileService } from "../../services/files/upload-file.service";
import { Dialog } from "../../components/Dialog";
import { Loader } from "lucide-react";
import {
FilesDialogsContext,
AuthContext,
AVAILABLE_DIALOGS,
FilesContext
} from "../../context/index";

interface INewFile {
isFile: boolean;
isReady: boolean;
name: string;
size: number;
uuid: string;
}

export const UploadFileDialog = () => {
// Dialog state
const { closeDialog, dialogsVisibilityState } =
useContext(FilesDialogsContext);
const [isUploading, setIsUploading] = useState(false);
const isOpen = dialogsVisibilityState[AVAILABLE_DIALOGS.UPLOAD_FILE];
const { addFiles, currentDirectory } = useContext(FilesContext);
const { session } = useContext(AuthContext);

const files: INewFile[] = [];
const fileInputRef = useRef<HTMLInputElement>(null);

const uploadFile = async () => {
setIsUploading(true);
const fileInput = fileInputRef.current;
if (fileInput?.files?.length) {
const fileList = fileInput.files;
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];

const token = session!.token;
const uploadRequest = {
fileContent: file,
fileName: file.name,
location: currentDirectory || "",
token
};
const response = await uploadfileService(uploadRequest);

if (response.success) {
const newFile: INewFile = {
isFile: true,
isReady: true,
name: file.name,
size: 0,
uuid: response.fileUUID!
};
toast.success(`The file ${file.name} has been uploaded successfully`);

files.push(newFile);
} else {
toast.error(response.msg);
}
}
}
addFilesUI();
setIsUploading(false);
};

const addFilesUI = () => {
addFiles(files);
closeDialog(AVAILABLE_DIALOGS.UPLOAD_FILE);
};

return (
<Dialog
isOpen={isOpen}
onClose={() => closeDialog(AVAILABLE_DIALOGS.UPLOAD_FILE)}
title="Select the files to upload"
>
<input
type="file"
ref={fileInputRef}
multiple
aria-label="Select the files"
className="w-full rounded-lg border p-2"
/>
<button
className="hover-bg-blue-700 mt-4 flex rounded-full bg-blue-600 px-4 py-2 text-white"
onClick={uploadFile}
disabled={isUploading}
>
{isUploading && <Loader className="mr-2 animate-spin" />}
Upload files
</button>
</Dialog>
);
};
55 changes: 55 additions & 0 deletions src/services/files/upload-file.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import axios from "axios";
import { AxiosError } from "axios";
import { ENVIRONMENT } from "../../config/environment";

type UploadfileRequest = {
fileContent: File;
fileName: string;
location: string;
token: string;
};

type UploadfileResponse = {
msg: string;
success: boolean;
fileUUID?: string;
};

export const uploadfileService = async (
req: UploadfileRequest
): Promise<UploadfileResponse> => {
try {
const formData = new FormData();
formData.append("file", req.fileContent);
formData.append("fileName", req.fileName);
formData.append("location", req.location);

const response = await axios.post(
`${ENVIRONMENT.PROXY_BASE_URL}/file/upload`,
formData,
{
headers: {
"Authorization": `Bearer ${req.token}`,
"Content-Type": "multipart/form-data"
}
}
);

return {
success: true,
msg: "Upload successful",
fileUUID: response.data.fileUUID
};
} catch (error) {
let errorMsg = "There was an error in the upload.";

if (error instanceof AxiosError) {
errorMsg = error.response?.data.msg || errorMsg;
}

return {
msg: errorMsg,
success: false
};
}
};

0 comments on commit c5138ac

Please sign in to comment.