From 8d7096913f84c306cf8318b61dd393096fa9aacc Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 10:42:20 -0800 Subject: [PATCH 01/12] [NEW-API-ENDPOINT | pages/api/teams/[teamId]/folders/move.ts] To move folder into another folder --- pages/api/teams/[teamId]/folders/move.ts | 300 +++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 pages/api/teams/[teamId]/folders/move.ts diff --git a/pages/api/teams/[teamId]/folders/move.ts b/pages/api/teams/[teamId]/folders/move.ts new file mode 100644 index 000000000..c26521a83 --- /dev/null +++ b/pages/api/teams/[teamId]/folders/move.ts @@ -0,0 +1,300 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { getServerSession } from "next-auth/next"; + +import prisma from "@/lib/prisma"; +import { CustomUser } from "@/lib/types"; + +import slugify from "@sindresorhus/slugify"; +import { Folder, PrismaPromise } from "@prisma/client"; + + +/** Could handle multiple folders to move to new single parent folder. In app we ideally use this to move single folder into another. + * User can move folder to anywhere except to move a parent folder into childFolder + */ +export default async function handle( + req: NextApiRequest, + res: NextApiResponse, +) { + const { + isAuthorized, + isReqBodyValid, + userTeamCredentials, + ids, + body, + } = await reqInsight(req, res); + + if (req.method === "PATCH") { + // This is able to tackle both scenarios: + // 1. every passed folderId point to the same parentFolder (At the moment) + // 2. passed foldersIds have different parentFolders (At the moment) + // 3. if this api returns 202, then all passed folder will have a common parentFolder + + const {userId, teamId} = ids; + + if (!isAuthorized || !userId){ + return res.status(401).end('Unauthorized') + }; + + if (!teamId){ + return res.status(404).end("TeamId not found") + }; + + if (!isReqBodyValid){ + return res.status(400).end("Request body need to have and with valid data types") + }; + + let {folderIds, newParentFolderId} = body; + + if (folderIds.length === 0) { + return res.status(400).end("No folder selected"); + }; + + if (newParentFolderId && folderIds.includes(newParentFolderId)){ + return res.status(400).end("A Folder cannot be moved into itself"); + }; + + const doesUserBelongsToThisTeam = !! await userTeamCredentials(); + + if (!doesUserBelongsToThisTeam) { + return res.status(403).end("Forbidden"); + }; + + let destinationParentPath : string | undefined; + let requestedFoldersToBeMoved: Folder[] = []; // in here we will store folders whose id is in folderIds + let nameConflict = false; // nameConflict occurs when destination folder's existing child 's name matches with any requested Folder's name + + const destinationParentFolderIsRoot = newParentFolderId === null; + + if (destinationParentFolderIsRoot){ + destinationParentPath = ""; + // folders who going to be the neighbors:sibling of requestedFoldersToBeMoved. + const foldersWhoExistAtRoot = await prisma.folder.findMany({ + where: { + parentId: null, + teamId + } + }); + + // Exclude if folder is already a child of destinationFolder. + folderIds = folderIds.filter(fId => !foldersWhoExistAtRoot.some(f => f.id === fId)) + + requestedFoldersToBeMoved = await prisma.folder.findMany({ + where: { + id: {in: folderIds}, + teamId + } + }); + + const requestedFolderNames = Array.from(requestedFoldersToBeMoved, (rFolder) => rFolder.name); + nameConflict = foldersWhoExistAtRoot.some(newNeighborFolder => requestedFolderNames.includes(newNeighborFolder.name)); + + + } else { + // Now we would need to make a extra query to find out the new parent folder + const destinationFolder = await prisma.folder.findUnique({ + where: { + id: newParentFolderId, + teamId + }, + include: { + childFolders: true + } + }); + + //destinationFolder does not exist. + if (!destinationFolder){ + return res.status(404).end("Destination folder not found") + }; + + // GOAL of this block is to deal if requested folder is a parent of destination folder (new desired parent folder) + if (destinationFolder.parentId){ + const requestedFolderWhoIsParentOfDestinationFolder = folderIds.find(fId => fId === destinationFolder.parentId); + if (requestedFolderWhoIsParentOfDestinationFolder){ + return res.status(400).end(`New desired parent folder <${destinationFolder.name}:${destinationFolder.path}> found to be child of one of the requested folders `) + }; + } + + // If folder already belongs to the destinationParentFolder then exclude it + folderIds = folderIds.filter(fId => !destinationFolder.childFolders.some(cFolder => cFolder.id === fId)); + // destinationParentPath will be required to assign correct path to folders + destinationParentPath = destinationFolder.path; + + requestedFoldersToBeMoved = await prisma.folder.findMany({ + where: { + id: {in: folderIds}, + teamId + } + }); + + const requestedFolderNames = Array.from(requestedFoldersToBeMoved, (rFolder) => rFolder.name); + nameConflict = destinationFolder.childFolders.some(cFolder => requestedFolderNames.includes(cFolder.name)); + }; + + if (nameConflict){ + return res.status(400).end("Folder name conflict has occurred due to matched name of one of the destination folder's existing child.") + }; + + // Goal of this block : To deal if any requested folder is a parent of destination folder (new desired parent folder) + if (!destinationParentFolderIsRoot){ + const requestedFolderWhoIsParentOfDestinationFolder = requestedFoldersToBeMoved.find(rFolder => destinationParentPath.startsWith(rFolder.path)); + if (requestedFolderWhoIsParentOfDestinationFolder){ + return res.status(400).end(`Destination folder found to be a child of <${requestedFolderWhoIsParentOfDestinationFolder.name}:${requestedFolderWhoIsParentOfDestinationFolder.path}>`) + } + }; + + /** + * ************************** UPDATE PHASE ************************** + * Tasks to do: + * 1) For every folder in `requestedFoldersToBeMoved` we need to update: + * (i) parent folder's reference + * (ii) update path value of folder and entire folder hierarchy. + * e.g: oldParentPath/folderPathName ----> newFolderPath/folderPathName, + * Even though, after [Step(i)] Parent of the entire folder hierarchy has been changed but also need to be careful about following: + * Also need to do: oldParentPath/folderPathName/childPathName ---> newParentPath/folderPathName/childPathName [:: for every sub-child] + */ + + + const foldersRecord : { + [path:string]: { + foldersToUpdate:Folder[], + // callback Fn which will be called in prisma.$transaction + callbackFnUpdate: (folderToUpdate:Folder)=>PrismaPromise + } + } = Object.fromEntries( + Array.from(requestedFoldersToBeMoved, (rFolder) => [ + // key + rFolder.path, + // value + { + foldersToUpdate: [rFolder], + // Remember this is just the callback which will be executed inside prisma.$transaction. + callbackFnUpdate: (folderToUpdate) => { + if (!folderToUpdate.path.startsWith(rFolder.path)){ + // As far i think there would no chance of this statement to occur, but still making sure that we handle it. + throw new Error(`${folderToUpdate.name}:${folderToUpdate.path} is not a child of ${rFolder.name}:${rFolder.path}`) + } + return prisma.folder.update({ + where: { + teamId_path: { + teamId, + path: folderToUpdate.path + } + }, + data: { + // If folderToUpdate is a subChild folder of requestedFolder then only `path` will be changed otherwise in the case of requestedFolderToBeMoved parentId will also be changed. + ...(folderToUpdate.path === rFolder.path && { + parentId: newParentFolderId + }), + // calculating new path value. + path: destinationParentPath + "/" + slugify(rFolder.name) + folderToUpdate.path.substring(rFolder.path.length) + } + }); + } + } + ]) + ); + + //Adding the corresponding child folders in the record. + for(let path in foldersRecord){ + const subFolders = await prisma.folder.findMany({ + where: { + path: { + // used '/' at the end so that it will not fetch folders that we already have in folderRecord[path] means i will not refetch the `requestedFoldersToBeMoved` + startsWith: path + "/" + }, + teamId, + }, + }); + foldersRecord[path].foldersToUpdate.push(...subFolders); + }; + + try { + const updatedFolders = await prisma.$transaction( + Object.values(foldersRecord).map( + ({foldersToUpdate, callbackFnUpdate}) => { + return foldersToUpdate.map(callbackFnUpdate) + } + ).flat() + ); + + if (updatedFolders.length === 0){ + return res.status(404).end("No folders were moved"); + }; + + return res.status(200).json({ + message: "Folder moved successfully", + // count of folders whose path and parent reference has been changed + updatedCount: requestedFoldersToBeMoved.length, + // total count includes child folders whose path has got updated + updatedTotalCount: updatedFolders.length, + // leading path of new parent + newPath: destinationParentPath, + }); + } catch { + return res.status(500).end("Oops! Failed to perform the DB transaction to move the folder location") + } + } else { + // We only allow PATCH requests + res.setHeader("Allow", ["PATCH"]); + return res.status(405).end(`Method ${req.method} Not Allowed`); + } +} + +async function reqInsight (req:NextApiRequest, res: NextApiResponse){ + const session = await getServerSession(req, res, authOptions); + const isAuthorized= !!session; + + // By default we assume req body is not valid + let isReqBodyValid = false; + + let {folderIds, newParentFolderId} = req.body as { + folderIds: string[] | string, + newParentFolderId: string | null + }; + + if (typeof folderIds === 'string'){ + // In the api we prefer to handle it only as string-array for consistency. + folderIds = [folderIds] + }; + + if ( + Array.isArray(folderIds) + && folderIds.every(f => typeof f === 'string') + && (newParentFolderId === null || typeof newParentFolderId === 'string') + ){ + isReqBodyValid = true; + //Ensure that there is no duplicate folderIds. + folderIds = Array.from(new Set(folderIds)) + }; + + const ids = { + teamId: req.query?.teamId as undefined | string, + userId: isAuthorized ? (session.user as CustomUser)?.id : undefined + } + + const userTeamCredentials = async () => { + const {teamId , userId} = ids; + if (teamId && userId){ + return await prisma.userTeam.findUnique({ + where: { + userId_teamId:{userId, teamId} + } + }); + }; + return null + } + + return { + isAuthorized, + session, + isReqBodyValid, + ids, + userTeamCredentials, + body: { + folderIds, + newParentFolderId + } + } +}; \ No newline at end of file From 60268375ac990aa89c397db0d26d7c9144433426 Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 11:40:00 -0800 Subject: [PATCH 02/12] [ Key-Renamed E Response ] renamed the key-name for better readablity --- pages/api/teams/[teamId]/folders/move.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/teams/[teamId]/folders/move.ts b/pages/api/teams/[teamId]/folders/move.ts index c26521a83..b4f797ad0 100644 --- a/pages/api/teams/[teamId]/folders/move.ts +++ b/pages/api/teams/[teamId]/folders/move.ts @@ -230,7 +230,7 @@ export default async function handle( // total count includes child folders whose path has got updated updatedTotalCount: updatedFolders.length, // leading path of new parent - newPath: destinationParentPath, + pathOfNewParent: destinationParentPath, }); } catch { return res.status(500).end("Oops! Failed to perform the DB transaction to move the folder location") From 8f2ea63b44925f8db97692df6d0cac26cd4ac0dc Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 11:41:12 -0800 Subject: [PATCH 03/12] [Func | move-folders-into-folder] A function that calls api to move selected folders into new parent folder --- lib/folders/move-folders-into-folder.ts | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 lib/folders/move-folders-into-folder.ts diff --git a/lib/folders/move-folders-into-folder.ts b/lib/folders/move-folders-into-folder.ts new file mode 100644 index 000000000..96abd02a1 --- /dev/null +++ b/lib/folders/move-folders-into-folder.ts @@ -0,0 +1,87 @@ +import {toast} from "sonner"; +import {mutate} from "swr"; + +type Props = { + selectedFolderIds: string | string[]; + newParentFolderId: null | string; // if null means new desired parent is root + selectedFoldersPathName?: string[]; // if undefined means selected folders exist at the root + teamId ?: string; +}; + +const apiEndpoint = (teamId:string) => `/api/teams/${teamId}/folders/move`; +const isPlural = (n:number) => n > 1; + +export const moveFoldersIntoFolder = async({ + selectedFolderIds, newParentFolderId, selectedFoldersPathName, teamId +}:Props) => { + + if (!teamId) { + toast.error("Team is required to move folders"); + return; + }; + + const selectedFoldersExistAtRoot = selectedFoldersPathName === undefined; + + const apiEndpointThatPointsToPathFromWhereFolderAreSelected = selectedFoldersExistAtRoot ? ( + `api/teams/${teamId}/folders` + ) : ( + `api/teams/${teamId}/folders/${selectedFoldersPathName.join("/")}` + ); + + if (typeof selectedFolderIds === "string"){ + selectedFolderIds = [selectedFolderIds] + }; + + try { + // Just-To-Keep-In-Mind: If one of the selected folder's name matched with newParent's child then API gonna throw error with 4xx. + const response = await fetch( + apiEndpoint(teamId),{ + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + folderIds: selectedFolderIds, + newParentFolderId + }) + } + ); + + if (!response.ok){ + throw new Error(`Failed to move folders, failed with status code ${response.status}`); + }; + + const { + updatedCount, + updatedTotalCount, + //message, + //pathOfNewParent + } = await response.json(); + + [ + apiEndpointThatPointsToPathFromWhereFolderAreSelected, + `/api/teams/${teamId}/folders?root=true`, + `/api/teams/${teamId}/folders` + ].forEach( + path => mutate(path) + ); + + let successMessage = isPlural(updatedCount) ? ( + `${updatedCount} folders moved successfully` + ) : ( + `${updatedCount} folder moved successfully` + ); + + const totalFoldersThatAreUpdated = parseInt(updatedTotalCount) + if (Number.isInteger(totalFoldersThatAreUpdated) && totalFoldersThatAreUpdated > selectedFolderIds.length){ + const noOfSubFoldersUpdated = totalFoldersThatAreUpdated - selectedFolderIds.length; + successMessage += (" " + `including their ${noOfSubFoldersUpdated} sub folder${isPlural(noOfSubFoldersUpdated) ? "s" : ""}`) + } + + toast.success(successMessage) + + } catch (error) { + toast.error("Failed to move folders"); + mutate(apiEndpointThatPointsToPathFromWhereFolderAreSelected) + } +}; \ No newline at end of file From 4f27d679afdb26e4dc466a6538982be6eba2a238 Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 12:15:55 -0800 Subject: [PATCH 04/12] [New-Modal | move-folders-into-folder-modal] Added new Modal which allows to select folders so they can be moved --- .../move-folders-into-folder-modal.tsx | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 components/folders/move-folders-into-folder-modal.tsx diff --git a/components/folders/move-folders-into-folder-modal.tsx b/components/folders/move-folders-into-folder-modal.tsx new file mode 100644 index 000000000..9f39e0a43 --- /dev/null +++ b/components/folders/move-folders-into-folder-modal.tsx @@ -0,0 +1,121 @@ +import { useRouter } from "next/router"; + +import { useState } from "react"; +import { useTeam } from "@/context/team-context"; +import { toast } from "sonner"; + +import { SidebarFolderTreeSelection } from "@/components/sidebar-folders"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +import { moveFoldersIntoFolder } from "@/lib/folders/move-folders-into-folder"; + +type ModalProps = { + open: boolean, + setOpen: React.Dispatch>; + setSelectedFolders?: React.Dispatch>; + folderIds: string[]; + folderName?: string; +}; +export type TSelectedFolder = { id: string | null; name: string } | null; + +//util +const isString = (val:unknown) => typeof val === 'string'; + +export function MoveFoldersInToFolderModal({ + open, setOpen, setSelectedFolders, folderIds, folderName +}:ModalProps){ + + const router = useRouter(); + const [selectedDestinationFolder, setSelectedDestinationFolder] = useState(null); + const [loading, setLoading] = useState(false); + + const teamInfo = useTeam(); + const teamId = teamInfo?.currentTeam?.id; + + const currPath = router.query.name ? ( + isString(router.query.name) ? router.query.name : router.query.name.join("/") + ): ( + "" + ); + + const handleSubmit = async (event:any) => { + event.preventDefault(); + event.stopPropagation(); + + if (!selectedDestinationFolder) return; + + if (folderIds.length === 0){ + return toast.error("No folder selected!") + } + + if (selectedDestinationFolder?.id && folderIds.includes(selectedDestinationFolder.id)){ + // Even though this condition is also handled in `SidebarFolderTreeSelection` to ensure that user can't select the same folder as destinationFolder. + return toast.error("Can't move to the same folder"); + }; + + setLoading(true); + + await moveFoldersIntoFolder({ + selectedFolderIds: folderIds, + newParentFolderId: selectedDestinationFolder.id!, + selectedFoldersPathName: currPath ? currPath.split("/") : undefined, + teamId + }); + + setLoading(false); + setOpen(false); // Close the modal + setSelectedFolders?.([]); // Clear the selected folders + }; + + + return ( + + + + + Move +
+ {folderName ? folderName : `${folderIds.length} items`} +
+
+ Relocate your folder. +
+
+
+ +
+ + + + +
+
+
+ ); +} \ No newline at end of file From 68f686f5a48933f0b60ac9eeba570bfa2215c910 Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 13:12:44 -0800 Subject: [PATCH 05/12] [Modified Folder-Selection-Tree] Made this tree compatiable with new feature of moving folders into a new foder --- .../folders/move-folders-into-folder-modal.tsx | 12 ++++++++++-- components/sidebar-folders.tsx | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/components/folders/move-folders-into-folder-modal.tsx b/components/folders/move-folders-into-folder-modal.tsx index 9f39e0a43..59a7dee90 100644 --- a/components/folders/move-folders-into-folder-modal.tsx +++ b/components/folders/move-folders-into-folder-modal.tsx @@ -16,6 +16,7 @@ import { } from "@/components/ui/dialog"; import { moveFoldersIntoFolder } from "@/lib/folders/move-folders-into-folder"; +import { FolderWithDocuments } from "@/lib/swr/use-documents"; type ModalProps = { open: boolean, @@ -73,7 +74,14 @@ export function MoveFoldersInToFolderModal({ setLoading(false); setOpen(false); // Close the modal setSelectedFolders?.([]); // Clear the selected folders - }; + }; + + //In the folder tree selection, this func will exclude some folders which are invalid to be selected. + const filterFoldersFn = (folders: FolderWithDocuments[]) => { + const pathsOfSelectedFolderIds = folders.filter(f => folderIds.includes(f.id)).map(sf => sf.path); + // From the Tree selection exclude the selected folders and their corresponding child folders. + return folders.filter(f => !pathsOfSelectedFolderIds.some(path => f.path.startsWith(path))) + } return ( @@ -93,7 +101,7 @@ export function MoveFoldersInToFolderModal({ diff --git a/components/sidebar-folders.tsx b/components/sidebar-folders.tsx index 15213791e..c682d9fd1 100644 --- a/components/sidebar-folders.tsx +++ b/components/sidebar-folders.tsx @@ -213,14 +213,20 @@ const SidebarFoldersSelection = ({ export function SidebarFolderTreeSelection({ selectedFolder, setSelectedFolder, + filterFoldersFn }: { selectedFolder: TSelectedFolder; setSelectedFolder: React.Dispatch>; + filterFoldersFn ?: (folders: FolderWithDocuments[]) => FolderWithDocuments[] }) { - const { folders, error } = useFolders(); + let { folders, error } = useFolders(); if (!folders || error) return null; + if (folders && folders.length && filterFoldersFn && typeof filterFoldersFn === 'function'){ + folders = filterFoldersFn(folders) + } + return ( Date: Thu, 7 Nov 2024 14:19:27 -0800 Subject: [PATCH 06/12] [New-Drop-Down-Option | Move-Folder] Added a button to allow user to move folder location --- components/documents/folder-card.tsx | 30 ++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/components/documents/folder-card.tsx b/components/documents/folder-card.tsx index e649f35fd..9c0a6c5ad 100644 --- a/components/documents/folder-card.tsx +++ b/components/documents/folder-card.tsx @@ -10,6 +10,7 @@ import { MoreVertical, PackagePlusIcon, TrashIcon, + FolderUpIcon } from "lucide-react"; import { toast } from "sonner"; import { mutate } from "swr"; @@ -31,6 +32,7 @@ import { timeAgo } from "@/lib/utils"; import { EditFolderModal } from "../folders/edit-folder-modal"; import { AddFolderToDataroomModal } from "./add-folder-to-dataroom-modal"; import { DeleteFolderModal } from "./delete-folder-modal"; +import { MoveFoldersInToFolderModal } from "../folders/move-folders-into-folder-modal"; type FolderCardProps = { folder: FolderWithCount | DataroomFolderWithCount; @@ -53,6 +55,7 @@ export default function FolderCard({ const [menuOpen, setMenuOpen] = useState(false); const [addDataroomOpen, setAddDataroomOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [moveFolderToFolderModalOpen, setMoveFolderToFolderModalOpen] = useState(false); const dropdownRef = useRef(null); const folderPath = @@ -66,12 +69,12 @@ export default function FolderCard({ // https://github.com/radix-ui/primitives/issues/1241#issuecomment-1888232392 useEffect(() => { - if (!openFolder || !addDataroomOpen || !deleteModalOpen) { + if (!openFolder || !addDataroomOpen || !deleteModalOpen || !moveFolderToFolderModalOpen) { setTimeout(() => { document.body.style.pointerEvents = ""; }); } - }, [openFolder, addDataroomOpen, deleteModalOpen]); + }, [openFolder, addDataroomOpen, deleteModalOpen, moveFolderToFolderModalOpen]); const handleButtonClick = (event: any, documentId: string) => { event.stopPropagation(); @@ -153,6 +156,12 @@ export default function FolderCard({ router.push(folderPath); }; + const handleMoveFolderModalOpen = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setMoveFolderToFolderModalOpen(true); + } + return ( <>
+ + + + Move folder + + { event.preventDefault(); @@ -301,6 +318,15 @@ export default function FolderCard({ handleButtonClick={handleButtonClick} /> ) : null} + {moveFolderToFolderModalOpen ? ( + + ): null + } ); } From cf9ab746bb678535c038cb646b9e18c4fb291123 Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 15:00:50 -0800 Subject: [PATCH 07/12] [BUG-FIXED] corrected api endpoint to mutate --- lib/folders/move-folders-into-folder.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/folders/move-folders-into-folder.ts b/lib/folders/move-folders-into-folder.ts index 96abd02a1..acebed82d 100644 --- a/lib/folders/move-folders-into-folder.ts +++ b/lib/folders/move-folders-into-folder.ts @@ -10,6 +10,7 @@ type Props = { const apiEndpoint = (teamId:string) => `/api/teams/${teamId}/folders/move`; const isPlural = (n:number) => n > 1; +const isString = (val:unknown) => typeof val === 'string'; export const moveFoldersIntoFolder = async({ selectedFolderIds, newParentFolderId, selectedFoldersPathName, teamId @@ -23,9 +24,9 @@ export const moveFoldersIntoFolder = async({ const selectedFoldersExistAtRoot = selectedFoldersPathName === undefined; const apiEndpointThatPointsToPathFromWhereFolderAreSelected = selectedFoldersExistAtRoot ? ( - `api/teams/${teamId}/folders` + `/api/teams/${teamId}/folders` ) : ( - `api/teams/${teamId}/folders/${selectedFoldersPathName.join("/")}` + `/api/teams/${teamId}/folders/${selectedFoldersPathName.join("/")}` ); if (typeof selectedFolderIds === "string"){ @@ -55,16 +56,17 @@ export const moveFoldersIntoFolder = async({ updatedCount, updatedTotalCount, //message, - //pathOfNewParent + pathOfNewParent } = await response.json(); - [ - apiEndpointThatPointsToPathFromWhereFolderAreSelected, - `/api/teams/${teamId}/folders?root=true`, - `/api/teams/${teamId}/folders` - ].forEach( - path => mutate(path) - ); + Array.from( + new Set([ + apiEndpointThatPointsToPathFromWhereFolderAreSelected, + `/api/teams/${teamId}/folders?root=true`, + `/api/teams/${teamId}/folders`, + isString(pathOfNewParent) && `/api/teams/${teamId}/folders` + pathOfNewParent + ]) + ).forEach(path => path && mutate(path)); let successMessage = isPlural(updatedCount) ? ( `${updatedCount} folders moved successfully` From 279781062a65a963ed0a0ddd657726f14c4108bc Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 16:21:23 -0800 Subject: [PATCH 08/12] [NEW-API-ENDPOINT | pages/api/teams/[teamId]/datarooms/[id]/folders/move.ts] To move dataroom-folder into another dataroom-folder --- .../[teamId]/datarooms/[id]/folders/move.ts | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 pages/api/teams/[teamId]/datarooms/[id]/folders/move.ts diff --git a/pages/api/teams/[teamId]/datarooms/[id]/folders/move.ts b/pages/api/teams/[teamId]/datarooms/[id]/folders/move.ts new file mode 100644 index 000000000..6303f4f0a --- /dev/null +++ b/pages/api/teams/[teamId]/datarooms/[id]/folders/move.ts @@ -0,0 +1,319 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { authOptions } from "@/pages/api/auth/[...nextauth]"; +import { getServerSession } from "next-auth/next"; + +import prisma from "@/lib/prisma"; +import { CustomUser } from "@/lib/types"; + +import slugify from "@sindresorhus/slugify"; +import { DataroomFolder, PrismaPromise } from "@prisma/client"; + +export default async function handle(req:NextApiRequest, res: NextApiResponse){ + + if (req.method === "PATCH"){ + const { + isAuthorized, + isReqBodyValid, + userTeamCredentials, + teamDataroomCredentials, + ids, + body, + } = await reqInsight(req, res); + + const {userId, teamId, dataroomId} = ids; + + if (!isAuthorized || !userId){ + return res.status(401).end('Unauthorized') + }; + + if (!teamId){ + return res.status(404).end("TeamId not found") + }; + + if (!dataroomId){ + return res.status(404).end("DataroomId not found") + } + + if (!isReqBodyValid){ + return res.status(400).end("Request body need to have and with valid data types") + }; + + let {folderIds, newParentFolderId} = body; + + if (folderIds.length === 0) { + return res.status(400).end("No folder selected"); + }; + + if (newParentFolderId && folderIds.includes(newParentFolderId)){ + return res.status(400).end("A Folder cannot be moved into itself"); + }; + + const doesUserBelongsToThisTeam = !! await userTeamCredentials(); + + + if (!doesUserBelongsToThisTeam) { + return res.status(403).end("Forbidden"); + }; + // Now we are sure that user is part of the given team + + const doesThisDataroomBelongsToThisTeam = !! await teamDataroomCredentials(); + + if (!doesThisDataroomBelongsToThisTeam){ + return res.status(403).end('Forbidden') + } + // Now we are sure that this given data-room belongs to this team + + let destinationParentPath : string | undefined; + let requestedFoldersToBeMoved: DataroomFolder[] = []; // in here we will store folders whose id is in folderIds + let nameConflict = false; // nameConflict occurs when destination folder's existing child 's name matches with any requested Folder's name + + const destinationParentFolderIsRoot = newParentFolderId === null; + + if (destinationParentFolderIsRoot){ + destinationParentPath = ""; + // folders who going to be the neighbors:sibling of requestedFoldersToBeMoved. + const foldersWhoExistAtRoot = await prisma.dataroomFolder.findMany({ + where: { + parentId: null, + dataroomId + } + }); + + // Exclude if folder is already a child of destinationFolder. + folderIds = folderIds.filter(fId => !foldersWhoExistAtRoot.some(f => f.id === fId)) + + requestedFoldersToBeMoved = await prisma.dataroomFolder.findMany({ + where: { + id: {in: folderIds}, + dataroomId + } + }); + + const requestedFolderNames = Array.from(requestedFoldersToBeMoved, (rFolder) => rFolder.name); + nameConflict = foldersWhoExistAtRoot.some(newNeighborFolder => requestedFolderNames.includes(newNeighborFolder.name)); + + } else { + // Now we would need to make a extra query to find out the new parent folder + const destinationFolder = await prisma.dataroomFolder.findUnique({ + where: { + id: newParentFolderId, + dataroomId + }, + include: { + childFolders: true + } + }); + + //destinationFolder does not exist. + if (!destinationFolder){ + return res.status(404).end("Destination folder not found") + }; + + // GOAL of this block is to deal if requested folder is a parent of destination folder (new desired parent folder) + if (destinationFolder.parentId){ + const requestedFolderWhoIsParentOfDestinationFolder = folderIds.find(fId => fId === destinationFolder.parentId); + if (requestedFolderWhoIsParentOfDestinationFolder){ + return res.status(400).end(`New desired parent folder <${destinationFolder.name}:${destinationFolder.path}> found to be child of one of the requested folders `) + }; + }; + + // If folder already belongs to the destinationParentFolder then exclude it + folderIds = folderIds.filter(fId => !destinationFolder.childFolders.some(cFolder => cFolder.id === fId)); + // destinationParentPath will be required to assign correct path to folders + destinationParentPath = destinationFolder.path; + + requestedFoldersToBeMoved = await prisma.dataroomFolder.findMany({ + where: { + id: {in: folderIds}, + dataroomId + } + }); + + const requestedFolderNames = Array.from(requestedFoldersToBeMoved, (rFolder) => rFolder.name); + nameConflict = destinationFolder.childFolders.some(cFolder => requestedFolderNames.includes(cFolder.name)); + }; + + if (nameConflict){ + return res.status(400).end("Folder name conflict has occurred due to matched name of one of the destination folder's existing child.") + }; + + // Goal of this block : To deal if any requested folder is a parent of destination folder (new desired parent folder) + if (!destinationParentFolderIsRoot){ + const requestedFolderWhoIsParentOfDestinationFolder = requestedFoldersToBeMoved.find(rFolder => destinationParentPath.startsWith(rFolder.path)); + if (requestedFolderWhoIsParentOfDestinationFolder){ + return res.status(400).end(`Destination folder found to be a child of <${requestedFolderWhoIsParentOfDestinationFolder.name}:${requestedFolderWhoIsParentOfDestinationFolder.path}>`) + } + }; + + /** + * ************************** UPDATE PHASE ************************** + * Tasks to do: + * 1) For every folder in `requestedFoldersToBeMoved` we need to update: + * (i) parent folder's reference + * (ii) update path value of folder and entire folder hierarchy. + * e.g: oldParentPath/folderPathName ----> newFolderPath/folderPathName, + * Even though, after [Step(i)] Parent of the entire folder hierarchy has been changed but also need to be careful about following: + * Also need to do: oldParentPath/folderPathName/childPathName ---> newParentPath/folderPathName/childPathName [:: for every sub-child] + */ + + + const foldersRecord : { + [path:string]: { + foldersToUpdate:DataroomFolder[], + // callback Fn which will be called in prisma.$transaction + callbackFnUpdate: (folderToUpdate:DataroomFolder)=>PrismaPromise + } + } = Object.fromEntries( + Array.from(requestedFoldersToBeMoved, (rFolder) => [ + // key + rFolder.path, + // value + { + foldersToUpdate: [rFolder], + // Remember this is just the callback which will be executed inside prisma.$transaction. + callbackFnUpdate: (folderToUpdate) => { + if (!folderToUpdate.path.startsWith(rFolder.path)){ + // As far i think there would no chance of this statement to occur, but still making sure that we handle it. + throw new Error(`${folderToUpdate.name}:${folderToUpdate.path} is not a child of ${rFolder.name}:${rFolder.path}`) + } + return prisma.dataroomFolder.update({ + where: { + dataroomId_path: { + dataroomId, + path: folderToUpdate.path + } + }, + data: { + // If folderToUpdate is a subChild folder of requestedFolder then only `path` will be changed otherwise in the case of requestedFolderToBeMoved parentId will also be changed. + ...(folderToUpdate.path === rFolder.path && { + parentId: newParentFolderId + }), + // calculating new path value. + path: destinationParentPath + "/" + slugify(rFolder.name) + folderToUpdate.path.substring(rFolder.path.length) + } + }); + } + } + ]) + ); + + //Adding the corresponding child folders in the record. + for(let path in foldersRecord){ + const subFolders = await prisma.dataroomFolder.findMany({ + where: { + path: { + // used '/' at the end so that it will not fetch folders that we already have in folderRecord[path] means i will not refetch the `requestedFoldersToBeMoved` + startsWith: path + "/" + }, + dataroomId, + }, + }); + foldersRecord[path].foldersToUpdate.push(...subFolders); + }; + + try { + + const updatedFolders = await prisma.$transaction( + Object.values(foldersRecord).map( + ({foldersToUpdate, callbackFnUpdate}) => { + return foldersToUpdate.map(callbackFnUpdate) + } + ).flat() + ); + + if (updatedFolders.length === 0){ + return res.status(404).end("No folders were moved"); + }; + + return res.status(200).json({ + message: "Folder moved successfully", + // count of folders whose path and parent reference has been changed + updatedCount: requestedFoldersToBeMoved.length, + // total count includes child folders whose path has got updated + updatedTotalCount: updatedFolders.length, + // leading path of new parent + pathOfNewParent: destinationParentPath, + }); + } catch { + return res.status(500).end("Oops! Failed to perform the DB transaction to move the folder location") + } + } else { + res.setHeader("Allow", ["PATCH"]); + return res.status(405).end(`Method ${req.method} Not Allowed`); + } +} + + +async function reqInsight(req:NextApiRequest, res: NextApiResponse){ + const session = await getServerSession(req, res, authOptions); + const isAuthorized= !!session; + + // By default we assume req body is not valid + let isReqBodyValid = false; + + + let {folderIds, newParentFolderId} = req.body as { + folderIds: string[] | string, + newParentFolderId: string | null + }; + + if (typeof folderIds === 'string'){ + // In the api we prefer to handle it only as string-array for consistency. + folderIds = [folderIds] + }; + + if ( + Array.isArray(folderIds) + && folderIds.every(f => typeof f === 'string') + && (newParentFolderId === null || typeof newParentFolderId === 'string') + ){ + isReqBodyValid = true; + //Ensure that there is no duplicate folderIds. + folderIds = Array.from(new Set(folderIds)) + }; + + const ids = { + teamId: req.query?.teamId as undefined | string, + dataroomId: req.query?.id as undefined | string, + userId: isAuthorized ? (session.user as CustomUser)?.id : undefined + } + + const userTeamCredentials = async () => { + const {teamId , userId} = ids; + if (teamId && userId){ + return await prisma.userTeam.findUnique({ + where: { + userId_teamId:{userId, teamId} + } + }); + }; + return null + }; + + const teamDataroomCredentials = async () => { + const {teamId, dataroomId} = ids; + if (teamId && dataroomId){ + return await prisma.dataroom.findUnique({ + where: { + id: dataroomId, + teamId + } + }) + }; + return null + } + + return { + isAuthorized, + session, + isReqBodyValid, + ids, + userTeamCredentials, + teamDataroomCredentials, + body: { + folderIds, + newParentFolderId + } + } +} \ No newline at end of file From 3ce0f7d718c5f757e98ebcec1e9d603eff1c8339 Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 19:28:14 -0800 Subject: [PATCH 09/12] [Modified | Fn | move-folders-into-folder.ts] Now this function is able to handle both dataroom folder and also ordinary folders to be moved freely and makes approriate API call to move folder location --- lib/folders/move-folders-into-folder.ts | 64 +++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/folders/move-folders-into-folder.ts b/lib/folders/move-folders-into-folder.ts index acebed82d..38507554e 100644 --- a/lib/folders/move-folders-into-folder.ts +++ b/lib/folders/move-folders-into-folder.ts @@ -1,21 +1,31 @@ import {toast} from "sonner"; import {mutate} from "swr"; -type Props = { +type BaseProps = { selectedFolderIds: string | string[]; newParentFolderId: null | string; // if null means new desired parent is root selectedFoldersPathName?: string[]; // if undefined means selected folders exist at the root teamId ?: string; }; -const apiEndpoint = (teamId:string) => `/api/teams/${teamId}/folders/move`; const isPlural = (n:number) => n > 1; const isString = (val:unknown) => typeof val === 'string'; -export const moveFoldersIntoFolder = async({ - selectedFolderIds, newParentFolderId, selectedFoldersPathName, teamId -}:Props) => { +type Props = BaseProps & { + API_ENDPOINT: string; + //base_path should not end with '/' + BASE_PATH: string; +} +//Base function +const moveFolder = async({ + selectedFolderIds, + newParentFolderId, + selectedFoldersPathName, + teamId, + API_ENDPOINT, + BASE_PATH +}: Props) => { if (!teamId) { toast.error("Team is required to move folders"); return; @@ -23,10 +33,14 @@ export const moveFoldersIntoFolder = async({ const selectedFoldersExistAtRoot = selectedFoldersPathName === undefined; + if (BASE_PATH.endsWith("/")){ + BASE_PATH = BASE_PATH.substring(0, BASE_PATH.length - 1) + } + const apiEndpointThatPointsToPathFromWhereFolderAreSelected = selectedFoldersExistAtRoot ? ( - `/api/teams/${teamId}/folders` + BASE_PATH ) : ( - `/api/teams/${teamId}/folders/${selectedFoldersPathName.join("/")}` + `${BASE_PATH}/${selectedFoldersPathName.join("/")}` ); if (typeof selectedFolderIds === "string"){ @@ -36,7 +50,7 @@ export const moveFoldersIntoFolder = async({ try { // Just-To-Keep-In-Mind: If one of the selected folder's name matched with newParent's child then API gonna throw error with 4xx. const response = await fetch( - apiEndpoint(teamId),{ + API_ENDPOINT , { method: "PATCH", headers: { "Content-Type": "application/json" @@ -61,10 +75,10 @@ export const moveFoldersIntoFolder = async({ Array.from( new Set([ + BASE_PATH, apiEndpointThatPointsToPathFromWhereFolderAreSelected, - `/api/teams/${teamId}/folders?root=true`, - `/api/teams/${teamId}/folders`, - isString(pathOfNewParent) && `/api/teams/${teamId}/folders` + pathOfNewParent + `${BASE_PATH}?root=true`, + isString(pathOfNewParent) && BASE_PATH + pathOfNewParent ]) ).forEach(path => path && mutate(path)); @@ -80,10 +94,32 @@ export const moveFoldersIntoFolder = async({ successMessage += (" " + `including their ${noOfSubFoldersUpdated} sub folder${isPlural(noOfSubFoldersUpdated) ? "s" : ""}`) } - toast.success(successMessage) + toast.success(successMessage); - } catch (error) { + } catch { toast.error("Failed to move folders"); mutate(apiEndpointThatPointsToPathFromWhereFolderAreSelected) } -}; \ No newline at end of file +}; + + +export const moveFoldersIntoFolder = ( + props: BaseProps +) => { + const BASE_PATH = `/api/teams/${props.teamId}/folders`; + const API_ENDPOINT = `/api/teams/${props.teamId}/folders/move`; + return moveFolder({...props, BASE_PATH, API_ENDPOINT}) +}; + +type MoveDataroomFolderProps = BaseProps & { + dataroomId: string +} + +export const moveDataroomFolderIntoDataroomFolder = ( + props: MoveDataroomFolderProps +) => { + const {dataroomId, ...baseProps} = props; + const BASE_PATH = `/api/teams/${props.teamId}/datarooms/${dataroomId}/folders`; + const API_ENDPOINT = `/api/teams/${props.teamId}/datarooms/${dataroomId}/folders/move`; + return moveFolder({...baseProps, BASE_PATH, API_ENDPOINT}) +} \ No newline at end of file From d1ee9a4d7b69bce8105284ce83d566aced13cb6a Mon Sep 17 00:00:00 2001 From: subh Date: Thu, 7 Nov 2024 19:38:55 -0800 Subject: [PATCH 10/12] [Modified | Dataroom-SidebarFolderTreeSelection] Added optional functionality of filtering folders data to made this component with new feature of moving dataroom folders into another dataroom folder --- components/datarooms/folders/selection-tree.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/datarooms/folders/selection-tree.tsx b/components/datarooms/folders/selection-tree.tsx index bdbf5acc0..86b844dc0 100644 --- a/components/datarooms/folders/selection-tree.tsx +++ b/components/datarooms/folders/selection-tree.tsx @@ -109,15 +109,22 @@ export function SidebarFolderTreeSelection({ dataroomId, selectedFolder, setSelectedFolder, + filterFoldersFn }: { dataroomId: string; selectedFolder: TSelectedFolder; setSelectedFolder: React.Dispatch>; + filterFoldersFn ?: (folders: DataroomFolderWithDocuments[]) => DataroomFolderWithDocuments[] }) { - const { folders, error } = useDataroomFoldersTree({ dataroomId }); + let { folders, error } = useDataroomFoldersTree({ dataroomId }); if (!folders || error) return null; + if (folders && folders.length && filterFoldersFn && typeof filterFoldersFn === 'function'){ + folders = filterFoldersFn(folders) + } + + return ( Date: Fri, 8 Nov 2024 11:45:44 -0800 Subject: [PATCH 11/12] [Modified | folder-card & move-folders-into-folder-nodal] made those components compatiable for the functionality to move datarrom folders into other folders --- components/documents/folder-card.tsx | 1 + .../move-folders-into-folder-modal.tsx | 58 ++++++++++++++----- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/components/documents/folder-card.tsx b/components/documents/folder-card.tsx index 9c0a6c5ad..1b9c5ace3 100644 --- a/components/documents/folder-card.tsx +++ b/components/documents/folder-card.tsx @@ -324,6 +324,7 @@ export default function FolderCard({ setOpen={setMoveFolderToFolderModalOpen} folderIds={[folder.id]} folderName={folder.name} + dataroomId={dataroomId} /> ): null } diff --git a/components/folders/move-folders-into-folder-modal.tsx b/components/folders/move-folders-into-folder-modal.tsx index 59a7dee90..3ae658f64 100644 --- a/components/folders/move-folders-into-folder-modal.tsx +++ b/components/folders/move-folders-into-folder-modal.tsx @@ -5,6 +5,7 @@ import { useTeam } from "@/context/team-context"; import { toast } from "sonner"; import { SidebarFolderTreeSelection } from "@/components/sidebar-folders"; +import { SidebarFolderTreeSelection as SidebarDataroomFolderTreeSelection } from "../datarooms/folders"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -15,13 +16,15 @@ import { DialogTitle, } from "@/components/ui/dialog"; -import { moveFoldersIntoFolder } from "@/lib/folders/move-folders-into-folder"; +import { moveDataroomFolderIntoDataroomFolder, moveFoldersIntoFolder } from "@/lib/folders/move-folders-into-folder"; import { FolderWithDocuments } from "@/lib/swr/use-documents"; +import { DataroomFolderWithDocuments } from "@/lib/swr/use-dataroom"; type ModalProps = { open: boolean, setOpen: React.Dispatch>; setSelectedFolders?: React.Dispatch>; + dataroomId?: string; folderIds: string[]; folderName?: string; }; @@ -31,7 +34,7 @@ export type TSelectedFolder = { id: string | null; name: string } | null; const isString = (val:unknown) => typeof val === 'string'; export function MoveFoldersInToFolderModal({ - open, setOpen, setSelectedFolders, folderIds, folderName + open, setOpen, setSelectedFolders, folderIds, folderName, dataroomId }:ModalProps){ const router = useRouter(); @@ -64,25 +67,37 @@ export function MoveFoldersInToFolderModal({ setLoading(true); - await moveFoldersIntoFolder({ + if (dataroomId){ + await moveDataroomFolderIntoDataroomFolder({ selectedFolderIds: folderIds, newParentFolderId: selectedDestinationFolder.id!, selectedFoldersPathName: currPath ? currPath.split("/") : undefined, - teamId - }); + teamId, + dataroomId + }) + } else { + await moveFoldersIntoFolder({ + selectedFolderIds: folderIds, + newParentFolderId: selectedDestinationFolder.id!, + selectedFoldersPathName: currPath ? currPath.split("/") : undefined, + teamId + }); + } + setLoading(false); setOpen(false); // Close the modal setSelectedFolders?.([]); // Clear the selected folders }; - + //In the folder tree selection, this func will exclude some folders which are invalid to be selected. - const filterFoldersFn = (folders: FolderWithDocuments[]) => { + const filterFoldersFn = ( + folders: any[] // FolderWithDocuments[] or DataroomFolderWithDocuments[] + ) => { const pathsOfSelectedFolderIds = folders.filter(f => folderIds.includes(f.id)).map(sf => sf.path); // From the Tree selection exclude the selected folders and their corresponding child folders. return folders.filter(f => !pathsOfSelectedFolderIds.some(path => f.path.startsWith(path))) - } - + }; return ( @@ -98,11 +113,24 @@ export function MoveFoldersInToFolderModal({
- + { + dataroomId ? ( + + ) : ( + + ) + }
@@ -126,4 +154,4 @@ export function MoveFoldersInToFolderModal({
); -} \ No newline at end of file +}; \ No newline at end of file From f4b3a90e37ca69f75812697471aa0de7db13947e Mon Sep 17 00:00:00 2001 From: subh Date: Fri, 8 Nov 2024 14:52:55 -0800 Subject: [PATCH 12/12] [Modified | Implemented more checks before making API req to move folders] In this modification code is giving more meaningful toast errors to user to identify the cause of invalid folder selection. --- .../move-folders-into-folder-modal.tsx | 45 +++++++++++++++++-- lib/folders/move-folders-into-folder.ts | 2 +- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/components/folders/move-folders-into-folder-modal.tsx b/components/folders/move-folders-into-folder-modal.tsx index 3ae658f64..357de78f1 100644 --- a/components/folders/move-folders-into-folder-modal.tsx +++ b/components/folders/move-folders-into-folder-modal.tsx @@ -1,6 +1,6 @@ import { useRouter } from "next/router"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { useTeam } from "@/context/team-context"; import { toast } from "sonner"; @@ -44,6 +44,10 @@ export function MoveFoldersInToFolderModal({ const teamInfo = useTeam(); const teamId = teamInfo?.currentTeam?.id; + // The following refs can be used to give more meaningful toast error messages to the user. + const selectedFoldersToBeMoved = useRef(null); + const allPossibleFoldersToBeSelected = useRef(null) + const currPath = router.query.name ? ( isString(router.query.name) ? router.query.name : router.query.name.join("/") ): ( @@ -58,12 +62,37 @@ export function MoveFoldersInToFolderModal({ if (folderIds.length === 0){ return toast.error("No folder selected!") - } + }; if (selectedDestinationFolder?.id && folderIds.includes(selectedDestinationFolder.id)){ // Even though this condition is also handled in `SidebarFolderTreeSelection` to ensure that user can't select the same folder as destinationFolder. return toast.error("Can't move to the same folder"); }; + + // Before making API call this block verify the validity of selection and would give more meaningful error messages to user. + if (allPossibleFoldersToBeSelected.current && selectedFoldersToBeMoved.current){ + + // Handle if same parent selected + const oldParentEqualsNewParent = !! selectedFoldersToBeMoved.current.find(folder => folder.parentId === selectedDestinationFolder.id); + if (oldParentEqualsNewParent){ + return toast.error("Please select a different folder other than the existing parent folder") + }; + + // Handle if nameConflict is found. Under a parent it is must that each child has its unique name so that a unique pathname can be produced. + const existingChildrenOfNewParent = allPossibleFoldersToBeSelected.current.filter(folder => folder.parentId === selectedDestinationFolder.id); + + const duplicateName = existingChildrenOfNewParent.find(existingChild => selectedFoldersToBeMoved.current?.some(folder => folder.name === existingChild.name))?.name; + + if (!!duplicateName){ + const newDestinationParentName = selectedDestinationFolder.id === null ? ( + 'HOME' + ) : ( + allPossibleFoldersToBeSelected.current.find(folder => folder.id === selectedDestinationFolder.id)?.name ?? "parent" + ) + return toast.error(`Oops! A folder with the name of "${duplicateName}" already exist at "${newDestinationParentName}" directory. Each child folder must have a unique name`) + }; + + } setLoading(true); @@ -94,9 +123,17 @@ export function MoveFoldersInToFolderModal({ const filterFoldersFn = ( folders: any[] // FolderWithDocuments[] or DataroomFolderWithDocuments[] ) => { - const pathsOfSelectedFolderIds = folders.filter(f => folderIds.includes(f.id)).map(sf => sf.path); + + const selectedFolders = folders.filter(f => folderIds.includes(f.id)) + const pathsOfSelectedFolderIds = selectedFolders.map(sf => sf.path); + // From the Tree selection exclude the selected folders and their corresponding child folders. - return folders.filter(f => !pathsOfSelectedFolderIds.some(path => f.path.startsWith(path))) + const foldersInSelectionTree = folders.filter(f => !pathsOfSelectedFolderIds.some(path => f.path.startsWith(path))); + + selectedFoldersToBeMoved.current = selectedFolders; + allPossibleFoldersToBeSelected.current = foldersInSelectionTree; + + return foldersInSelectionTree }; return ( diff --git a/lib/folders/move-folders-into-folder.ts b/lib/folders/move-folders-into-folder.ts index 38507554e..c9a7e152e 100644 --- a/lib/folders/move-folders-into-folder.ts +++ b/lib/folders/move-folders-into-folder.ts @@ -91,7 +91,7 @@ const moveFolder = async({ const totalFoldersThatAreUpdated = parseInt(updatedTotalCount) if (Number.isInteger(totalFoldersThatAreUpdated) && totalFoldersThatAreUpdated > selectedFolderIds.length){ const noOfSubFoldersUpdated = totalFoldersThatAreUpdated - selectedFolderIds.length; - successMessage += (" " + `including their ${noOfSubFoldersUpdated} sub folder${isPlural(noOfSubFoldersUpdated) ? "s" : ""}`) + successMessage += (" " + `including ${noOfSubFoldersUpdated} sub folder${isPlural(noOfSubFoldersUpdated) ? "s" : ""}`) } toast.success(successMessage);