diff --git a/README.md b/README.md index 0a72e80..5326353 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,69 @@ type File = { | `primaryColor` | string | The primary color for the component's theme. Accepts any valid CSS color format (e.g., `'blue'`, `'#E97451'`, `'rgb(52, 152, 219)'`). This color will be applied to buttons, highlights, and other key elements. `default: #6155b4`. | | `width` | string \| number | The width of the component `default: 100%`. Can be a string (e.g., `'100%'`, `'10rem'`) or a number (in pixels). | + + +## Action properties + +| Property | Type | Description | +|---------------|------------|-------------------------------------------------------------------------------------------------------------------------| +| **title** | `string` | Display name of the action (e.g., "Rename", "Delete", "Download"). | +| **key** | `string` | Unique identifier for the action (e.g., "rename", "delete", "download"). | +| **onClick** | `Function` | Function executed when the action is triggered, receiving selected item/items as an argument. | +| **showToolbar** | `boolean` | Determines if the action appears in the toolbar (`true` = visible, `false` = hidden). | +| **showMenu** | `boolean` | Determines if the action appears in the context menu (`true` = visible, `false` = hidden). | +| **multiple** | `boolean` | Defines if the action supports multiple selections (`true` = supports multiple items, `false` = single selection only). | +| **applyTo** (TBD) | `string[]` \| `undefined` | Specifies applicable file types (e.g., folder, pdf, mp4). If undefined, the action applies to all types. | +| **icon** | `ReactNode` | Defines the icon associated with the action (e.g., "mdi-delete"). | +| **hidden** | `boolean` | Controls whether the action is hidden from the UI (`true` = hidden, `false` = visible). | + + +## Example Usage + +```JSX +, + hidden: false + }, + ], + }} + {...} + /> +``` + +## Notes + +- `title` property might be handy to maintain internationalization(i18n). +- `applyTo` helps limit actions to **specific file types**, ensuring they are only displayed where applicable. +- `hidden: true` can be useful for **conditionally displaying actions** based on user roles, permissions, or UI state. + +--- + + ## ⌨️ Keyboard Shortcuts | **Action** | **Shortcut** | @@ -222,3 +285,4 @@ Check `backend/.env.example` for database configuration details. ## ©️ License React File Manager is [MIT Licensed](LICENSE). + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 02b3e84..463863e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import { copyItemAPI, moveItemAPI } from "./api/fileTransferAPI"; import { getAllFilesAPI } from "./api/getAllFilesAPI"; import { downloadFile } from "./api/downloadFileAPI"; import "./App.scss"; +import { FaRegPaste, FaRegUser } from "react-icons/fa6"; function App() { const fileUploadConfig = { @@ -109,6 +110,10 @@ function App() { console.log(`Opening file: ${file.name}`); }; + const handleCustomAction = (file) => { + console.log(`Custom Action:`, file); + }; + const handleError = (error, file) => { console.error(error); }; @@ -133,18 +138,72 @@ function App() {
, + hidden: false + }, + { + title: "Toolbar Action", + key: "custom-tb-action", + onClick: handleCustomAction, + showToolbar: true, + showMenu: false, + multiple: false, + applyTo: [''], + icon: , + hidden: false + }, + ], + }} files={files} fileUploadConfig={fileUploadConfig} isLoading={isLoading} onCreateFolder={handleCreateFolder} onFileUploading={handleFileUploading} onFileUploaded={handleFileUploaded} - onCut={handleCut} - onCopy={handleCopy} - onPaste={handlePaste} - onRename={handleRename} onDownload={handleDownload} - onDelete={handleDelete} onLayoutChange={handleLayoutChange} onRefresh={handleRefresh} onFileOpen={handleFileOpen} diff --git a/frontend/src/FileManager/FileList/FileItem.jsx b/frontend/src/FileManager/FileList/FileItem.jsx index 679dd10..75cb4fc 100644 --- a/frontend/src/FileManager/FileList/FileItem.jsx +++ b/frontend/src/FileManager/FileList/FileItem.jsx @@ -5,6 +5,7 @@ import CreateFolderAction from "../Actions/CreateFolder/CreateFolder.action"; import RenameAction from "../Actions/Rename/Rename.action"; import { getDataSize } from "../../utils/getDataSize"; import { formatDate } from "../../utils/formatDate"; +import { getActionByKey } from "../../utils/getActionByKey"; import { useFileNavigation } from "../../contexts/FileNavigationContext"; import { useSelection } from "../../contexts/SelectionContext"; import { useClipBoard } from "../../contexts/ClipboardContext"; @@ -16,10 +17,9 @@ const dragIconSize = 50; const FileItem = ({ index, file, + actions, onCreateFolder, - onRename, enableFilePreview, - onFileOpen, filesViewRef, selectedFileIndexes, triggerAction, @@ -46,7 +46,8 @@ const FileItem = ({ clipBoard.files.find((f) => f.name === file.name && f.path === file.path); const handleFileAccess = () => { - onFileOpen(file); + const openAction = getActionByKey(actions, "open"); + openAction.onClick(file); if (file.isDirectory) { setCurrentPath(file.path); setSelectedFiles([]); @@ -234,7 +235,7 @@ const FileItem = ({ )} diff --git a/frontend/src/FileManager/FileList/FileList.jsx b/frontend/src/FileManager/FileList/FileList.jsx index bb2bc33..06177aa 100644 --- a/frontend/src/FileManager/FileList/FileList.jsx +++ b/frontend/src/FileManager/FileList/FileList.jsx @@ -10,11 +10,10 @@ import "./FileList.scss"; const FileList = ({ onCreateFolder, - onRename, - onFileOpen, onRefresh, enableFilePreview, triggerAction, + actions }) => { const { currentPathFiles } = useFileNavigation(); const filesViewRef = useRef(null); @@ -31,7 +30,7 @@ const FileList = ({ selectedFileIndexes, clickPosition, isSelectionCtx, - } = useFileList(onRefresh, enableFilePreview, triggerAction); + } = useFileList(onRefresh, enableFilePreview, triggerAction, actions); const contextMenuRef = useDetectOutsideClick(() => setVisible(false)); @@ -51,9 +50,8 @@ const FileList = ({ key={index} index={index} file={file} + actions={actions} onCreateFolder={onCreateFolder} - onRename={onRename} - onFileOpen={onFileOpen} enableFilePreview={enableFilePreview} triggerAction={triggerAction} filesViewRef={filesViewRef} @@ -71,7 +69,7 @@ const FileList = ({ { +const useFileList = (onRefresh, enableFilePreview, triggerAction, actions) => { const [selectedFileIndexes, setSelectedFileIndexes] = useState([]); const [visible, setVisible] = useState(false); const [isSelectionCtx, setIsSelectionCtx] = useState(false); @@ -25,8 +25,8 @@ const useFileList = (onRefresh, enableFilePreview, triggerAction) => { useFileNavigation(); const { activeLayout, setActiveLayout } = useLayout(); - // Context Menu const handleFileOpen = () => { + // Context Menu if (lastSelectedFile.isDirectory) { setCurrentPath(lastSelectedFile.path); setSelectedFileIndexes([]); @@ -133,51 +133,103 @@ const useFileList = (onRefresh, enableFilePreview, triggerAction) => { }, ]; - const selecCtxItems = [ - { - title: "Open", - icon: lastSelectedFile?.isDirectory ? : , - onClick: handleFileOpen, - divider: true, - }, - { - title: "Cut", - icon: , - onClick: () => handleMoveOrCopyItems(true), - }, - { - title: "Copy", - icon: , - onClick: () => handleMoveOrCopyItems(false), - divider: !lastSelectedFile?.isDirectory, - }, - { - title: "Paste", - icon: , - onClick: handleFilePasting, - className: `${clipBoard ? "" : "disable-paste"}`, - hidden: !lastSelectedFile?.isDirectory, - divider: true, - }, - { - title: "Rename", - icon: , - onClick: handleRenaming, - hidden: selectedFiles.length > 1, - }, - { - title: "Download", - icon: , - onClick: handleDownloadItems, - hidden: lastSelectedFile?.isDirectory, - }, - { - title: "Delete", - icon: , - onClick: handleDelete, - }, - ]; - // + const selecCtxItems = () => { + return actions + .filter((item) => item.showMenu) + .map((item) => { + switch (item.key) { + case "open": { + return { + ...item, + onClick: (args) => { + handleFileOpen(args); + item.onClick(args); + }, + icon: item.icon || lastSelectedFile?.isDirectory ? : , + divider: true, + } + } + case "cut": { + return { + ...item, + onClick: () => { + item.onClick(); + handleMoveOrCopyItems(true); + }, + icon: item.icon || , + hidden: selectedFiles.length > 1, + } + } + case "copy": { + return { + ...item, + onClick: (args) => { + item.onClick(args); + handleMoveOrCopyItems(false); + }, + icon: item.icon || , + divider: !lastSelectedFile?.isDirectory, + } + } + case "paste": { + return { + ...item, + onClick: (args) => { + item.onClick(args); + handleFilePasting(args); + }, + icon: item.icon || , + className: `${clipBoard ? "" : "disable-paste"}`, + hidden: !lastSelectedFile?.isDirectory, + divider: !lastSelectedFile?.isDirectory, + } + } + case "rename": { + return { + ...item, + icon: item.icon || , + onClick: (args) => { + item.onClick(args); + handleRenaming(args); + }, + hidden: selectedFiles.length > 1, + } + } + case "download": { + return { + ...item, + icon: item.icon || , + onClick: (args) => { + item.onClick(args); + handleDownloadItems(args); + }, + hidden: lastSelectedFile?.isDirectory, + } + } + case "delete": { + return { + ...item, + icon: item.icon || , + onClick: () => { + item.onClick(); + handleDelete(); + }, + hidden: lastSelectedFile?.isDirectory, + } + } + default: { + return { + ...item, + onClick: () => { + item.onClick(item.multiple ? selectedFiles : selectedFiles?.[0]); + setVisible(false); + }, + hidden: item.hidden || (!item.multiple && selectedFiles.length > 1), + } + } + } + }); +} const handleFolderCreating = () => { setCurrentPathFiles((prev) => { diff --git a/frontend/src/FileManager/FileManager.jsx b/frontend/src/FileManager/FileManager.jsx index 1fca34e..c10f018 100644 --- a/frontend/src/FileManager/FileManager.jsx +++ b/frontend/src/FileManager/FileManager.jsx @@ -12,22 +12,78 @@ import { LayoutProvider } from "../contexts/LayoutContext"; import { useTriggerAction } from "../hooks/useTriggerAction"; import { useColumnResize } from "../hooks/useColumnResize"; import PropTypes from "prop-types"; +import { getActionByKey } from "../utils/getActionByKey"; import { dateStringValidator, urlValidator } from "../validators/propValidators"; import "./FileManager.scss"; +const defaultActions = [ + { + title: "Open", + key: "open", + onClick: () => {}, + showToolbar: false, + showMenu: true, + icon: null, + }, + { + title: "Copy", + key: "copy", + onClick: () => {}, + showToolbar: true, + showMenu: true, + icon: null, + }, + { + title: "Cut", + key: "cut", + onClick: () => {}, + showToolbar: true, + showMenu: false, + icon: null, + }, + { + title: "Paste", + key: "paste", + onClick: () => {}, + showToolbar: true, + showMenu: true, + icon: null, + }, + { + title: "Rename", + key: "rename", + onClick: () => {}, + showToolbar: true, + showMenu: true, + icon: null, + }, + { + title: "Delete", + key: "delete", + onClick: () => {}, + showToolbar: true, + showMenu: true, + icon: null, + }, + { + title: "Download", + key: "download", + onClick: () => {}, + showToolbar: true, + showMenu: true, + icon: null, + }, +]; + const FileManager = ({ files, + config, fileUploadConfig, isLoading, onCreateFolder, onFileUploading = () => {}, onFileUploaded = () => {}, - onCut, - onCopy, - onPaste, - onRename, onDownload, - onDelete = () => null, onLayoutChange = () => {}, onRefresh, onFileOpen = () => {}, @@ -55,18 +111,29 @@ const FileManager = ({ width, }; + const getActions = () => { + const resultActions = new Map(defaultActions.map(item => [item.key, item])); + if (config?.actions) { + config.actions.forEach(item => + resultActions.set(item.key, { ...resultActions.get(item.key), ...item }) + ); + } + return [...resultActions.values()]; + } + return (
e.preventDefault()} style={customStyles}> - + @@ -87,9 +154,8 @@ const FileManager = ({
{ @@ -34,23 +35,107 @@ const Toolbar = ({ const toolbarLeftItems = [ { icon: , - text: "New folder", + title: "New folder", permission: allowCreateFolder, onClick: () => triggerAction.show("createFolder"), }, { icon: , - text: "Upload", + title: "Upload", permission: allowUploadFile, onClick: () => triggerAction.show("uploadFile"), }, { icon: , - text: "Paste", + title: "Paste", permission: !!clipBoard, onClick: handleFilePasting, }, ]; + const selectedToolbarActions = () => { + return actions + .filter((item) => item.showToolbar) + .map((item) => { + switch (item.key) { + case "cut": { + return { + ...item, + onClick: () => { + item.onClick(); + handleCutCopy(true); + }, + icon: item.icon || , + hidden: selectedFiles.length > 1, + } + } + case "copy": { + return { + ...item, + onClick: (args) => { + item.onClick(args); + handleCutCopy(false); + }, + icon: item.icon || , + } + } + case "paste": { + return { + ...item, + onClick: (args) => { + item.onClick(args); + handleFilePasting(args); + }, + icon: item.icon || , + className: `${clipBoard ? "" : "disable-paste"}`, + hidden: clipBoard?.files?.length === 0, + } + } + case "rename": { + return { + ...item, + icon: item.icon || , + onClick: (args) => { + console.log(args); + item.onClick(args); + triggerAction.show("rename") + }, + hidden: selectedFiles.length > 1, + } + } + case "download": { + return { + ...item, + icon: item.icon || , + onClick: (args) => { + item.onClick(args); + handleDownloadItems(args); + }, + hidden: selectedFiles.isDirectory, + } + } + case "delete": { + return { + ...item, + icon: item.icon || , + onClick: () => { + item.onClick(); + triggerAction.show("delete") + }, + hidden: selectedFiles.isDirectory, + } + } + default: { + return { + ...item, + onClick: () => { + item.onClick(item.multiple ? selectedFiles : selectedFiles?.[0]); + }, + hidden: item.hidden || (!item.multiple && selectedFiles.length > 1), + } + } + } + }); + } const toolbarRightItems = [ { @@ -83,10 +168,14 @@ const Toolbar = ({
- + {selectedToolbarActions().map((item) => ( + + ))} + {/* - )} - + */}
))}
diff --git a/frontend/src/contexts/ClipboardContext.jsx b/frontend/src/contexts/ClipboardContext.jsx index 24e2792..e7a1d4a 100644 --- a/frontend/src/contexts/ClipboardContext.jsx +++ b/frontend/src/contexts/ClipboardContext.jsx @@ -1,10 +1,11 @@ import { createContext, useContext, useState } from "react"; import { useSelection } from "./SelectionContext"; import { validateApiCallback } from "../utils/validateApiCallback"; +import { getActionByKey } from "../utils/getActionByKey"; const ClipBoardContext = createContext(); -export const ClipBoardProvider = ({ children, onPaste, onCut, onCopy }) => { +export const ClipBoardProvider = ({ children, actions }) => { const [clipBoard, setClipBoard] = useState(null); const { selectedFiles, setSelectedFiles } = useSelection(); @@ -15,8 +16,10 @@ export const ClipBoardProvider = ({ children, onPaste, onCut, onCopy }) => { }); if (isMoving) { + const onCut = getActionByKey(actions, "cut").onClick; !!onCut && onCut(selectedFiles); } else { + const onCopy = getActionByKey(actions, "copy").onClick; !!onCopy && onCopy(selectedFiles); } }; @@ -27,6 +30,7 @@ export const ClipBoardProvider = ({ children, onPaste, onCut, onCopy }) => { const copiedFiles = clipBoard.files; const operationType = clipBoard.isMoving ? "move" : "copy"; + const onPaste = getActionByKey(actions, "paste").onClick; validateApiCallback(onPaste, "onPaste", copiedFiles, destinationFolder, operationType); diff --git a/frontend/src/utils/getActionByKey.js b/frontend/src/utils/getActionByKey.js new file mode 100644 index 0000000..bd8903a --- /dev/null +++ b/frontend/src/utils/getActionByKey.js @@ -0,0 +1,2 @@ +export const getActionByKey = (actions, key) => + actions.find((item) => item.key === key); \ No newline at end of file