Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the error free and tested functionality to move folders into another folder. Work for all types of folders (#933) #1379

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion components/datarooms/folders/selection-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,22 @@ export function SidebarFolderTreeSelection({
dataroomId,
selectedFolder,
setSelectedFolder,
filterFoldersFn
}: {
dataroomId: string;
selectedFolder: TSelectedFolder;
setSelectedFolder: React.Dispatch<React.SetStateAction<TSelectedFolder>>;
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 (
<SidebarFoldersSelection
folders={folders}
Expand Down
31 changes: 29 additions & 2 deletions components/documents/folder-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MoreVertical,
PackagePlusIcon,
TrashIcon,
FolderUpIcon
} from "lucide-react";
import { toast } from "sonner";
import { mutate } from "swr";
Expand All @@ -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;
Expand All @@ -53,6 +55,7 @@ export default function FolderCard({
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const [addDataroomOpen, setAddDataroomOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [moveFolderToFolderModalOpen, setMoveFolderToFolderModalOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement | null>(null);

const folderPath =
Expand All @@ -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();
Expand Down Expand Up @@ -153,6 +156,12 @@ export default function FolderCard({
router.push(folderPath);
};

const handleMoveFolderModalOpen = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setMoveFolderToFolderModalOpen(true);
}

return (
<>
<div
Expand Down Expand Up @@ -244,8 +253,16 @@ export default function FolderCard({
? "Copy folder to other dataroom"
: "Add folder to dataroom"}
</DropdownMenuItem>

<DropdownMenuSeparator />

<DropdownMenuItem
onClick={handleMoveFolderModalOpen}
>
<FolderUpIcon className="mr-2 h-4 w-4" />
Move folder
</DropdownMenuItem>

<DropdownMenuItem
onClick={(event) => {
event.preventDefault();
Expand Down Expand Up @@ -301,6 +318,16 @@ export default function FolderCard({
handleButtonClick={handleButtonClick}
/>
) : null}
{moveFolderToFolderModalOpen ? (
<MoveFoldersInToFolderModal
open={moveFolderToFolderModalOpen}
setOpen={setMoveFolderToFolderModalOpen}
folderIds={[folder.id]}
folderName={folder.name}
dataroomId={dataroomId}
/>
): null
}
</>
);
}
194 changes: 194 additions & 0 deletions components/folders/move-folders-into-folder-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { useRouter } from "next/router";

import { useState, useRef } from "react";
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,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";

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<React.SetStateAction<boolean>>;
setSelectedFolders?: React.Dispatch<React.SetStateAction<string[]>>;
dataroomId?: string;
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, dataroomId
}:ModalProps){

const router = useRouter();
const [selectedDestinationFolder, setSelectedDestinationFolder] = useState<TSelectedFolder>(null);
const [loading, setLoading] = useState<boolean>(false);

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 | (FolderWithDocuments | DataroomFolderWithDocuments)[]>(null);
const allPossibleFoldersToBeSelected = useRef<null | (FolderWithDocuments | DataroomFolderWithDocuments)[]>(null)

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");
};

// 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);

if (dataroomId){
await moveDataroomFolderIntoDataroomFolder({
selectedFolderIds: folderIds,
newParentFolderId: selectedDestinationFolder.id!,
selectedFoldersPathName: currPath ? currPath.split("/") : undefined,
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: any[] // FolderWithDocuments[] or DataroomFolderWithDocuments[]
) => {

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.
const foldersInSelectionTree = folders.filter(f => !pathsOfSelectedFolderIds.some(path => f.path.startsWith(path)));

selectedFoldersToBeMoved.current = selectedFolders;
allPossibleFoldersToBeSelected.current = foldersInSelectionTree;

return foldersInSelectionTree
};

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="text-start">
<DialogTitle>
Move
<div className="w-[376px] truncate font-bold">
{folderName ? folderName : `${folderIds.length} items`}
</div>
</DialogTitle>
<DialogDescription>Relocate your folder.</DialogDescription>
</DialogHeader>
<form>
<div className="mb-2 max-h-[75vh] overflow-y-scroll">
{
dataroomId ? (
<SidebarDataroomFolderTreeSelection
key="sidebar-dataroom-folder-tree-selection"
dataroomId={dataroomId}
selectedFolder={selectedDestinationFolder}
setSelectedFolder={setSelectedDestinationFolder}
filterFoldersFn={filterFoldersFn}
/>
) : (
<SidebarFolderTreeSelection
key="sidebar-folder-tree-selection"
selectedFolder={selectedDestinationFolder}
setSelectedFolder={setSelectedDestinationFolder}
filterFoldersFn={filterFoldersFn}
/>
)
}
</div>

<DialogFooter>
<Button
onClick={handleSubmit}
className="flex h-9 w-full gap-1"
loading={loading}
disabled={!selectedDestinationFolder}
>
{!selectedDestinationFolder ? (
"Select a folder"
) : (
<>
Move to{" "}
<span className="font-medium">{selectedDestinationFolder.name}</span>
</>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
8 changes: 7 additions & 1 deletion components/sidebar-folders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,20 @@ const SidebarFoldersSelection = ({
export function SidebarFolderTreeSelection({
selectedFolder,
setSelectedFolder,
filterFoldersFn
}: {
selectedFolder: TSelectedFolder;
setSelectedFolder: React.Dispatch<React.SetStateAction<TSelectedFolder>>;
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 (
<SidebarFoldersSelection
folders={folders}
Expand Down
Loading
Loading