diff --git a/README.md b/README.md
index 5334036a..0dfdcb4c 100644
--- a/README.md
+++ b/README.md
@@ -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
};
```
@@ -201,6 +202,45 @@ const CustomImagePreviewer = ({ file }) => {
filePreviewComponent={(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 : , // 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
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index d017f263..c7111ebb 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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 : ,
+ onClick : (selectedFiles) => {console.log("click on custom button, with selected files : ", selectedFiles)}
+}
function App() {
const fileUploadConfig = {
@@ -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);
diff --git a/frontend/src/FileManager/FileList/useFileList.jsx b/frontend/src/FileManager/FileList/useFileList.jsx
index f32dc0bc..ab149533 100644
--- a/frontend/src/FileManager/FileList/useFileList.jsx
+++ b/frontend/src/FileManager/FileList/useFileList.jsx
@@ -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";
@@ -140,54 +140,82 @@ const useFileList = (onRefresh, enableFilePreview, triggerAction, permissions, o
},
];
- const selecCtxItems = [
- {
- title: t("open"),
- icon: lastSelectedFile?.isDirectory ? : ,
- onClick: handleFileOpen,
- divider: true,
- },
- {
- title: t("cut"),
- icon: ,
- onClick: () => handleMoveOrCopyItems(true),
- divider: !lastSelectedFile?.isDirectory && !permissions.copy,
- hidden: !permissions.move,
- },
- {
- title: t("copy"),
- icon: ,
- onClick: () => handleMoveOrCopyItems(false),
- divider: !lastSelectedFile?.isDirectory,
- hidden: !permissions.copy,
- },
- {
- title: t("paste"),
- icon: ,
- onClick: handleFilePasting,
- className: `${clipBoard ? "" : "disable-paste"}`,
- hidden: !lastSelectedFile?.isDirectory || (!permissions.move && !permissions.copy),
- divider: true,
- },
- {
- title: t("rename"),
- icon: ,
- onClick: handleRenaming,
- hidden: selectedFiles.length > 1 || !permissions.rename,
- },
- {
- title: t("download"),
- icon: ,
- onClick: handleDownloadItems,
- hidden: !permissions.download,
- },
- {
- title: t("delete"),
- icon: ,
- 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 : ,
+ 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 ? : ,
+ onClick: handleFileOpen,
+ divider: true,
+ },
+ {
+ title: t("cut"),
+ icon: ,
+ onClick: () => handleMoveOrCopyItems(true),
+ divider: !lastSelectedFile?.isDirectory && !permissions.copy,
+ hidden: !permissions.move,
+ },
+ {
+ title: t("copy"),
+ icon: ,
+ onClick: () => handleMoveOrCopyItems(false),
+ divider: !lastSelectedFile?.isDirectory,
+ hidden: !permissions.copy,
+ },
+ {
+ title: t("paste"),
+ icon: ,
+ onClick: handleFilePasting,
+ className: `${clipBoard ? "" : "disable-paste"}`,
+ hidden: !lastSelectedFile?.isDirectory || (!permissions.move && !permissions.copy),
+ divider: true,
+ },
+ {
+ title: t("rename"),
+ icon: ,
+ onClick: handleRenaming,
+ hidden: selectedFiles.length > 1 || !permissions.rename,
+ },
+ {
+ title: t("download"),
+ icon: ,
+ onClick: handleDownloadItems,
+ hidden: !permissions.download,
+ },
+ {
+ title: t("delete"),
+ icon: ,
+ onClick: handleDelete,
+ hidden: !permissions.delete,
+ },
+ ]);
//
const handleFolderCreating = () => {
diff --git a/frontend/src/FileManager/Toolbar/Toolbar.jsx b/frontend/src/FileManager/Toolbar/Toolbar.jsx
index 41475788..be5d5960 100644
--- a/frontend/src/FileManager/Toolbar/Toolbar.jsx
+++ b/frontend/src/FileManager/Toolbar/Toolbar.jsx
@@ -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";
@@ -75,6 +75,7 @@ const Toolbar = ({ onLayoutChange, onRefresh, triggerAction, permissions }) => {
// Selected File/Folder Actions
if (selectedFiles.length > 0) {
+ let customButtonId = 0;
return (
@@ -125,6 +126,32 @@ const Toolbar = ({ onLayoutChange, onRefresh, triggerAction, permissions }) => {
{t("delete")}
)}
+ {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 => (
+
+ )
+ )}