diff --git a/frontend/src/FileManager/FileList/FileList.jsx b/frontend/src/FileManager/FileList/FileList.jsx index e4d71e4..15d4509 100644 --- a/frontend/src/FileManager/FileList/FileList.jsx +++ b/frontend/src/FileManager/FileList/FileList.jsx @@ -1,212 +1,49 @@ -import { useEffect, useRef, useState } from "react"; +import { useRef } from "react"; import FileItem from "./FileItem"; -import { duplicateNameHandler } from "../../utils/duplicateNameHandler"; import { useFileNavigation } from "../../contexts/FileNavigationContext"; -import { useSelection } from "../../contexts/SelectionContext"; import { useLayout } from "../../contexts/LayoutContext"; import ContextMenu from "../../components/ContextMenu/ContextMenu"; import { useDetectOutsideClick } from "../../hooks/useDetectOutsideClick"; -import { BsCopy, BsFolderPlus, BsScissors } from "react-icons/bs"; -import { MdOutlineDelete, MdOutlineFileDownload, MdOutlineFileUpload } from "react-icons/md"; -import { FiRefreshCw } from "react-icons/fi"; +import useFileList from "./useFileList"; +import FilesHeader from "./FilesHeader"; import "./FileList.scss"; -import { PiFolderOpen } from "react-icons/pi"; -import { FaRegFile, FaRegPaste } from "react-icons/fa6"; -import { BiRename } from "react-icons/bi"; -import { useClipBoard } from "../../contexts/ClipboardContext"; -const FileList = ({ onCreateFolder, onRename, onFileOpen, enableFilePreview, triggerAction }) => { - const [selectedFileIndexes, setSelectedFileIndexes] = useState([]); - const [visible, setVisible] = useState(false); - const [isSelectionCtx, setIsSelectionCtx] = useState(false); - const [clickPosition, setClickPosition] = useState({ clickX: 0, clickY: 0 }); - const [lastSelectedFile, setLastSelectedFile] = useState(null); - - const { currentPath, setCurrentPath, currentPathFiles, setCurrentPathFiles } = - useFileNavigation(); +const FileList = ({ + onCreateFolder, + onRename, + onFileOpen, + onRefresh, + enableFilePreview, + triggerAction, +}) => { + const { currentPathFiles } = useFileNavigation(); const filesViewRef = useRef(null); - const { selectedFiles, setSelectedFiles, handleDownload } = useSelection(); - const { clipBoard, handleCutCopy, handlePasting } = useClipBoard(); const { activeLayout } = useLayout(); - const contextMenuRef = useDetectOutsideClick(() => setVisible(false)); - - const emptySelecCtxItems = [ - { - title: "Refresh", - icon: , - onClick: () => {}, - }, - { - title: "New Folder", - icon: , - onClick: () => {}, - }, - { - title: "Upload", - icon: , - onClick: () => {}, - }, - ]; - - const selecCtxItems = [ - { - title: "Open", - icon: lastSelectedFile?.isDirectory ? : , - onClick: handleFileOpen, - }, - { - title: "Cut", - icon: , - onClick: () => handleMoveOrCopyItems(true), - }, - { - title: "Copy", - icon: , - onClick: () => handleMoveOrCopyItems(false), - }, - { - title: "Paste", - icon: , - onClick: handleFilePasting, - className: `${clipBoard ? "" : "disable-paste"}`, - hidden: !lastSelectedFile?.isDirectory, - }, - { - title: "Rename", - icon: , - onClick: handleRenaming, - hidden: selectedFiles.length > 1, - }, - { - title: "Download", - icon: , - onClick: handleDownloadItems, - hidden: lastSelectedFile?.isDirectory, - }, - { - title: "Delete", - icon: , - onClick: handleDelete, - }, - ]; - - function handleFileOpen() { - if (lastSelectedFile.isDirectory) { - setCurrentPath(lastSelectedFile.path); - setSelectedFileIndexes([]); - setSelectedFiles([]); - } else { - enableFilePreview && triggerAction.show("previewFile"); - } - setVisible(false); - } - - function handleMoveOrCopyItems(isMoving) { - handleCutCopy(isMoving); - setVisible(false); - } - - function handleFilePasting() { - handlePasting(lastSelectedFile); - setVisible(false); - } - - function handleRenaming() { - setVisible(false); - triggerAction.show("rename"); - } - - function handleDownloadItems() { - handleDownload(); - setVisible(false); - } - - function handleDelete() { - setVisible(false); - triggerAction.show("delete"); - } - const handleFolderCreating = () => { - setCurrentPathFiles((prev) => { - return [ - ...prev, - { - name: duplicateNameHandler("New Folder", true, prev), - isDirectory: true, - path: currentPath, - isEditing: true, - key: new Date().valueOf(), - }, - ]; - }); - }; + const { + emptySelecCtxItems, + selecCtxItems, + handleContextMenu, + unselectFiles, + visible, + setVisible, + setLastSelectedFile, + selectedFileIndexes, + clickPosition, + isSelectionCtx, + } = useFileList(onRefresh, enableFilePreview, triggerAction); - const handleItemRenaming = () => { - setCurrentPathFiles((prev) => { - if (prev[selectedFileIndexes.at(-1)]) { - prev[selectedFileIndexes.at(-1)].isEditing = true; - } - return prev; - }); - - setSelectedFileIndexes([]); - setSelectedFiles([]); - }; - - const handleContextMenu = (e, isSelection) => { - e.preventDefault(); - setClickPosition({ clickX: e.clientX, clickY: e.clientY }); - setIsSelectionCtx(isSelection); - setVisible(true); - }; - - useEffect(() => { - if (triggerAction.isActive) { - switch (triggerAction.actionType) { - case "createFolder": - handleFolderCreating(); - break; - case "rename": - handleItemRenaming(); - break; - } - } - }, [triggerAction.isActive]); - - useEffect(() => { - setSelectedFileIndexes([]); - setSelectedFiles([]); - }, [currentPath]); - - useEffect(() => { - if (selectedFiles.length > 0) { - setSelectedFileIndexes(() => { - return selectedFiles.map((selectedFile) => { - return currentPathFiles.findIndex((f) => f.path === selectedFile.path); - }); - }); - } else { - setSelectedFileIndexes([]); - } - }, [selectedFiles, currentPathFiles]); + const contextMenuRef = useDetectOutsideClick(() => setVisible(false)); return (
handleContextMenu(e, false)} - onClick={() => { - setSelectedFileIndexes([]); - setSelectedFiles((prev) => (prev.length > 0 ? [] : prev)); - }} + onContextMenu={handleContextMenu} + onClick={unselectFiles} > - {activeLayout === "list" && ( -
-
Name
-
Modified
-
Size
-
- )} + {activeLayout === "list" && } + {currentPathFiles?.length > 0 ? ( <> {currentPathFiles.map((file, index) => ( @@ -218,9 +55,9 @@ const FileList = ({ onCreateFolder, onRename, onFileOpen, enableFilePreview, tri onRename={onRename} onFileOpen={onFileOpen} enableFilePreview={enableFilePreview} + triggerAction={triggerAction} filesViewRef={filesViewRef} selectedFileIndexes={selectedFileIndexes} - triggerAction={triggerAction} handleContextMenu={handleContextMenu} setVisible={setVisible} setLastSelectedFile={setLastSelectedFile} diff --git a/frontend/src/FileManager/FileList/FileList.scss b/frontend/src/FileManager/FileList/FileList.scss index 1d391d1..d20b226 100644 --- a/frontend/src/FileManager/FileList/FileList.scss +++ b/frontend/src/FileManager/FileList/FileList.scss @@ -90,7 +90,7 @@ .rename-file-container.list { top: 6px; - left: 48px; + left: 58px; text-align: left; .rename-file { @@ -101,7 +101,7 @@ top: 5px; white-space: nowrap; overflow-x: hidden; - max-width: calc(100% - 48px); + max-width: calc(100% - 62px); } .folder-name-error.right { @@ -127,37 +127,6 @@ .file-moving { opacity: 0.5; } - - .file-context-menu-list { - font-size: 1.1em; - - ul { - list-style-type: none; - padding-left: 0; - margin: 0; - - li { - display: flex; - gap: 6px; - align-items: center; - padding: 6px 22px 6px 14px; - - &:hover { - cursor: pointer; - background-color: rgb(0, 0, 0, 0.04); - } - } - - li.disable-paste { - opacity: 0.5; - - &:hover { - cursor: default; - background-color: transparent; - } - } - } - } } .files.list { @@ -173,15 +142,20 @@ display: flex; gap: 5px; border-bottom: 1px solid #dddddd; - padding: 4px 0; + padding: 4px 0 4px 5px; position: sticky; top: 0; background-color: #f5f5f5; z-index: 1; + .file-select-all { + width: 5%; + height: 0.83em; + } + .file-name { - width: calc(70% - 65px); - padding-left: 65px; + width: calc(65% - 35px); + padding-left: 35px; } .file-date { @@ -199,6 +173,7 @@ display: flex; width: 100%; margin: 0; + border-radius: 0; &:hover { background-color: rgb(0, 0, 0, 0.04); diff --git a/frontend/src/FileManager/FileList/FilesHeader.jsx b/frontend/src/FileManager/FileList/FilesHeader.jsx new file mode 100644 index 0000000..95a2207 --- /dev/null +++ b/frontend/src/FileManager/FileList/FilesHeader.jsx @@ -0,0 +1,43 @@ +import { useMemo, useState } from "react"; +import Checkbox from "../../components/Checkbox/Checkbox"; +import { useSelection } from "../../contexts/SelectionContext"; +import { useFileNavigation } from "../../contexts/FileNavigationContext"; + +const FilesHeader = ({ unselectFiles }) => { + const [showSelectAll, setShowSelectAll] = useState(false); + + const { selectedFiles, setSelectedFiles } = useSelection(); + const { currentPathFiles } = useFileNavigation(); + + const allFilesSelected = useMemo(() => { + return selectedFiles.length === currentPathFiles.length; + }, [selectedFiles, currentPathFiles]); + + const handleSelectAll = (e) => { + if (e.target.checked) { + setSelectedFiles(currentPathFiles); + setShowSelectAll(true); + } else { + unselectFiles(); + } + }; + + return ( +
setShowSelectAll(true)} + onMouseLeave={() => setShowSelectAll(false)} + > +
+ {(showSelectAll || allFilesSelected) && ( + + )} +
+
Name
+
Modified
+
Size
+
+ ); +}; + +export default FilesHeader; diff --git a/frontend/src/FileManager/FileList/useFileList.jsx b/frontend/src/FileManager/FileList/useFileList.jsx new file mode 100644 index 0000000..1e86a94 --- /dev/null +++ b/frontend/src/FileManager/FileList/useFileList.jsx @@ -0,0 +1,266 @@ +import { BiRename, BiSelectMultiple } 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"; +import { MdOutlineDelete, MdOutlineFileDownload, MdOutlineFileUpload } from "react-icons/md"; +import { PiFolderOpen } from "react-icons/pi"; +import { useClipBoard } from "../../contexts/ClipboardContext"; +import { useEffect, useState } from "react"; +import { useSelection } from "../../contexts/SelectionContext"; +import { useLayout } from "../../contexts/LayoutContext"; +import { useFileNavigation } from "../../contexts/FileNavigationContext"; +import { duplicateNameHandler } from "../../utils/duplicateNameHandler"; +import { validateApiCallback } from "../../utils/validateApiCallback"; + +const useFileList = (onRefresh, enableFilePreview, triggerAction) => { + const [selectedFileIndexes, setSelectedFileIndexes] = useState([]); + const [visible, setVisible] = useState(false); + const [isSelectionCtx, setIsSelectionCtx] = useState(false); + const [clickPosition, setClickPosition] = useState({ clickX: 0, clickY: 0 }); + const [lastSelectedFile, setLastSelectedFile] = useState(null); + + const { clipBoard, setClipBoard, handleCutCopy, handlePasting } = useClipBoard(); + const { selectedFiles, setSelectedFiles, handleDownload } = useSelection(); + const { currentPath, setCurrentPath, currentPathFiles, setCurrentPathFiles } = + useFileNavigation(); + const { activeLayout, setActiveLayout } = useLayout(); + + // Context Menu + const handleFileOpen = () => { + if (lastSelectedFile.isDirectory) { + setCurrentPath(lastSelectedFile.path); + setSelectedFileIndexes([]); + setSelectedFiles([]); + } else { + enableFilePreview && triggerAction.show("previewFile"); + } + setVisible(false); + }; + + const handleMoveOrCopyItems = (isMoving) => { + handleCutCopy(isMoving); + setVisible(false); + }; + + const handleFilePasting = () => { + handlePasting(lastSelectedFile); + setVisible(false); + }; + + const handleRenaming = () => { + setVisible(false); + triggerAction.show("rename"); + }; + + const handleDownloadItems = () => { + handleDownload(); + setVisible(false); + }; + + const handleDelete = () => { + setVisible(false); + triggerAction.show("delete"); + }; + + const handleRefresh = () => { + setVisible(false); + validateApiCallback(onRefresh, "onRefresh"); + setClipBoard(null); + }; + + const handleCreateNewFolder = () => { + triggerAction.show("createFolder"); + setVisible(false); + }; + + const handleUpload = () => { + setVisible(false); + triggerAction.show("uploadFile"); + }; + + const handleselectAllFiles = () => { + setSelectedFiles(currentPathFiles); + setVisible(false); + }; + + const emptySelecCtxItems = [ + { + title: "View", + icon: activeLayout === "grid" ? : , + onClick: () => {}, + children: [ + { + title: "Grid", + icon: , + selected: activeLayout === "grid", + onClick: () => { + setActiveLayout("grid"); + setVisible(false); + }, + }, + { + title: "List", + icon: , + selected: activeLayout === "list", + onClick: () => { + setActiveLayout("list"); + setVisible(false); + }, + }, + ], + }, + { + title: "Refresh", + icon: , + onClick: handleRefresh, + divider: true, + }, + { + title: "New folder", + icon: , + onClick: handleCreateNewFolder, + }, + { + title: "Upload", + icon: , + onClick: handleUpload, + divider: true, + }, + { + title: "Select all", + icon: , + onClick: handleselectAllFiles, + }, + ]; + + 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 handleFolderCreating = () => { + setCurrentPathFiles((prev) => { + return [ + ...prev, + { + name: duplicateNameHandler("New Folder", true, prev), + isDirectory: true, + path: currentPath, + isEditing: true, + key: new Date().valueOf(), + }, + ]; + }); + }; + + const handleItemRenaming = () => { + setCurrentPathFiles((prev) => { + if (prev[selectedFileIndexes.at(-1)]) { + prev[selectedFileIndexes.at(-1)].isEditing = true; + } + return prev; + }); + + setSelectedFileIndexes([]); + setSelectedFiles([]); + }; + + const unselectFiles = () => { + setSelectedFileIndexes([]); + setSelectedFiles((prev) => (prev.length > 0 ? [] : prev)); + }; + + const handleContextMenu = (e, isSelection = false) => { + e.preventDefault(); + setClickPosition({ clickX: e.clientX, clickY: e.clientY }); + setIsSelectionCtx(isSelection); + !isSelection && unselectFiles(); + setVisible(true); + }; + + useEffect(() => { + if (triggerAction.isActive) { + switch (triggerAction.actionType) { + case "createFolder": + handleFolderCreating(); + break; + case "rename": + handleItemRenaming(); + break; + } + } + }, [triggerAction.isActive]); + + useEffect(() => { + setSelectedFileIndexes([]); + setSelectedFiles([]); + }, [currentPath]); + + useEffect(() => { + if (selectedFiles.length > 0) { + setSelectedFileIndexes(() => { + return selectedFiles.map((selectedFile) => { + return currentPathFiles.findIndex((f) => f.path === selectedFile.path); + }); + }); + } else { + setSelectedFileIndexes([]); + } + }, [selectedFiles, currentPathFiles]); + + return { + emptySelecCtxItems, + selecCtxItems, + handleContextMenu, + unselectFiles, + visible, + setVisible, + setLastSelectedFile, + selectedFileIndexes, + clickPosition, + isSelectionCtx, + }; +}; + +export default useFileList; diff --git a/frontend/src/FileManager/FileManager.jsx b/frontend/src/FileManager/FileManager.jsx index 41eac05..c778406 100644 --- a/frontend/src/FileManager/FileManager.jsx +++ b/frontend/src/FileManager/FileManager.jsx @@ -81,6 +81,7 @@ const FileManager = ({ onCreateFolder={onCreateFolder} onRename={onRename} onFileOpen={onFileOpen} + onRefresh={onRefresh} enableFilePreview={enableFilePreview} triggerAction={triggerAction} /> diff --git a/frontend/src/FileManager/Toolbar/Toolbar.jsx b/frontend/src/FileManager/Toolbar/Toolbar.jsx index 07d3efe..b3e5971 100644 --- a/frontend/src/FileManager/Toolbar/Toolbar.jsx +++ b/frontend/src/FileManager/Toolbar/Toolbar.jsx @@ -34,7 +34,7 @@ const Toolbar = ({ const toolbarLeftItems = [ { icon: , - text: "New Folder", + text: "New folder", permission: allowCreateFolder, onClick: () => triggerAction.show("createFolder"), }, diff --git a/frontend/src/components/Checkbox/Checkbox.jsx b/frontend/src/components/Checkbox/Checkbox.jsx index 928f081..a31e7db 100644 --- a/frontend/src/components/Checkbox/Checkbox.jsx +++ b/frontend/src/components/Checkbox/Checkbox.jsx @@ -1,6 +1,6 @@ import "./Checkbox.scss"; -const Checkbox = ({ name, id, checked, onClick, onChange, className = "" }) => { +const Checkbox = ({ name, id, checked, onClick, onChange, className = "", title }) => { return ( { checked={checked} onClick={onClick} onChange={onChange} + title={title} /> ); }; diff --git a/frontend/src/components/ContextMenu/ContextMenu.jsx b/frontend/src/components/ContextMenu/ContextMenu.jsx index 69b0590..916031d 100644 --- a/frontend/src/components/ContextMenu/ContextMenu.jsx +++ b/frontend/src/components/ContextMenu/ContextMenu.jsx @@ -1,16 +1,12 @@ import { useEffect, useState } from "react"; +import { FaChevronRight } from "react-icons/fa6"; +import SubMenu from "./SubMenu"; import "./ContextMenu.scss"; -const ContextMenu = ({ - filesViewRef, - contextMenuRef, - menuItems, - visible, - setVisible, - clickPosition, -}) => { +const ContextMenu = ({ filesViewRef, contextMenuRef, menuItems, visible, clickPosition }) => { const [left, setLeft] = useState(0); const [top, setTop] = useState(0); + const [activeSubMenuIndex, setActiveSubMenuIndex] = useState(null); const contextMenuPosition = () => { const { clickX, clickY } = clickPosition; @@ -52,12 +48,17 @@ const ContextMenu = ({ e.stopPropagation(); }; + const handleMouseOver = (index) => { + setActiveSubMenuIndex(index); + }; + useEffect(() => { if (visible && contextMenuRef.current) { contextMenuPosition(); } else { setTop(0); setLeft(0); + setActiveSubMenuIndex(null); } }, [visible]); @@ -77,12 +78,29 @@ const ContextMenu = ({
    {menuItems .filter((item) => !item.hidden) - .map((item) => ( -
  • - {item.icon} - {item.title} -
  • - ))} + .map((item, index) => { + const hasChildren = item.hasOwnProperty("children"); + const activeSubMenu = activeSubMenuIndex === index; + return ( +
    +
  • handleMouseOver(index)} + > + {item.icon} + {item.title} + {hasChildren && ( + <> + + {activeSubMenu && } + + )} +
  • + {item.divider &&
    } +
    + ); + })}
diff --git a/frontend/src/components/ContextMenu/ContextMenu.scss b/frontend/src/components/ContextMenu/ContextMenu.scss index f171836..3ab6754 100644 --- a/frontend/src/components/ContextMenu/ContextMenu.scss +++ b/frontend/src/components/ContextMenu/ContextMenu.scss @@ -1,11 +1,86 @@ +@import "../../styles/variables"; + .fm-context-menu { position: absolute; background-color: white; border: 1px solid #c6c6c6; - border-radius: 5px; - padding: 5px 0; + border-radius: 6px; + padding: 4px; z-index: 1; transition: opacity 0.1s linear; + + .file-context-menu-list { + font-size: 1.1em; + + ul { + list-style-type: none; + padding-left: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 3px; + + li { + display: flex; + gap: 9px; + align-items: center; + padding: 3px 13px; + position: relative; + border-radius: 4px; + + &:hover { + cursor: pointer; + background-color: rgb(0, 0, 0, 0.07); + } + } + + li.active { + background-color: rgb(0, 0, 0, 0.07); + } + + li.disable-paste { + opacity: 0.5; + + &:hover { + cursor: default; + background-color: transparent; + } + } + } + + .divider { + border-bottom: 1px solid #c6c6c6; + margin: 5px 0 3px 0; + } + + .list-expand-icon { + margin-left: auto; + color: #444444; + } + + .sub-menu { + position: absolute; + left: calc(100% - 2px); + top: 0; + background-color: white; + border: 1px solid #c6c6c6; + border-radius: 6px; + padding: 4px; + z-index: 1; + + .item-selected { + width: 13px; + color: #444444; + } + + li { + + &:hover { + background-color: rgb(0, 0, 0, 0.07) !important; + } + } + } + } } .fm-context-menu.hidden { @@ -18,4 +93,4 @@ opacity: 1; pointer-events: all; visibility: visible; -} +} \ No newline at end of file diff --git a/frontend/src/components/ContextMenu/SubMenu.jsx b/frontend/src/components/ContextMenu/SubMenu.jsx new file mode 100644 index 0000000..df04d88 --- /dev/null +++ b/frontend/src/components/ContextMenu/SubMenu.jsx @@ -0,0 +1,17 @@ +import { FaCheck } from "react-icons/fa6"; + +const SubMenu = ({ list }) => { + return ( +
    + {list?.map((item) => ( +
  • + {item.selected && } + {item.icon} + {item.title} +
  • + ))} +
+ ); +}; + +export default SubMenu;