Skip to content
Open
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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ type File = {
path: string;
updatedAt?: string; // Optional: Last update timestamp in ISO 8601 format
size?: number; // Optional: File size in bytes (only applicable for files)
customActions?: object // Optional: used to add custom buttons on the file. See `Custom File actions` part
};
```

Expand Down Expand Up @@ -201,6 +202,45 @@ const CustomImagePreviewer = ({ file }) => {
filePreviewComponent={(file) => <CustomImagePreviewer file={file} />}
/>;
```
## Custom File actions

A file can specify custom actions that can be displayed in the context menu and the toolbar menu.

```jsx
const customButtonExample = {
inContextMenu : true, // true to display in the context menu
inToolbarMenu : true, // true to display in the toolbar when the file is selected
enableMultiItems : true, // true to display the button when multiple files are selected, and all of them use the same button
hideContextMenuOnClick : false, // true to hide on click in the context menu
title : "customButton", // text displayed
icon : <BiAperture />, // icon of the button
onClick : (selectedFiles) => {console.log("click on custom button, with selected files : ", selectedFiles)}
}

const files = [
{
name: "Documents",
isDirectory: true, // Folder
path: "/Documents", // Located in Root directory
updatedAt: "2024-09-09T10:30:00Z", // Last updated time
customActions : [customButtonExample]
},
{
name: "Pictures",
isDirectory: true,
path: "/Pictures", // Located in Root directory as well
updatedAt: "2024-09-09T11:00:00Z",
customActions : [customButtonExample]
},
{
name: "Pic.png",
isDirectory: false, // File
path: "/Pictures/Pic.png", // Located inside the "Pictures" folder
updatedAt: "2024-09-08T16:45:00Z",
size: 2048, // File size in bytes (example: 2 KB)
},
]
```

## 🧭 Handling Current Path

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ import { getAllFilesAPI } from "./api/getAllFilesAPI";
import { renameAPI } from "./api/renameAPI";
import "./App.scss";
import FileManager from "./FileManager/FileManager";
import { BiAperture } from "react-icons/bi";

const customButtonExample = {
inContextMenu : true,
inToolbarMenu : true,
enableMultiItems : true,
hideContextMenuOnClick : false,
title : "customButton",
icon : <BiAperture />,
onClick : (selectedFiles) => {console.log("click on custom button, with selected files : ", selectedFiles)}
}

