From 70ef8cd2000b3218e66e0e7e6425487d329221cd Mon Sep 17 00:00:00 2001
From: Marc Seitz
Date: Sat, 27 Jul 2024 12:50:06 +0300
Subject: [PATCH 1/5] feat: remove folders and contents from dataroom
---
components/documents/folder-card.tsx | 15 ++++--
.../[id]/folders/manage/[folderId]/index.ts | 48 ++++++++++++-------
2 files changed, 41 insertions(+), 22 deletions(-)
diff --git a/components/documents/folder-card.tsx b/components/documents/folder-card.tsx
index 2cb28565b..3bdf39ade 100644
--- a/components/documents/folder-card.tsx
+++ b/components/documents/folder-card.tsx
@@ -103,7 +103,7 @@ export default function FolderCard({
},
),
{
- loading: "Deleting folder...",
+ loading: isDataroom ? "Removing folder..." : "Deleting folder...",
success: () => {
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}?root=true`,
@@ -114,9 +114,13 @@ export default function FolderCard({
mutate(
`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}${parentFolderPath}`,
);
- return "Folder deleted successfully.";
+ return isDataroom
+ ? "Folder removed successfully."
+ : "Folder deleted successfully.";
},
- error: "Failed to delete folder. Move documents first.",
+ error: isDataroom
+ ? "Failed to remove folder."
+ : "Failed to delete folder. Move documents first.",
},
);
};
@@ -242,10 +246,11 @@ export default function FolderCard({
className="text-destructive duration-200 focus:bg-destructive focus:text-destructive-foreground"
>
{isFirstClick ? (
- "Really delete?"
+ `Really ${isDataroom ? "remove" : "delete"}?`
) : (
<>
- Delete Folder
+ {" "}
+ {isDataroom ? "Remove Folder" : "Delete Folder"}
>
)}
diff --git a/pages/api/teams/[teamId]/datarooms/[id]/folders/manage/[folderId]/index.ts b/pages/api/teams/[teamId]/datarooms/[id]/folders/manage/[folderId]/index.ts
index d254a177d..886f5970e 100644
--- a/pages/api/teams/[teamId]/datarooms/[id]/folders/manage/[folderId]/index.ts
+++ b/pages/api/teams/[teamId]/datarooms/[id]/folders/manage/[folderId]/index.ts
@@ -64,28 +64,16 @@ export default async function handle(
id: folderId,
dataroomId: dataroomId,
},
- select: {
- _count: {
- select: {
- documents: true,
- childFolders: true,
- },
- },
- },
});
- 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.dataroomFolder.delete({
- where: {
- id: folderId,
- dataroomId: dataroomId,
- },
- });
+ // Delete the folder and its contents recursively
+ await deleteFolderAndContents(folderId);
return res.status(204).end(); // 204 No Content response for successful deletes
} catch (error) {
@@ -97,3 +85,29 @@ export default async function handle(
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
+
+async function deleteFolderAndContents(folderId: string) {
+ const childFoldersToDelete = await prisma.dataroomFolder.findMany({
+ where: {
+ parentId: folderId,
+ },
+ });
+
+ console.log("Deleting folder and contents", childFoldersToDelete);
+
+ for (const folder of childFoldersToDelete) {
+ await deleteFolderAndContents(folder.id);
+ }
+
+ await prisma.dataroomDocument.deleteMany({
+ where: {
+ folderId: folderId,
+ },
+ });
+
+ await prisma.dataroomFolder.delete({
+ where: {
+ id: folderId,
+ },
+ });
+}
From 0ff039aa0e41ff694aa8023e0f53b17177496edf Mon Sep 17 00:00:00 2001
From: Marc Seitz
Date: Sat, 27 Jul 2024 12:50:15 +0300
Subject: [PATCH 2/5] feat: remove documents from dataroom
---
.../datarooms/dataroom-document-card.tsx | 119 +++++++++++++++---
components/documents/documents-list.tsx | 1 +
.../[id]/documents/[documentId]/index.ts | 55 ++++++++
3 files changed, 161 insertions(+), 14 deletions(-)
diff --git a/components/datarooms/dataroom-document-card.tsx b/components/datarooms/dataroom-document-card.tsx
index b9932a672..96b703bd7 100644
--- a/components/datarooms/dataroom-document-card.tsx
+++ b/components/datarooms/dataroom-document-card.tsx
@@ -1,5 +1,6 @@
import Image from "next/image";
import Link from "next/link";
+import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
@@ -34,21 +35,26 @@ import { MoveToDataroomFolderModal } from "./move-dataroom-folder-modal";
type DocumentsCardProps = {
document: DataroomFolderDocument;
teamInfo: TeamContextType | null;
+ dataroomId: string;
};
export default function DataroomDocumentCard({
document: dataroomDocument,
teamInfo,
+ dataroomId,
}: DocumentsCardProps) {
const { theme, systemTheme } = useTheme();
const isLight =
theme === "light" || (theme === "system" && systemTheme === "light");
+ const router = useRouter();
- const { isCopied, copyToClipboard } = useCopyToClipboard({});
const [isFirstClick, setIsFirstClick] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [moveFolderOpen, setMoveFolderOpen] = useState(false);
const dropdownRef = useRef(null);
+ /** current folder name */
+ const currentFolderPath = router.query.name as string[] | undefined;
+
// https://github.com/radix-ui/primitives/issues/1241#issuecomment-1888232392
useEffect(() => {
if (!moveFolderOpen) {
@@ -58,6 +64,89 @@ export default function DataroomDocumentCard({
}
}, [moveFolderOpen]);
+ 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);
+ };
+ }, []);
+
+ const handleButtonClick = (event: any, documentId: string) => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ console.log("isFirstClick", isFirstClick);
+ if (isFirstClick) {
+ handleRemoveDocument(documentId);
+ setIsFirstClick(false);
+ setMenuOpen(false); // Close the dropdown after deleting
+ } else {
+ setIsFirstClick(true);
+ }
+ };
+
+ const handleRemoveDocument = async (documentId: string) => {
+ // Prevent the first click from deleting the document
+ if (!isFirstClick) {
+ setIsFirstClick(true);
+ return;
+ }
+
+ const endpoint = currentFolderPath
+ ? `/folders/documents/${currentFolderPath.join("/")}`
+ : "/documents";
+
+ toast.promise(
+ fetch(
+ `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/documents/${documentId}`,
+ {
+ method: "DELETE",
+ },
+ ).then(() => {
+ mutate(
+ `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}${endpoint}`,
+ null,
+ {
+ populateCache: (_, docs) => {
+ return docs.filter(
+ (doc: DocumentWithLinksAndLinkCountAndViewCount) =>
+ doc.id !== documentId,
+ );
+ },
+ revalidate: false,
+ },
+ );
+ }),
+ {
+ loading: "Removing document...",
+ success: "Document removed successfully.",
+ error: "Failed to remove document. Try again.",
+ },
+ );
+ };
+
+ 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
+ }
+ };
+
return (
<>
@@ -114,7 +203,7 @@ export default function DataroomDocumentCard({
-
+
diff --git a/components/documents/documents-list.tsx b/components/documents/documents-list.tsx
index b7c4b40a7..b8c85625c 100644
--- a/components/documents/documents-list.tsx
+++ b/components/documents/documents-list.tsx
@@ -114,6 +114,7 @@ export function DocumentsList({
key={document.id}
document={document as DataroomFolderDocument}
teamInfo={teamInfo}
+ dataroomId={dataroomId}
/>
);
} else {
diff --git a/pages/api/teams/[teamId]/datarooms/[id]/documents/[documentId]/index.ts b/pages/api/teams/[teamId]/datarooms/[id]/documents/[documentId]/index.ts
index 17289f6ed..e9d8a784a 100644
--- a/pages/api/teams/[teamId]/datarooms/[id]/documents/[documentId]/index.ts
+++ b/pages/api/teams/[teamId]/datarooms/[id]/documents/[documentId]/index.ts
@@ -72,6 +72,61 @@ export default async function handle(
oldPath: currentPathName,
});
} catch (error) {}
+ } else if (req.method === "DELETE") {
+ /// DELETE /api/teams/:teamId/datarooms/:id/documents/:documentId
+ const session = await getServerSession(req, res, authOptions);
+ if (!session) {
+ res.status(401).end("Unauthorized");
+ return;
+ }
+
+ const userId = (session.user as CustomUser).id;
+ const {
+ teamId,
+ id: dataroomId,
+ documentId,
+ } = req.query as { teamId: string; id: string; documentId: string };
+
+ try {
+ const team = await prisma.team.findUnique({
+ where: {
+ id: teamId,
+ users: {
+ some: {
+ userId: userId,
+ },
+ },
+ },
+ });
+
+ if (!team) {
+ return res.status(401).end("Unauthorized");
+ }
+
+ const dataroom = await prisma.dataroom.findUnique({
+ where: {
+ id: dataroomId,
+ teamId: team.id,
+ },
+ });
+
+ if (!dataroom) {
+ return res.status(401).end("Dataroom not found");
+ }
+
+ const document = await prisma.dataroomDocument.delete({
+ where: {
+ id: documentId,
+ dataroomId: dataroomId,
+ },
+ });
+
+ if (!document) {
+ return res.status(404).end("Document not found");
+ }
+
+ return res.status(204).end(); // No Content
+ } catch (error) {}
} else {
// We only allow GET, PUT and DELETE requests
res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
From 4af0347936ffce72412cc794a7d214b018fd6270 Mon Sep 17 00:00:00 2001
From: Marc Seitz
Date: Sat, 27 Jul 2024 13:09:49 +0300
Subject: [PATCH 3/5] feat: add to dataroom modal for document
---
components/documents/document-header.tsx | 27 +++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/components/documents/document-header.tsx b/components/documents/document-header.tsx
index 5506c5e88..7fc421fd3 100644
--- a/components/documents/document-header.tsx
+++ b/components/documents/document-header.tsx
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react";
import { useTeam } from "@/context/team-context";
import { Document, DocumentVersion } from "@prisma/client";
-import { Sparkles, TrashIcon } from "lucide-react";
+import { BetweenHorizontalStartIcon, Sparkles, TrashIcon } from "lucide-react";
import { usePlausible } from "next-plausible";
import { useTheme } from "next-themes";
import { toast } from "sonner";
@@ -32,6 +32,7 @@ import { cn, getExtension } from "@/lib/utils";
import PortraitLandscape from "../shared/icons/portrait-landscape";
import LoadingSpinner from "../ui/loading-spinner";
import { AddDocumentModal } from "./add-document-modal";
+import { AddToDataroomModal } from "./move-dataroom-modal";
export default function DocumentHeader({
prismaDocument,
@@ -54,6 +55,7 @@ export default function DocumentHeader({
const [menuOpen, setMenuOpen] = useState(false);
const [isFirstClick, setIsFirstClick] = useState(false);
const [orientationLoading, setOrientationLoading] = useState(false);
+ const [addDataroomOpen, setAddDataroomOpen] = useState(false);
const nameRef = useRef(null);
const enterPressedRef = useRef(false);
@@ -68,6 +70,15 @@ export default function DocumentHeader({
const plausible = usePlausible();
+ // https://github.com/radix-ui/primitives/issues/1241#issuecomment-1888232392
+ useEffect(() => {
+ if (!addDataroomOpen) {
+ setTimeout(() => {
+ document.body.style.pointerEvents = "";
+ });
+ }
+ }, [addDataroomOpen]);
+
const handleNameSubmit = async () => {
if (enterPressedRef.current) {
enterPressedRef.current = false;
@@ -442,6 +453,11 @@ export default function DocumentHeader({
))}
+ setAddDataroomOpen(true)}>
+
+ Add to dataroom
+
+
+
+ {addDataroomOpen ? (
+
+ ) : null}
);
}
From 814da4183aa8893b4d671221c7b378e5f44717d9 Mon Sep 17 00:00:00 2001
From: Marc Seitz
Date: Sat, 27 Jul 2024 13:15:41 +0300
Subject: [PATCH 4/5] fix(branding): show logo if present
---
pages/settings/branding.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/pages/settings/branding.tsx b/pages/settings/branding.tsx
index dcf298f2d..d63ba99c0 100644
--- a/pages/settings/branding.tsx
+++ b/pages/settings/branding.tsx
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTeam } from "@/context/team-context";
import { PlusIcon } from "lucide-react";
+import { encode } from "next-auth/jwt";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { toast } from "sonner";
import { mutate } from "swr";
@@ -395,7 +396,7 @@ export default function Branding() {
key={`branding-${brandColor}-${accentColor}`}
name="checkout-demo"
id="checkout-demo"
- src={`/nav_ppreview_demo?brandColor=${encodeURIComponent(brandColor)}&accentColor=${encodeURIComponent(accentColor)}&brandLogo=${blobUrl ? encodeURIComponent(blobUrl) : ""}`}
+ src={`/nav_ppreview_demo?brandColor=${encodeURIComponent(brandColor)}&accentColor=${encodeURIComponent(accentColor)}&brandLogo=${blobUrl ? encodeURIComponent(blobUrl) : logo ? encodeURIComponent(logo) : ""}`}
style={{
width: "1390px",
height: "831px",
From af1bdbb8407740245a9774fce0f7d36773313c99 Mon Sep 17 00:00:00 2001
From: Marc Seitz
Date: Sat, 27 Jul 2024 13:49:35 +0300
Subject: [PATCH 5/5] feat(billing): open to billing portal if user has a
subscription already
---
components/billing/upgrade-plan-modal.tsx | 23 ++++++++++++++++++++++
pages/api/teams/[teamId]/billing/manage.ts | 6 ++++++
2 files changed, 29 insertions(+)
diff --git a/components/billing/upgrade-plan-modal.tsx b/components/billing/upgrade-plan-modal.tsx
index 0720cbc1f..3134d629b 100644
--- a/components/billing/upgrade-plan-modal.tsx
+++ b/components/billing/upgrade-plan-modal.tsx
@@ -1,3 +1,5 @@
+import { useRouter } from "next/router";
+
import { useEffect, useMemo, useState } from "react";
import React from "react";
@@ -13,6 +15,7 @@ import { useAnalytics } from "@/lib/analytics";
import { STAGGER_CHILD_VARIANTS } from "@/lib/constants";
import { getStripe } from "@/lib/stripe/client";
import { PLANS } from "@/lib/stripe/utils";
+import { usePlan } from "@/lib/swr/use-billing";
import { capitalize } from "@/lib/utils";
import { DataroomTrialModal } from "../datarooms/dataroom-trial-modal";
@@ -31,12 +34,14 @@ export function UpgradePlanModal({
setOpen?: React.Dispatch>;
children?: React.ReactNode;
}) {
+ const router = useRouter();
const [plan, setPlan] = useState<"Pro" | "Business" | "Data Rooms">(
clickedPlan,
);
const [period, setPeriod] = useState<"yearly" | "monthly">("yearly");
const [clicked, setClicked] = useState(false);
const teamInfo = useTeam();
+ const { plan: teamPlan } = usePlan();
const analytics = useAnalytics();
const features = useMemo(() => {
@@ -238,6 +243,23 @@ export function UpgradePlanModal({
// @ts-ignore
// prettier-ignore
+ if (teamPlan !== "free") {
+ fetch(
+ `/api/teams/${teamInfo?.currentTeam?.id}/billing/manage`,
+ {
+ method: "POST",
+ },
+ )
+ .then(async (res) => {
+ const url = await res.json();
+ router.push(url);
+ })
+ .catch((err) => {
+ alert(err);
+ setClicked(false);
+ });
+ } else {
+
fetch(
`/api/teams/${
teamInfo?.currentTeam?.id
@@ -265,6 +287,7 @@ export function UpgradePlanModal({
alert(err);
setClicked(false);
});
+ }
}}
>{`Upgrade to ${plan} ${capitalize(period)}`}
diff --git a/pages/api/teams/[teamId]/billing/manage.ts b/pages/api/teams/[teamId]/billing/manage.ts
index f24fed6a7..2819327ea 100644
--- a/pages/api/teams/[teamId]/billing/manage.ts
+++ b/pages/api/teams/[teamId]/billing/manage.ts
@@ -5,6 +5,7 @@ import { getServerSession } from "next-auth/next";
import { errorhandler } from "@/lib/errorHandler";
import prisma from "@/lib/prisma";
import { stripe } from "@/lib/stripe";
+import { CustomUser } from "@/lib/types";
import { authOptions } from "../../../auth/[...nextauth]";
@@ -25,6 +26,11 @@ export default async function handle(
const team = await prisma.team.findUnique({
where: {
id: teamId,
+ users: {
+ some: {
+ userId: (session.user as CustomUser).id,
+ },
+ },
},
select: {
stripeId: true,