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 => ( + + ) + )}