function App() {
const fileUploadConfig = {
Expand All @@ -22,6 +33,11 @@ function App() {
setIsLoading(true);
const response = await getAllFilesAPI();
if (response.status === 200 && response.data) {
response.data.forEach(x=> {
if(x.name.startsWith("_custom")) {
x.customActions = [customButtonExample];
}
})
setFiles(response.data);
} else {
console.error(response);
Expand Down
126 changes: 77 additions & 49 deletions frontend/src/FileManager/FileList/useFileList.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BiRename, BiSelectMultiple } from "react-icons/bi";
import { BiRename, BiSelectMultiple, BiQuestionMark } from "react-icons/bi";
import { BsCopy, BsFolderPlus, BsGrid, BsScissors } from "react-icons/bs";
import { FaListUl, FaRegFile, FaRegPaste } from "react-icons/fa6";
import { FiRefreshCw } from "react-icons/fi";
Expand Down Expand Up @@ -140,54 +140,82 @@ const useFileList = (onRefresh, enableFilePreview, triggerAction, permissions, o
},
];

const selecCtxItems = [
{
title: t("open"),
icon: lastSelectedFile?.isDirectory ? <PiFolderOpen size={20} /> : <FaRegFile size={16} />,
onClick: handleFileOpen,
divider: true,
},
{
title: t("cut"),
icon: <BsScissors size={19} />,
onClick: () => handleMoveOrCopyItems(true),
divider: !lastSelectedFile?.isDirectory && !permissions.copy,
hidden: !permissions.move,
},
{
title: t("copy"),
icon: <BsCopy strokeWidth={0.1} size={17} />,
onClick: () => handleMoveOrCopyItems(false),
divider: !lastSelectedFile?.isDirectory,
hidden: !permissions.copy,
},
{
title: t("paste"),
icon: <FaRegPaste size={18} />,
onClick: handleFilePasting,
className: `${clipBoard ? "" : "disable-paste"}`,
hidden: !lastSelectedFile?.isDirectory || (!permissions.move && !permissions.copy),
divider: true,
},
{
title: t("rename"),
icon: <BiRename size={19} />,
onClick: handleRenaming,
hidden: selectedFiles.length > 1 || !permissions.rename,
},
{
title: t("download"),
icon: <MdOutlineFileDownload size={18} />,
onClick: handleDownloadItems,
hidden: !permissions.download,
},
{
title: t("delete"),
icon: <MdOutlineDelete size={19} />,
onClick: handleDelete,
hidden: !permissions.delete,
},
];
let customButtons = [];

if(lastSelectedFile?.customActions) {
// Handle custom actions, that are tagged 'inContextMenu'
customButtons = lastSelectedFile.customActions.filter(x => x.inContextMenu).map(customButton => {
let hidden = (selectedFiles.length == 1) ? false : true;
if(selectedFiles.length > 1 && customButton.enableMultiItems) {
// Display button only if all the selected items have the same button
hidden = !selectedFiles.every(x=>x.customActions && x.customActions.includes(customButton))
}
return {
title: customButton.title ?? "??",
icon: customButton.icon ? customButton.icon : <BiQuestionMark />,
hidden,
onClick: () => {
if(customButton.onClick) {
// Send selected files as argument, to make it work the same for multi and single selection
customButton.onClick(selectedFiles);
} else {
console.warn("no onClick specified for this custom button");
}
setVisible(customButton.hideContextMenuOnClick === undefined ? false : !customButton.hideContextMenuOnClick);
},
divider: customButton.divider ? customButton.divider : false,
};
})
}

const selecCtxItems = customButtons.concat([
{
title: t("open"),
icon: lastSelectedFile?.isDirectory ? <PiFolderOpen size={20} /> : <FaRegFile size={16} />,
onClick: handleFileOpen,
divider: true,
},
{
title: t("cut"),
icon: <BsScissors size={19} />,
onClick: () => handleMoveOrCopyItems(true),
divider: !lastSelectedFile?.isDirectory && !permissions.copy,
hidden: !permissions.move,
},
{
title: t("copy"),
icon: <BsCopy strokeWidth={0.1} size={17} />,
onClick: () => handleMoveOrCopyItems(false),
divider: !lastSelectedFile?.isDirectory,
hidden: !permissions.copy,
},
{
title: t("paste"),
icon: <FaRegPaste size={18} />,
onClick: handleFilePasting,
className: `${clipBoard ? "" : "disable-paste"}`,
hidden: !lastSelectedFile?.isDirectory || (!permissions.move && !permissions.copy),
divider: true,
},
{
title: t("rename"),
icon: <BiRename size={19} />,
onClick: handleRenaming,
hidden: selectedFiles.length > 1 || !permissions.rename,
},
{
title: t("download"),
icon: <MdOutlineFileDownload size={18} />,
onClick: handleDownloadItems,
hidden: !permissions.download,
},
{
title: t("delete"),
icon: <MdOutlineDelete size={19} />,
onClick: handleDelete,
hidden: !permissions.delete,
},
]);
//

const handleFolderCreating = () => {
Expand Down
29 changes: 28 additions & 1 deletion frontend/src/FileManager/Toolbar/Toolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
MdOutlineFileDownload,
MdOutlineFileUpload,
} from "react-icons/md";
import { BiRename } from "react-icons/bi";
import { BiRename, BiQuestionMark } from "react-icons/bi";
import { FaListUl, FaRegPaste } from "react-icons/fa6";
import LayoutToggler from "./LayoutToggler";
import { useFileNavigation } from "../../contexts/FileNavigationContext";
Expand Down Expand Up @@ -75,6 +75,7 @@ const Toolbar = ({ onLayoutChange, onRefresh, triggerAction, permissions }) => {

// Selected File/Folder Actions
if (selectedFiles.length > 0) {
let customButtonId = 0;
return (
<div className="toolbar file-selected">
<div className="file-action-container">
Expand Down Expand Up @@ -125,6 +126,32 @@ const Toolbar = ({ onLayoutChange, onRefresh, triggerAction, permissions }) => {
<span>{t("delete")}</span>
</button>
)}
{selectedFiles.length > 0 && selectedFiles[0].customActions &&
selectedFiles[0].customActions.filter(x => x.inToolbarMenu)
.filter(customButton => {
let hidden = (selectedFiles.length == 1) ? false : true;
if(selectedFiles.length > 1 && customButton.enableMultiItems) {
// Display button only if all the selected items have the same button
hidden = !selectedFiles.every(x=>x.customActions && x.customActions.includes(customButton))
}
return !hidden;
})
.map(customButton => (
<button key={"customButtonId_" + (customButtonId++)}
className="item-action file-action"
onClick={() => {
if(customButton.onClick) {
customButton.onClick(selectedFiles);
} else {
console.warn("no onClick specified for this custom button");
}
}}
>
{customButton.icon ? customButton.icon : <BiQuestionMark />}
<span>{customButton.title ?? "??"}</span>
</button>
)
)}
</div>
<button
className="item-action file-action"
Expand Down