Skip to content

Commit

Permalink
Added delete folder modal with strong prompt, added logic to delete f…
Browse files Browse the repository at this point in the history
…older with its associated files and folders (#1191)

* Added delete folder modal with string prompt, added logic to delete folder and all its associated files and folder

* Chande delete folder modal to match delete dataroom model and file delete from storate

* feat: update delete folder modal and functions

- update copy for dataroom

---------

Co-authored-by: Marc Seitz <[email protected]>
  • Loading branch information
AndrewHamal and mfts authored Oct 31, 2024
1 parent 5460ffe commit e454bba
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 69 deletions.
115 changes: 115 additions & 0 deletions components/documents/delete-folder-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { FileIcon, Files, Folder } from "lucide-react";

import { Button } from "@/components/ui/button";

import { useMediaQuery } from "@/lib/utils/use-media-query";

import { CardDescription, CardTitle } from "../ui/card";
import { Input } from "../ui/input";
import { Modal } from "../ui/modal";

export type TSelectedDataroom = { id: string; name: string } | null;

export function DeleteFolderModal({
open,
setOpen,
folderName,
folderId,
documents,
childFolders,
isDataroom,
handleButtonClick,
}: {
open: boolean;
folderId: string;
folderName?: string;
documents: number;
childFolders: number;
isDataroom?: boolean;
handleButtonClick?: any;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { isMobile } = useMediaQuery();

return (
<Modal showModal={open} setShowModal={setOpen}>
<div className="flex flex-col items-center justify-center space-y-3 border-b border-border bg-white px-4 py-4 pt-8 dark:border-gray-900 dark:bg-gray-900 sm:px-8">
<CardTitle>{isDataroom ? "Remove Folder" : "Delete Folder"}</CardTitle>
<CardDescription>
{isDataroom
? "This will remove the folder and its contents from this dataroom. The original documents will remain in your workspace."
: "This will permanently delete the folder and all its contents, including subfolders, documents, dataroom references, and any visitor analytics."}
<div className="mt-3 flex items-center gap-5">
<span className="flex items-center gap-1 text-xs font-medium text-destructive">
<FileIcon size={15} /> {documents}{" "}
{documents > 1 ? "documents" : "document"}
</span>
<span className="flex items-center gap-1 text-xs font-medium text-destructive">
<Folder size={15} /> {childFolders}{" "}
{childFolders > 1 ? "folders" : "folder"}
</span>
</div>
</CardDescription>
</div>

<form
onSubmit={async (e) => {
e.preventDefault();
handleButtonClick(e, folderId);
}}
className="flex flex-col space-y-6 bg-muted px-4 py-8 text-left dark:bg-gray-900 sm:px-8"
>
<div>
<label
htmlFor="dataroom-name"
className="block text-sm font-medium text-muted-foreground"
>
Enter the folder name{" "}
<span className="font-semibold text-foreground">{folderName}</span>{" "}
to continue:
</label>

<div className="relative mt-1 rounded-md shadow-sm">
<Input
type="text"
name="dataroom-name"
id="dataroom-name"
autoFocus={!isMobile}
autoComplete="off"
required
pattern={folderName}
className="bg-white dark:border-gray-500 dark:bg-gray-800 focus:dark:bg-transparent"
/>
</div>
</div>

<div>
<label
htmlFor="verification"
className="block text-sm text-muted-foreground"
>
To verify, type{" "}
<span className="font-semibold text-foreground">
confirm {isDataroom ? "remove" : "delete"} folder
</span>{" "}
below
</label>
<div className="relative mt-1 rounded-md shadow-sm">
<Input
type="text"
name="verification"
id="verification"
pattern={`confirm ${isDataroom ? "remove" : "delete"} folder`}
required
autoComplete="off"
className="bg-white dark:border-gray-500 dark:bg-gray-800 focus:dark:bg-transparent"
/>
</div>
</div>
<Button variant="destructive">
Confirm {isDataroom ? "remove" : "delete"} folder
</Button>
</form>
</Modal>
);
}
95 changes: 35 additions & 60 deletions components/documents/folder-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { TeamContextType } from "@/context/team-context";
import {
BetweenHorizontalStartIcon,
FolderIcon,
FolderPenIcon,
MoreVertical,
PackagePlusIcon,
TrashIcon,
} from "lucide-react";
import { toast } from "sonner";
Expand All @@ -28,6 +30,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";

type FolderCardProps = {
folder: FolderWithCount | DataroomFolderWithCount;
Expand All @@ -47,10 +50,9 @@ export default function FolderCard({
}: FolderCardProps) {
const router = useRouter();
const [openFolder, setOpenFolder] = useState<boolean>(false);
const [isFirstClick, setIsFirstClick] = useState<boolean>(false);
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const [addDataroomOpen, setAddDataroomOpen] = useState<boolean>(false);

const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement | null>(null);

const folderPath =
Expand All @@ -62,49 +64,25 @@ export default function FolderCard({
folder.path.lastIndexOf("/"),
);

useEffect(() => {
function handleClickOutside(event: { target: any }) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setMenuOpen(false);
setIsFirstClick(false);
}
}

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

// https://github.com/radix-ui/primitives/issues/1241#issuecomment-1888232392
useEffect(() => {
if (!openFolder || !addDataroomOpen) {
if (!openFolder || !addDataroomOpen || !deleteModalOpen) {
setTimeout(() => {
document.body.style.pointerEvents = "";
});
}
}, [openFolder, addDataroomOpen]);
}, [openFolder, addDataroomOpen, deleteModalOpen]);

const handleButtonClick = (event: any, documentId: string) => {
event.stopPropagation();
event.preventDefault();

if (isFirstClick) {
handleDeleteFolder(documentId);
setIsFirstClick(false);
setMenuOpen(false); // Close the dropdown after deleting
} else {
setIsFirstClick(true);
}
setDeleteModalOpen(false);
handleDeleteFolder(documentId);
setMenuOpen(false);
};

const handleDeleteFolder = async (folderId: string) => {
// Prevent the first click from deleting the document
if (!isFirstClick) {
setIsFirstClick(true);
return;
}

const endpointTargetType =
isDataroom && dataroomId ? `datarooms/${dataroomId}/folders` : "folders";

Expand All @@ -129,7 +107,7 @@ export default function FolderCard({
);
return isDataroom
? "Folder removed successfully."
: "Folder deleted successfully.";
: `Folder deleted successfully with ${folder._count.documents} documents and ${folder._count.childFolders} folders`;
},
error: isDataroom
? "Failed to remove folder."
Expand All @@ -138,21 +116,6 @@ export default function FolderCard({
);
};

const handleMenuStateChange = (open: boolean) => {
if (isFirstClick) {
setMenuOpen(true); // Keep the dropdown open on the first click
return;
}

// If the menu is closed, reset the isFirstClick state
if (!open) {
setIsFirstClick(false);
setMenuOpen(false); // Ensure the dropdown is closed
} else {
setMenuOpen(true); // Open the dropdown
}
};

const handleCreateDataroom = (e: any, folderId: string) => {
e.stopPropagation();
e.preventDefault();
Expand Down Expand Up @@ -231,14 +194,14 @@ export default function FolderCard({
href={`/documents/${prismaDocument.id}`}
className="flex items-center z-10 space-x-1 rounded-md bg-gray-200 dark:bg-gray-700 px-1.5 sm:px-2 py-0.5 transition-all duration-75 hover:scale-105 active:scale-100"
>
<BarChart className="h-3 sm:h-4 w-3 sm:w-4 text-muted-foreground" />
<p className="whitespace-nowrap text-xs sm:text-sm text-muted-foreground">
<BarChart className="w-3 h-3 sm:h-4 sm:w-4 text-muted-foreground" />
<p className="text-xs whitespace-nowrap sm:text-sm text-muted-foreground">
{nFormatter(prismaDocument._count.views)}
<span className="ml-1 hidden sm:inline-block">views</span>
<span className="hidden ml-1 sm:inline-block">views</span>
</p>
</Link> */}

<DropdownMenu open={menuOpen} onOpenChange={handleMenuStateChange}>
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
// size="icon"
Expand All @@ -258,12 +221,14 @@ export default function FolderCard({
setOpenFolder(true);
}}
>
<FolderPenIcon className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
{!isDataroom ? (
<DropdownMenuItem
onClick={(e) => handleCreateDataroom(e, folder.id)}
>
<PackagePlusIcon className="mr-2 h-4 w-4" />
Create dataroom from folder
</DropdownMenuItem>
) : null}
Expand All @@ -282,17 +247,15 @@ export default function FolderCard({
<DropdownMenuSeparator />

<DropdownMenuItem
onClick={(event) => handleButtonClick(event, folder.id)}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setDeleteModalOpen(true);
}}
className="text-destructive duration-200 focus:bg-destructive focus:text-destructive-foreground"
>
{isFirstClick ? (
`Really ${isDataroom ? "remove" : "delete"}?`
) : (
<>
<TrashIcon className="mr-2 h-4 w-4" />{" "}
{isDataroom ? "Remove Folder" : "Delete Folder"}
</>
)}
<TrashIcon className="mr-2 h-4 w-4" />{" "}
{isDataroom ? "Remove Folder" : "Delete Folder"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down Expand Up @@ -326,6 +289,18 @@ export default function FolderCard({
dataroomId={dataroomId}
/>
) : null}
{deleteModalOpen ? (
<DeleteFolderModal
folderId={folder.id}
open={deleteModalOpen}
setOpen={setDeleteModalOpen}
folderName={folder.name}
documents={folder._count.documents}
childFolders={folder._count.childFolders}
isDataroom={isDataroom}
handleButtonClick={handleButtonClick}
/>
) : null}
</>
);
}
66 changes: 57 additions & 9 deletions pages/api/teams/[teamId]/folders/manage/[folderId]/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";

import { authOptions } from "@/pages/api/auth/[...nextauth]";
import slugify from "@sindresorhus/slugify";
import { getServerSession } from "next-auth/next";

import { errorhandler } from "@/lib/errorHandler";
import { deleteFile } from "@/lib/files/delete-file-server";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";

Expand Down Expand Up @@ -57,17 +57,14 @@ export default async function handle(
},
});

if (folder?._count.documents! > 0 || folder?._count.childFolders! > 0) {
return res.status(401).json({
message: "Folder contains documents or folders. Move them first",
if (!folder) {
return res.status(404).json({
message: "Folder not found",
});
}

await prisma.folder.delete({
where: {
id: folderId,
},
});
// Delete the folder and its contents
await deleteFolderAndContents(folderId);

return res.status(204).end(); // 204 No Content response for successful deletes
} catch (error) {
Expand All @@ -79,3 +76,54 @@ export default async function handle(
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

async function deleteFolderAndContents(folderId: string) {
const childFoldersToDelete = await prisma.folder.findMany({
where: {
parentId: folderId,
},
});

for (const childFolder of childFoldersToDelete) {
await deleteFolderAndContents(childFolder.id);
}

// Delete all documents in the folder
const documents = await prisma.document.findMany({
where: {
folderId: folderId,
type: {
not: "notion",
},
},
include: {
versions: {
select: {
id: true,
file: true,
type: true,
storageType: true,
},
},
},
});

documents.map(async (documentVersions: { versions: any }) => {
for (const version of documentVersions.versions) {
await deleteFile({ type: version.storageType, data: version.file });
}
});

await prisma.document.deleteMany({
where: {
folderId: folderId,
},
});

// Delete the folder itself
await prisma.folder.delete({
where: {
id: folderId,
},
});
}

0 comments on commit e454bba

Please sign in to comment.