diff --git a/backend/controllers/file-controller.ts b/backend/controllers/file-controller.ts index 3409658..a1f45e8 100644 --- a/backend/controllers/file-controller.ts +++ b/backend/controllers/file-controller.ts @@ -501,6 +501,28 @@ class FileController { } }; + moveMultiFile = async ( + req: RequestType, + res: Response, + next: NextFunction + ) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const items = req.body.items; + const parentID = (req.body.parentID as string) || "/"; + + await fileService.moveMultiFiles(userID, items, parentID); + + res.send(); + } catch (e) { + next(e); + } + }; + trashFile = async (req: RequestType, res: Response, next: NextFunction) => { if (!req.user) { return; diff --git a/backend/express-routers/file-router.ts b/backend/express-routers/file-router.ts index 6906419..f693408 100644 --- a/backend/express-routers/file-router.ts +++ b/backend/express-routers/file-router.ts @@ -16,6 +16,7 @@ import { makePrivateValidationRules, makePublicValidationRules, moveFileValidationRules, + moveMultiValidationRules, removeVideoStreamTokenValidationRules, renameFileValidationRules, restoreFileValidationRules, @@ -141,6 +142,13 @@ router.patch( fileController.moveFile ); +router.patch( + "/file-service/move-multi", + auth, + moveMultiValidationRules, + fileController.moveMultiFile +); + router.patch( "/file-service/trash", auth, diff --git a/backend/middleware/files/files-middleware.ts b/backend/middleware/files/files-middleware.ts index 0f93207..db9d119 100644 --- a/backend/middleware/files/files-middleware.ts +++ b/backend/middleware/files/files-middleware.ts @@ -101,6 +101,12 @@ export const moveFileValidationRules = [ middlewareValidationFunction, ]; +export const moveMultiValidationRules = [ + body("items").isArray().withMessage("Items must be an array"), + body("parentID").isString().optional().withMessage("Parent must be a string"), + middlewareValidationFunction, +]; + export const trashFileValidationRules = [ body("id").isString().withMessage("ID must be a string"), middlewareValidationFunction, diff --git a/backend/services/file-service/file-service.ts b/backend/services/file-service/file-service.ts index 81868de..c026eb3 100644 --- a/backend/services/file-service/file-service.ts +++ b/backend/services/file-service/file-service.ts @@ -331,6 +331,34 @@ class MongoFileService { return updatedFile; }; + + moveMultiFiles = async ( + userID: string, + items: { + type: "file" | "folder" | "quick-item"; + id: string; + file?: FileInterface; + folder?: FolderInterface; + }[], + parentID: string + ) => { + const fileList = items.filter( + (item) => item.type === "file" || item.type === "quick-item" + ); + const folderList = items + .filter((item) => item.type === "folder") + .sort((a, b) => { + if (!a.folder || !b.folder) return 0; + return b.folder.parentList.length - a.folder.parentList.length; + }); + + for (const file of fileList) { + await this.moveFile(userID, file.id, parentID); + } + for (const folder of folderList) { + await folderService.moveFolder(userID, folder.id, parentID); + } + }; } export default MongoFileService; diff --git a/backend/services/folder-service/folder-service.ts b/backend/services/folder-service/folder-service.ts index 44ebcf1..06a79bd 100644 --- a/backend/services/folder-service/folder-service.ts +++ b/backend/services/folder-service/folder-service.ts @@ -132,6 +132,16 @@ class FolderService { startParentList.push("/"); } + await Promise.all([ + folderDB.moveFolder(folderID, userID, parentID, startParentList), + fileDB.moveMultipleFiles( + userID, + folderID, + folderID, + [...startParentList, folderID].toString() + ), + ]); + for (let i = 0; i < foldersByIncludedParent.length; i++) { const currentFolder = foldersByIncludedParent[i]; @@ -141,16 +151,22 @@ class FolderService { newParentList.push( ...startParentList, + folderID, ...currentFolder.parentList.slice(currentParentIndex + 1) ); await Promise.all([ - folderDB.moveFolder(folderID, userID, parentID, newParentList), + folderDB.moveFolder( + currentFolder._id.toString(), + userID, + currentFolder.parent, + newParentList + ), fileDB.moveMultipleFiles( userID, currentFolder._id.toString(), - parentID, - newParentList.toString() + currentFolder._id.toString(), + [...newParentList, currentFolder._id.toString()].toString() ), ]); } diff --git a/src/api/filesAPI.ts b/src/api/filesAPI.ts index 31b3fc5..b9fe76d 100644 --- a/src/api/filesAPI.ts +++ b/src/api/filesAPI.ts @@ -190,6 +190,14 @@ export const moveFileAPI = async (fileID: string, parentID: string) => { return response.data; }; +export const moveMultiAPI = async (items: any, parentID: string) => { + const response = await axios.patch(`/file-service/move-multi`, { + items, + parentID, + }); + return response.data; +}; + // DELETE export const deleteFileAPI = async (fileID: string) => { diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx index 350e16c..13f4a69 100644 --- a/src/components/ContextMenu/ContextMenu.tsx +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -288,91 +288,93 @@ const ContextMenu: React.FC = memo((props) => { }; return ( -
-
-
- -

Multi-select

+
+
+
+
+ +

Multi-select

+
+ {!isTrash && !isMedia && ( +
+ +

Rename

+
+ )} + {!folderMode && !isTrash && ( +
+ +

Share

+
+ )} + {!isTrash && ( +
+ +

Download

+
+ )} + {!isTrash && !isMedia && ( +
+ +

Move

+
+ )} + {!isTrash && ( +
+ +

Trash

+
+ )} + {isTrash && ( +
+ +

Restore

+
+ )} + {isTrash && ( +
+ +

Delete

+
+ )}
- {!isTrash && !isMedia && ( -
- -

Rename

-
- )} - {!folderMode && !isTrash && ( -
- -

Share

-
- )} - {!isTrash && ( -
- -

Download

-
- )} - {!isTrash && !isMedia && ( -
- -

Move

-
- )} - {!isTrash && ( -
- -

Trash

-
- )} - {isTrash && ( -
- -

Restore

-
- )} - {isTrash && ( -
- -

Delete

-
- )}
); diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 4262bc7..7f7526b 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -2,7 +2,6 @@ import { useFiles } from "../../hooks/files"; import { useUtils } from "../../hooks/utils"; import React, { memo } from "react"; import FileItem from "../FileItem/FileItem"; -import ParentBar from "../ParentBar/ParentBar"; import classNames from "classnames"; import { useAppDispatch, useAppSelector } from "../../hooks/store"; import { toggleListView } from "../../reducers/general"; @@ -22,15 +21,9 @@ const Files = memo(() => {
- {isHome &&

Home Files

} - {!isHome && ( - -
- -
-
- )} - +

+ {isHome ? "Home" : ""} Files +

  • diff --git a/src/components/Folders/Folders.tsx b/src/components/Folders/Folders.tsx index 5f095bc..3f9cddf 100644 --- a/src/components/Folders/Folders.tsx +++ b/src/components/Folders/Folders.tsx @@ -1,15 +1,16 @@ import { useFolders } from "../../hooks/folders"; import FolderItem from "../FolderItem/FolderItem"; -import { memo } from "react"; +import React, { memo } from "react"; import classNames from "classnames"; import { useUtils } from "../../hooks/utils"; import { useAppDispatch, useAppSelector } from "../../hooks/store"; import { setSortBy } from "../../reducers/filter"; +import ParentBar from "../ParentBar/ParentBar"; const Folders = memo( ({ scrollDivRef }: { scrollDivRef: React.RefObject }) => { const { data: folders } = useFolders(false); - const { isTrash, isSearch } = useUtils(); + const { isTrash, isSearch, isHome } = useUtils(); const sortBy = useAppSelector((state) => state.filter.sortBy); const dispatch = useAppDispatch(); @@ -68,13 +69,24 @@ const Folders = memo( return "Trash"; } else if (isSearch) { return "Search"; + } else if (folders?.length === 0) { + return "No Folders"; + } else if (isHome) { + return "Home Folders"; } else { - return folders?.length === 0 ? "No Folders" : "Folders"; + return "Folders"; } })(); return (
    + {!isHome && ( + +
    + +
    +
    + )}

    { const multiSelectMode = useAppSelector( (state) => state.selected.multiSelectMode ); + const multiSelectMap = useAppSelector( + (state) => state.selected.multiSelectMap + ); const [isLoadingMove, setIsLoadingMove] = useState(false); const file = useAppSelector((state) => state.selected.moveModal.file); const folder = useAppSelector((state) => state.selected.moveModal.folder); @@ -131,6 +134,15 @@ const MoverPopup = () => { : parent?._id || "/"; try { if (multiSelectMode) { + const itemsToMove = Object.values(multiSelectMap); + await toast.promise(moveMultiAPI(itemsToMove, moveTo), { + pending: "Moving items...", + success: "Items Moved", + error: "Error Moving Items", + }); + refetchFiles(); + refetchFolders(); + dispatch(resetMoveModal()); } else if (file) { await toast.promise(moveFileAPI(file._id, moveTo), { pending: "Moving File...", diff --git a/src/components/ParentBar/ParentBar.tsx b/src/components/ParentBar/ParentBar.tsx index 9868894..e75cb1a 100644 --- a/src/components/ParentBar/ParentBar.tsx +++ b/src/components/ParentBar/ParentBar.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useUtils } from "../../hooks/utils"; import { useFolder } from "../../hooks/folders"; import SpacerIcon from "../../icons/SpacerIcon"; +import ArrowBackIcon from "../../icons/ArrowBackIcon"; const ParentBar = memo(() => { const { data: folder } = useFolder(false); @@ -25,26 +26,33 @@ const ParentBar = memo(() => { navigate(`/folder/${folder?._id}`); }; - // TODO: Decide how to handle loading - // if (!isLoading) { - // return
    - - //
    - // } + const goBackAFolder = () => { + if (folder?.parent === "/") { + navigate("/home"); + } else { + navigate(`/folder/${folder.parent}`); + } + }; return ( -
    +
    +
    + +
    {!isTrash ? "Home" : "Trash"} - +

    {folder.name}

    diff --git a/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx b/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx index 924cb0b..9cdb6eb 100644 --- a/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx +++ b/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx @@ -5,7 +5,11 @@ import CloseIcon from "../../icons/CloseIcon"; import ActionsIcon from "../../icons/ActionsIcon"; import { useContextMenu } from "../../hooks/contextMenu"; import ContextMenu from "../ContextMenu/ContextMenu"; -import { resetPopupSelect, setPopupSelect } from "../../reducers/selected"; +import { + resetPopupSelect, + setMainSelect, + setPopupSelect, +} from "../../reducers/selected"; import CircleLeftIcon from "../../icons/CircleLeftIcon"; import CircleRightIcon from "../../icons/CircleRightIcon"; import { useFiles, useQuickFiles } from "../../hooks/files"; @@ -122,13 +126,28 @@ const PhotoViewerPopup: React.FC = memo((props) => { const prevItem = filteredQuickFiles[index - 1]; if (prevItem) { dispatch(setPopupSelect({ type: "quick-item", file: prevItem })); + dispatch( + setMainSelect({ + file: prevItem, + id: prevItem._id, + type: "file", + folder: null, + }) + ); } } else { if (!files?.pages) return 0; const prevItem = findPrevFilesItem(); - console.log("prev item", prevItem); if (prevItem) { dispatch(setPopupSelect({ type: "file", file: prevItem })); + dispatch( + setMainSelect({ + file: prevItem, + id: prevItem._id, + type: "file", + folder: null, + }) + ); } // TODO: Perhaps implement this if needed in the future // else { @@ -186,23 +205,51 @@ const PhotoViewerPopup: React.FC = memo((props) => { const nextItem = filteredQuickFiles[index + 1]; if (nextItem) { dispatch(setPopupSelect({ type: "quick-item", file: nextItem })); + dispatch( + setMainSelect({ + file: nextItem, + id: nextItem._id, + type: "file", + folder: null, + }) + ); } } else { if (!files?.pages) return; const nextItem = findNextFilesItem(); if (nextItem) { dispatch(setPopupSelect({ type: "file", file: nextItem })); + dispatch( + setMainSelect({ + file: nextItem, + id: nextItem._id, + type: "file", + folder: null, + }) + ); } else if (!finalLastPageLoaded.current && !loadingNextPage.current) { loadingNextPage.current = true; + console.log("fetch next page"); const newFilesResponse = await fetchNextPage(); + console.log("new files response", newFilesResponse); if (!newFilesResponse.data?.pages) return; const fetchedNextItem = findNextFilesItem(newFilesResponse.data); + console.log("fetched next item", fetchedNextItem); if (fetchedNextItem) { dispatch(setPopupSelect({ type: "file", file: fetchedNextItem })); + dispatch( + setMainSelect({ + file: fetchedNextItem, + id: fetchedNextItem._id, + type: "file", + folder: null, + }) + ); } else { finalLastPageLoaded.current = true; } loadingNextPage.current = false; + console.log("end"); } } }; @@ -211,7 +258,16 @@ const PhotoViewerPopup: React.FC = memo((props) => { dispatch(resetPopupSelect()); }; + const outterWrapperClick = (e: React.MouseEvent) => { + console.log("outter wrapper click", e.target); + if ((e.target as HTMLDivElement).id !== "outer-wrapper") { + return; + } + closePhotoViewer(); + }; + useEffect(() => { + console.log("useeffect"); if (file.metadata.isVideo) { getVideo(); } @@ -222,7 +278,11 @@ const PhotoViewerPopup: React.FC = memo((props) => { }, [file.metadata.isVideo, getVideo, cleanUpVideo]); return ( -
    +
    {contextMenuState.selected && (
    = memo((props) => { className="h-[27px] w-[27px] bg-red-500 rounded-[3px] flex flex-row justify-center items-center" style={{ background: imageColor }} > - + {fileExtension}
    -

    +

    {file.filename}

    @@ -269,16 +329,14 @@ const PhotoViewerPopup: React.FC = memo((props) => {
    -
    - - -
    + +
    {isThumbnailLoading && !thumbnailError && } {!file.metadata.isVideo && (