Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ An open-source React.js package for easy integration of a file manager into appl
- **Navigation**: Use the breadcrumb trail and sidebar navigation pane for quick directory traversal.
- **Toolbar & Context Menu**: Access all common actions (upload, download, delete, copy, move, rename, etc.) via the toolbar or right-click for the same options in the context menu.
- **Multi-Selection**: Select multiple files and folders at once to perform bulk actions like delete, copy, move, or download.
- **Keyboard Shortcuts**: Quickly perform file operations like copy, paste, delete, and more using intuitive keyboard shortcuts.

![React File Manager](https://github.com/user-attachments/assets/e68f750b-86bf-450d-b27e-fd3dedebf1bd)

Expand Down Expand Up @@ -110,6 +111,26 @@ type File = {
| `onRename` | (file: [File](#-file-structure), newName: string) => void | A callback function triggered when a file or folder is renamed. |
| `width` | string \| number | The width of the component `default: 100%`. Can be a string (e.g., `'100%'`, `'10rem'`) or a number (in pixels). |

## ⌨️ Keyboard Shortcuts

| **Action** | **Shortcut** |
| ------------------------------ | ------------------ |
| New Folder | `Alt + Shift + N` |
| Upload Files | `CTRL + U` |
| Cut | `CTRL + X` |
| Copy | `CTRL + C` |
| Paste | `CTRL + V` |
| Rename | `F2` |
| Download | `CTRL + D` |
| Delete | `DEL` |
| Select All Files | `CTRL + A` |
| Jump to First File in the List | `Home` |
| Jump to Last File in the List | `End` |
| Switch to List Layout | `CTRL + Shift + 1` |
| Switch to Grid Layout | `CTRL + Shift + 2` |
| Refresh File List | `F5` |
| Clear Selection | `Esc` |

## 🤝 Contributing

Contributions are welcome! To contribute:
Expand Down
10 changes: 5 additions & 5 deletions backend/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ The backend supports the following file system operations:

- **📁 Create a Folder**: `/folder`
- **⬆️ Upload a File**: `/upload`
- **📋 Copy a File/Folder**: `/copy`
- **📋 Copy File(s) or Folder(s)**: `/copy`
- **📂 Get All Files/Folders**: `/`
- **⬇️ Download a File**: `/download/:id`
- **📤 Move a File/Folder**: `/move`
- **✏️ Rename a File/Folder**: `/rename`
- **🗑️ Delete a File/Folder**: `/:id`
- **⬇️ Download File(s) or Folder(s)**: `/download`
- **📤 Move File(s) or Folder(s)**: `/move`
- **✏️ Rename a File or Folder**: `/rename`
- **🗑️ Delete File(s) or Folder(s)**: `/`

Refer to the [Swagger Documentation](http://localhost:3000/api-docs/) for detailed request/response formats.

Expand Down
2 changes: 1 addition & 1 deletion backend/app/controllers/deleteItem.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const deleteRecursive = async (item) => {
};

const deleteItem = async (req, res) => {
// #swagger.summary = 'Deletes a file/folder(s).'
// #swagger.summary = 'Deletes file/folder(s).'
/* #swagger.parameters['body'] = {
in: 'body',
required: true,
Expand Down
3 changes: 2 additions & 1 deletion backend/app/controllers/downloadFile.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const mongoose = require("mongoose");
const archiver = require("archiver");

const downloadFile = async (req, res) => {
// #swagger.summary = 'Downloads a file.'
// Todo: Update download request query swagger docs.
// #swagger.summary = 'Downloads file/folder(s).'
/* #swagger.parameters['filePath'] = {
in: 'query',
type: 'string',
Expand Down
21 changes: 21 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ An open-source React.js package for easy integration of a file manager into appl
- **Navigation**: Use the breadcrumb trail and sidebar navigation pane for quick directory traversal.
- **Toolbar & Context Menu**: Access all common actions (upload, download, delete, copy, move, rename, etc.) via the toolbar or right-click for the same options in the context menu.
- **Multi-Selection**: Select multiple files and folders at once to perform bulk actions like delete, copy, move, or download.
- **Keyboard Shortcuts**: Quickly perform file operations like copy, paste, delete, and more using intuitive keyboard shortcuts.

![React File Manager](https://github.com/user-attachments/assets/e68f750b-86bf-450d-b27e-fd3dedebf1bd)

Expand Down Expand Up @@ -110,6 +111,26 @@ type File = {
| `onRename` | (file: [File](#-file-structure), newName: string) => void | A callback function triggered when a file or folder is renamed. |
| `width` | string \| number | The width of the component `default: 100%`. Can be a string (e.g., `'100%'`, `'10rem'`) or a number (in pixels). |

## ⌨️ Keyboard Shortcuts

| **Action** | **Shortcut** |
| ------------------------------ | ------------------ |
| New Folder | `Alt + Shift + N` |
| Upload Files | `CTRL + U` |
| Cut | `CTRL + X` |
| Copy | `CTRL + C` |
| Paste | `CTRL + V` |
| Rename | `F2` |
| Download | `CTRL + D` |
| Delete | `DEL` |
| Select All Files | `CTRL + A` |
| Jump to First File in the List | `Home` |
| Jump to Last File in the List | `End` |
| Switch to List Layout | `CTRL + Shift + 1` |
| Switch to Grid Layout | `CTRL + Shift + 2` |
| Refresh File List | `F5` |
| Clear Selection | `Esc` |

## 🤝 Contributing

Contributions are welcome! To contribute:
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/FileManager/Actions/Actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import DeleteAction from "./Delete/Delete.action";
import UploadFileAction from "./UploadFile/UploadFile.action";
import PreviewFileAction from "./PreviewFile/PreviewFile.action";
import { useSelection } from "../../contexts/SelectionContext";
import { useShortcutHandler } from "../../hooks/useShortcutHandler";

const Actions = ({
fileUploadConfig,
onFileUploading,
onFileUploaded,
onDelete,
onRefresh,
maxFileSize,
filePreviewPath,
acceptedFileTypes,
Expand All @@ -18,6 +20,9 @@ const Actions = ({
const [activeAction, setActiveAction] = useState(null);
const { selectedFiles } = useSelection();

// Triggers all the keyboard shortcuts based actions
useShortcutHandler(triggerAction, onRefresh);

const actionTypes = {
uploadFile: {
title: "Upload",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const CreateFolderAction = ({ filesViewRef, file, onCreateFolder, triggerAction

// Validate folder name and call "onCreateFolder" function
const handleValidateFolderName = (e) => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
handleFolderCreating();
return;
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/FileManager/Actions/Rename/Rename.action.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const RenameAction = ({ filesViewRef, file, onRename, triggerAction }) => {
});

const handleValidateFolderRename = (e) => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
outsideClick.setIsClicked(true);
return;
}
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/FileManager/Actions/UploadFile/UploadFile.action.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import Button from "../../../components/Button/Button";
import { AiOutlineCloudUpload } from "react-icons/ai";
import UploadItem from "./UploadItem";
Expand All @@ -21,6 +21,14 @@ const UploadFileAction = ({
const [isUploading, setIsUploading] = useState({});
const { currentFolder, currentPathFiles } = useFileNavigation();
const { onError } = useFiles();
const fileInputRef = useRef(null);

// To open choose file if the "Choose File" button is focused and Enter key is pressed
const handleChooseFileKeyDown = (e) => {
if (e.key === "Enter") {
fileInputRef.current.click();
}
};

const checkFileError = (file) => {
const extError = !acceptedFileTypes.includes(getFileExtension(file.name));
Expand Down Expand Up @@ -104,9 +112,10 @@ const UploadFileAction = ({
</div>
</div>
<div className="btn-choose-file">
<Button padding="0">
<Button padding="0" onKeyDown={handleChooseFileKeyDown}>
<label htmlFor="chooseFile">Choose File</label>
<input
ref={fileInputRef}
type="file"
id="chooseFile"
className="choose-file-input"
Expand Down
9 changes: 1 addition & 8 deletions frontend/src/FileManager/FileList/FileItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const FileItem = ({
onFileOpen,
filesViewRef,
selectedFileIndexes,
setSelectedFileIndexes,
triggerAction,
handleContextMenu,
setLastSelectedFile,
Expand All @@ -44,7 +43,6 @@ const FileItem = ({
onFileOpen(file);
if (file.isDirectory) {
setCurrentPath(file.path);
setSelectedFileIndexes([]);
setSelectedFiles([]);
} else {
enableFilePreview && triggerAction.show("previewFile");
Expand All @@ -56,7 +54,6 @@ const FileItem = ({
if (file.isEditing) return;

setSelectedFiles([file]);
setSelectedFileIndexes([index]);
const currentTime = new Date().getTime();
if (currentTime - lastClickTime < 300) {
handleFileAccess();
Expand All @@ -66,10 +63,9 @@ const FileItem = ({
};

const handleOnKeyDown = (e) => {
e.stopPropagation();
if (e.key === "Enter") {
e.stopPropagation();
setSelectedFiles([file]);
setSelectedFileIndexes([index]);
handleFileAccess();
}
};
Expand All @@ -82,7 +78,6 @@ const FileItem = ({

if (!fileSelected) {
setSelectedFiles([file]);
setSelectedFileIndexes([index]);
}

setLastSelectedFile(file);
Expand All @@ -101,10 +96,8 @@ const FileItem = ({
const handleCheckboxChange = (e) => {
if (e.target.checked) {
setSelectedFiles((prev) => [...prev, file]);
setSelectedFileIndexes((prev) => [...prev, index]);
} else {
setSelectedFiles((prev) => prev.filter((f) => f.name !== file.name && f.path !== file.path));
setSelectedFileIndexes((prev) => prev.filter((i) => i !== index));
}

setFileSelected(e.target.checked);
Expand Down
60 changes: 22 additions & 38 deletions frontend/src/FileManager/FileList/FileList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,7 @@ import { FaRegFile, FaRegPaste } from "react-icons/fa6";
import { BiRename } from "react-icons/bi";
import { useClipBoard } from "../../contexts/ClipboardContext";

const FileList = ({
onCreateFolder,
onPaste,
onRename,
onDownload,
onFileOpen,
enableFilePreview,
triggerAction,
}) => {
const FileList = ({ onCreateFolder, onRename, onFileOpen, enableFilePreview, triggerAction }) => {
const [selectedFileIndexes, setSelectedFileIndexes] = useState([]);
const [visible, setVisible] = useState(false);
const [isSelectionCtx, setIsSelectionCtx] = useState(false);
Expand All @@ -33,8 +25,8 @@ const FileList = ({
const { currentPath, setCurrentPath, currentPathFiles, setCurrentPathFiles } =
useFileNavigation();
const filesViewRef = useRef(null);
const { selectedFiles, setSelectedFiles } = useSelection();
const { clipBoard, setClipBoard } = useClipBoard();
const { selectedFiles, setSelectedFiles, handleDownload } = useSelection();
const { clipBoard, handleCutCopy, handlePasting } = useClipBoard();
const { activeLayout } = useLayout();
const contextMenuRef = useDetectOutsideClick(() => setVisible(false));

Expand Down Expand Up @@ -65,12 +57,12 @@ const FileList = ({
{
title: "Cut",
icon: <BsScissors size={19} />,
onClick: () => handleCutCopy(true),
onClick: () => handleMoveOrCopyItems(true),
},
{
title: "Copy",
icon: <BsCopy strokeWidth={0.1} size={17} />,
onClick: () => handleCutCopy(false),
onClick: () => handleMoveOrCopyItems(false),
},
{
title: "Paste",
Expand All @@ -88,7 +80,7 @@ const FileList = ({
{
title: "Download",
icon: <MdOutlineFileDownload size={18} />,
onClick: handleDownload,
onClick: handleDownloadItems,
hidden: lastSelectedFile?.isDirectory,
},
{
Expand All @@ -109,24 +101,13 @@ const FileList = ({
setVisible(false);
}

function handleCutCopy(isMoving) {
setClipBoard({
files: selectedFiles,
isMoving: isMoving,
});
function handleMoveOrCopyItems(isMoving) {
handleCutCopy(isMoving);
setVisible(false);
}

function handleFilePasting() {
if (clipBoard) {
const copiedFiles = clipBoard.files;
const operationType = clipBoard.isMoving ? "move" : "copy";

onPaste(copiedFiles, lastSelectedFile, operationType);

clipBoard.isMoving && setClipBoard(null);
setSelectedFiles([]);
}
handlePasting(lastSelectedFile);
setVisible(false);
}

Expand All @@ -135,8 +116,8 @@ const FileList = ({
triggerAction.show("rename");
}

function handleDownload() {
onDownload(selectedFiles);
function handleDownloadItems() {
handleDownload();
setVisible(false);
}

Expand All @@ -162,8 +143,8 @@ const FileList = ({

const handleItemRenaming = () => {
setCurrentPathFiles((prev) => {
if (prev[selectedFileIndexes[0]]) {
prev[selectedFileIndexes[0]].isEditing = true;
if (prev[selectedFileIndexes.at(-1)]) {
prev[selectedFileIndexes.at(-1)].isEditing = true;
}
return prev;
});
Expand Down Expand Up @@ -198,10 +179,16 @@ const FileList = ({
}, [currentPath]);

useEffect(() => {
if (selectedFiles.length === 0) {
if (selectedFiles.length > 0) {
setSelectedFileIndexes(() => {
return selectedFiles.map((selectedFile) => {
return currentPathFiles.findIndex((f) => f.path === selectedFile.path);
});
});
} else {
setSelectedFileIndexes([]);
}
}, [selectedFiles]);
}, [selectedFiles, currentPathFiles]);

return (
<div
Expand All @@ -210,7 +197,7 @@ const FileList = ({
onContextMenu={(e) => handleContextMenu(e, false)}
onClick={() => {
setSelectedFileIndexes([]);
setSelectedFiles([]);
setSelectedFiles((prev) => (prev.length > 0 ? [] : prev));
}}
>
{activeLayout === "list" && (
Expand All @@ -228,14 +215,11 @@ const FileList = ({
index={index}
file={file}
onCreateFolder={onCreateFolder}
onPaste={onPaste}
onRename={onRename}
onDownload={onDownload}
onFileOpen={onFileOpen}
enableFilePreview={enableFilePreview}
filesViewRef={filesViewRef}
selectedFileIndexes={selectedFileIndexes}
setSelectedFileIndexes={setSelectedFileIndexes}
triggerAction={triggerAction}
handleContextMenu={handleContextMenu}
setVisible={setVisible}
Expand Down
Loading