From c648b9bfc6ffdae2a1ee4418e199903d12f2d051 Mon Sep 17 00:00:00 2001 From: subnub Date: Sun, 4 Aug 2024 02:46:09 -0400 Subject: [PATCH] started mover modal --- backend/controllers/folder.ts | 30 +++ backend/db/utils/folderUtils/index.ts | 30 +++ backend/express-routers/folder.ts | 8 + .../middleware/Folders/FolderMiddleware.ts | 18 ++ backend/services/FolderService/index.ts | 16 ++ package.json | 1 + src/api/foldersAPI.ts | 16 ++ src/components/ContextMenu/index.tsx | 8 +- src/components/MainSection/index.tsx | 6 + src/components/MoverPopup/MoverPopup.tsx | 173 ++++++++++++++++++ src/hooks/folders.ts | 26 ++- src/icons/ArrowBackIcon.tsx | 15 ++ src/icons/FolderIcon.tsx | 14 ++ src/reducers/mover2.ts | 42 +++++ src/reducers/selected.ts | 24 +++ 15 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 backend/middleware/Folders/FolderMiddleware.ts create mode 100644 src/components/MoverPopup/MoverPopup.tsx create mode 100644 src/icons/ArrowBackIcon.tsx create mode 100644 src/icons/FolderIcon.tsx create mode 100644 src/reducers/mover2.ts diff --git a/backend/controllers/folder.ts b/backend/controllers/folder.ts index 3f5b927..fb0270f 100644 --- a/backend/controllers/folder.ts +++ b/backend/controllers/folder.ts @@ -254,6 +254,36 @@ class FolderController { res.status(500).send("Server error renaming folder"); } }; + + getMoveFolderList = async (req: RequestType, res: Response) => { + if (!req.user) { + return; + } + + try { + const userID = req.user._id; + const parent = (req.query.parent as string) || undefined; + const search = (req.query.search as string) || undefined; + const folderID = (req.query.folderID as string) || undefined; + + console.log("folderID", folderID); + + const folderList = await folderService.getMoveFolderList( + userID, + parent, + search, + folderID + ); + + res.send(folderList); + } catch (e: unknown) { + if (e instanceof Error) { + console.log("\nGet Move Folder List Error Folder Route:", e.message); + } + + res.status(500).send("Server error getting move folder list"); + } + }; } export default FolderController; diff --git a/backend/db/utils/folderUtils/index.ts b/backend/db/utils/folderUtils/index.ts index 8a0ca4b..475996e 100644 --- a/backend/db/utils/folderUtils/index.ts +++ b/backend/db/utils/folderUtils/index.ts @@ -182,6 +182,36 @@ class DbUtil { ); return result; }; + + getMoveFolderList = async ( + userID: string, + parent = "/", + search?: string, + folderID?: string + ) => { + let query: any = { + owner: userID, + }; + + if (folderID) { + query.parentList = { $ne: folderID }; + query._id = { $ne: folderID }; + } + + if (!search || search === "") { + query.parent = parent; + } + + if (search && search !== "") { + query.name = new RegExp(search, "i"); + } + + console.log("query", query); + + const result = await Folder.find(query); + + return result; + }; } export default DbUtil; diff --git a/backend/express-routers/folder.ts b/backend/express-routers/folder.ts index a72b754..3405da4 100644 --- a/backend/express-routers/folder.ts +++ b/backend/express-routers/folder.ts @@ -4,6 +4,7 @@ import env from "../enviroment/env"; import FileSystemService from "../services/ChunkService/FileSystemService"; import S3Service from "../services/ChunkService/S3Service"; import FolderController from "../controllers/folder"; +import { moveFolderValidationRules } from "../middleware/Folders/FolderMiddleware"; let folderController: FolderController; @@ -47,4 +48,11 @@ router.get( folderController.getSubfolderFullList ); +router.get( + "/folder-service/move-folder-list", + auth, + moveFolderValidationRules, + folderController.getMoveFolderList +); + export default router; diff --git a/backend/middleware/Folders/FolderMiddleware.ts b/backend/middleware/Folders/FolderMiddleware.ts new file mode 100644 index 0000000..fbfa690 --- /dev/null +++ b/backend/middleware/Folders/FolderMiddleware.ts @@ -0,0 +1,18 @@ +import { query, validationResult } from "express-validator"; +import { Request, Response, NextFunction } from "express"; + +export const moveFolderValidationRules = [ + query("parent").optional().isString().withMessage("Parent must be a string"), + query("search").optional().isString().withMessage("Search must be a string"), + query("folderID") + .optional() + .isString() + .withMessage("FolderID must be a string"), + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + next(); + }, +]; diff --git a/backend/services/FolderService/index.ts b/backend/services/FolderService/index.ts index 542c1c4..f4b5e48 100644 --- a/backend/services/FolderService/index.ts +++ b/backend/services/FolderService/index.ts @@ -229,6 +229,22 @@ class FolderService { await folder.save(); }; + getMoveFolderList = async ( + userID: string, + parent?: string, + search?: string, + folderID?: string + ) => { + const folderList = await utilsFolder.getMoveFolderList( + userID, + parent, + search, + folderID + ); + + return folderList; + }; + moveFolder = async (userID: string, folderID: string, parentID: string) => { let parentList = ["/"]; diff --git a/package.json b/package.json index db57ad2..7d5123e 100755 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "dotenv": "^8.2.0", "eslint-plugin-react-hooks": "^4.6.2", "express": "^4.19.2", + "express-validator": "^7.1.0", "fluent-ffmpeg": "^2.1.3", "font-awesome": "^4.7.0", "helmet": "^3.21.2", diff --git a/src/api/foldersAPI.ts b/src/api/foldersAPI.ts index f890623..952cf5e 100644 --- a/src/api/foldersAPI.ts +++ b/src/api/foldersAPI.ts @@ -36,6 +36,22 @@ export const getFolderInfoAPI = async ({ return response.data; }; +export const getMoveFolderListAPI = async ({ + queryKey, +}: QueryFunctionContext< + [string, { parent: string; search: string; folderID?: string }] +>) => { + const [_key, { parent, search, folderID }] = queryKey; + const response = await axios.get(`/folder-service/move-folder-list`, { + params: { + parent, + search, + folderID, + }, + }); + return response.data; +}; + // POST export const createFolderAPI = async (name: string, parent?: string) => { diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index 883cced..8b18831 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -22,6 +22,7 @@ import { useClickOutOfBounds, useUtils } from "../../hooks/utils"; import { useAppDispatch } from "../../hooks/store"; import { resetSelected, + setMoveModal, setMultiSelectMode, setShareModal, } from "../../reducers/selected"; @@ -240,9 +241,12 @@ const ContextMenu: React.FC = memo((props) => { const openMoveItemModal = async () => { closeContext(); if (!folderMode && file) { - dispatch(setMoverID(file._id, file.metadata.parent, true)); + // dispatch(setMoverID(file._id, file.metadata.parent, true)); + dispatch(setMoveModal({ type: "file", file, folder: null })); } else if (folderMode && folder) { - dispatch(setMoverID(folder._id, folder.parent, false)); + // dispatch(setMoverID(folder._id, folder.parent, false)); + // } + dispatch(setMoveModal({ type: "folder", file: null, folder })); } }; diff --git a/src/components/MainSection/index.tsx b/src/components/MainSection/index.tsx index 37888b2..a9c4464 100644 --- a/src/components/MainSection/index.tsx +++ b/src/components/MainSection/index.tsx @@ -8,6 +8,7 @@ import { useAppSelector } from "../../hooks/store"; import PhotoViewerPopup from "../PhotoViewerPopup"; import FileInfoPopup from "../FileInfoPopup"; import SharePopup from "../SharePopup"; +import MoverPopup from "../MoverPopup/MoverPopup"; const MainSection = memo(() => { const popupModalItem = useAppSelector( @@ -16,6 +17,9 @@ const MainSection = memo(() => { const shareModalItem = useAppSelector( (state) => state.selected.shareModal.file ); + const moveModalItemType = useAppSelector( + (state) => state.selected.moveModal.type + ); const isMediaSelected = popupModalItem?.metadata.isVideo || popupModalItem?.metadata.hasThumbnail; @@ -33,6 +37,8 @@ const MainSection = memo(() => { {shareModalItem && } + {moveModalItemType && } +
diff --git a/src/components/MoverPopup/MoverPopup.tsx b/src/components/MoverPopup/MoverPopup.tsx new file mode 100644 index 0000000..2f1d06d --- /dev/null +++ b/src/components/MoverPopup/MoverPopup.tsx @@ -0,0 +1,173 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useAppDispatch, useAppSelector } from "../../hooks/store"; +import { useMoveFolders } from "../../hooks/folders"; +import { FolderInterface } from "../../types/folders"; +import CloseIcon from "../../icons/CloseIcon"; +import { resetMoveModal } from "../../reducers/selected"; +import { debounce } from "lodash"; +import Spinner from "../Spinner"; +import HomeIconOutline from "../../icons/HomeIconOutline"; +import ArrowBackIcon from "../../icons/ArrowBackIcon"; +import classNames from "classnames"; +import FolderIcon from "../../icons/FolderIcon"; + +const MoverPopup = () => { + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [parent, setParent] = useState("/"); + const [parentList, setParentList] = useState(["/"]); + const [selectedFolderID, setSelectedFolderID] = useState(""); + const file = useAppSelector((state) => state.selected.moveModal.file); + const folder = useAppSelector((state) => state.selected.moveModal.folder); + const dispatch = useAppDispatch(); + const lastSelected = useRef({ + timestamp: 0, + folderID: "", + }); + + const { data: folderList, isLoading: isLoadingFolders } = useMoveFolders( + parent, + debouncedSearch, + folder?._id + ); + + const debouncedSetSearchText = useMemo( + () => debounce(setDebouncedSearch, 500), + [] + ); + + useEffect(() => { + debouncedSetSearchText(search); + return () => { + debouncedSetSearchText.cancel(); + }; + }, [search, debouncedSetSearchText]); + + const onFolderClick = (folderID: string) => { + const currentDate = Date.now(); + + if ( + lastSelected.current.folderID === folderID && + currentDate - lastSelected.current.timestamp < 1500 + ) { + setSearch(""); + setDebouncedSearch(""); + setParentList([...parentList, folderID]); + setParent(folderID); + setSelectedFolderID(""); + } else { + setSelectedFolderID(folderID); + } + + lastSelected.current.timestamp = Date.now(); + lastSelected.current.folderID = folderID; + }; + + const onBackClick = () => { + setSearch(""); + setDebouncedSearch(""); + const newParentList = parentList.slice(0, parentList.length - 1); + if (newParentList.length <= 1) { + setParent("/"); + setParentList(["/"]); + } else { + setParentList(newParentList); + setParent(newParentList[newParentList.length - 1]); + } + }; + + const moveText = useMemo(() => { + if (selectedFolderID) { + return "Move to selected folder"; + } else if (parent === "/") { + return "Move home"; + } else { + return "Move here"; + } + }, [selectedFolderID, parent]); + + const onHomeClick = () => { + setSearch(""); + setDebouncedSearch(""); + setParent("/"); + setParentList(["/"]); + setSelectedFolderID(""); + }; + + return ( +
+
+
+ dispatch(resetMoveModal())} + /> +
+
+
+
+ + setSearch(e.target.value)} + /> + +
+
+ {!isLoadingFolders && ( + + {folderList?.map((folder: FolderInterface) => ( +
onFolderClick(folder._id)} + > + +

+ {folder.name} +

+
+ ))} +
+ )} + {isLoadingFolders && ( +
+ +
+ )} +
+
+ +
+
+
+ ); +}; + +export default MoverPopup; diff --git a/src/hooks/folders.ts b/src/hooks/folders.ts index 5d2ec5f..c2b0c50 100644 --- a/src/hooks/folders.ts +++ b/src/hooks/folders.ts @@ -1,6 +1,10 @@ import { UseQueryResult, useQuery, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; -import { getFolderInfoAPI, getFoldersListAPI } from "../api/foldersAPI"; +import { + getFolderInfoAPI, + getFoldersListAPI, + getMoveFolderListAPI, +} from "../api/foldersAPI"; import { useSelector } from "react-redux"; import { useUtils } from "./utils"; import { FolderInterface } from "../types/folders"; @@ -67,3 +71,23 @@ export const useFolder = () => { return { ...folderQuery }; }; + +export const useMoveFolders = ( + parent: string, + search: string, + folderID?: string +) => { + const moveFoldersQuery = useQuery( + [ + "move-folder-list", + { + parent, + search, + folderID, + }, + ], + getMoveFolderListAPI + ); + + return { ...moveFoldersQuery }; +}; diff --git a/src/icons/ArrowBackIcon.tsx b/src/icons/ArrowBackIcon.tsx new file mode 100644 index 0000000..76b8458 --- /dev/null +++ b/src/icons/ArrowBackIcon.tsx @@ -0,0 +1,15 @@ +type ArrowBackIconType = React.SVGAttributes; + +const ArrowBackIcon: React.FC = (props) => { + return ( + + arrow-left + + + ); +}; + +export default ArrowBackIcon; diff --git a/src/icons/FolderIcon.tsx b/src/icons/FolderIcon.tsx new file mode 100644 index 0000000..0e45fac --- /dev/null +++ b/src/icons/FolderIcon.tsx @@ -0,0 +1,14 @@ +type FolderIconType = React.SVGAttributes; + +const FolderIcon: React.FC = (props) => { + return ( + + + + ); +}; + +export default FolderIcon; diff --git a/src/reducers/mover2.ts b/src/reducers/mover2.ts new file mode 100644 index 0000000..d95086f --- /dev/null +++ b/src/reducers/mover2.ts @@ -0,0 +1,42 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { FileInterface } from "../types/file"; +import { FolderInterface } from "../types/folders"; + +export type SelectedStateType = { + type: "" | "file" | "folder"; + file: FileInterface | null; + folder: FolderInterface | null; +}; + +type MoverStateType = { + selectedItem: SelectedStateType; +}; + +const initialState: MoverStateType = { + selectedItem: { + type: "", + file: null, + folder: null, + }, +}; + +const moveSlice = createSlice({ + name: "selected", + initialState, + reducers: { + setMoverItem: (state, action: PayloadAction) => { + state.selectedItem = action.payload; + }, + resetMoverItem: (state) => { + state.selectedItem = { + type: "", + file: null, + folder: null, + }; + }, + }, +}); + +export const {} = moveSlice.actions; + +export default moveSlice.reducer; diff --git a/src/reducers/selected.ts b/src/reducers/selected.ts index dc17281..1f77bd0 100644 --- a/src/reducers/selected.ts +++ b/src/reducers/selected.ts @@ -9,6 +9,12 @@ interface MainSecionType { folder: FolderInterface | null; } +type MoveStateType = { + type: "" | "file" | "folder"; + file: FileInterface | null; + folder: FolderInterface | null; +}; + export interface SelectedStateType { mainSection: MainSecionType; popupModal: { @@ -23,6 +29,7 @@ export interface SelectedStateType { shareModal: { file: FileInterface | null; }; + moveModal: MoveStateType; } const initialState: SelectedStateType = { @@ -42,6 +49,11 @@ const initialState: SelectedStateType = { shareModal: { file: null, }, + moveModal: { + type: "", + file: null, + folder: null, + }, }; const selectedSlice = createSlice({ @@ -110,6 +122,16 @@ const selectedSlice = createSlice({ file: null, }; }, + setMoveModal: (state, action: PayloadAction) => { + state.moveModal = action.payload; + }, + resetMoveModal: (state) => { + state.moveModal = { + type: "", + file: null, + folder: null, + }; + }, }, }); @@ -122,6 +144,8 @@ export const { resetPopupSelect, setShareModal, resetShareModal, + setMoveModal, + resetMoveModal, } = selectedSlice.actions; export default selectedSlice.reducer;