diff --git a/Pipfile b/Pipfile index 1caf986bd..6ab55abc1 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -tinybird-cli = "3.3.0" +tinybird-cli = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index df35bf723..2115995af 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8e0f1f5a4e8effaf6521468f6532c63960c444e575b10c2fe2efdf81252899f9" + "sha256": "cc66168335ac666b7948edb82b78d888779351fe109225cb8b797a3d117ed0ba" }, "pipfile-spec": 6, "requires": { @@ -26,30 +26,30 @@ }, "black": { "hashes": [ - "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8", - "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8", - "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd", - "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", - "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31", - "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92", - "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f", - "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29", - "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4", - "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", - "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218", - "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a", - "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23", - "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0", - "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", - "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", - "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540", - "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430", - "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", - "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2", - "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", - "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d" - ], - "version": "==24.2.0" + "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", + "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", + "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", + "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", + "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", + "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", + "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", + "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", + "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", + "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", + "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", + "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", + "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", + "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", + "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", + "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", + "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", + "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", + "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", + "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", + "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", + "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" + ], + "version": "==24.3.0" }, "certifi": { "hashes": [ @@ -494,11 +494,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -591,12 +591,12 @@ }, "tinybird-cli": { "hashes": [ - "sha256:72bf317c3eb8dbc75f70ecd6d0bb8fc0631dcbdead3fc4e18f510aef084c60c6", - "sha256:81467425301b88a40e3032ad02d701ba5fee08e09efba1d5e296c80ec71c547b" + "sha256:28ea7c3f6d92a7ba45f372a46c751acd8d3c2b38b405242f3be162530c64b251", + "sha256:53a313fcdf4ef285e7c4c677c0c940d429eee2edfbcf2e7defae50b19018ed4d" ], "index": "pypi", "markers": "python_version < '3.12' and python_version >= '3.8'", - "version": "==3.3.0" + "version": "==3.5.0" }, "toposort": { "hashes": [ @@ -644,11 +644,11 @@ }, "wheel": { "hashes": [ - "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d", - "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8" + "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85", + "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81" ], - "markers": "python_version >= '3.7'", - "version": "==0.42.0" + "markers": "python_version >= '3.8'", + "version": "==0.43.0" } }, "develop": {} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 00b0d8a49..cb23cb8ff 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -20,9 +20,10 @@ import { PlusIcon, FolderIcon as FolderLucideIcon, FolderOpenIcon, + ServerIcon, } from "lucide-react"; import SiderbarFolders from "./sidebar-folders"; -import { AddFolderModal } from "./documents/add-folder-modal"; +import { AddFolderModal } from "./folders/add-folder-modal"; import { ScrollArea } from "./ui/scroll-area"; export default function Sidebar() { @@ -97,6 +98,14 @@ export const SidebarComponent = ({ className }: { className?: string }) => { active: router.pathname.includes("documents"), disabled: false, }, + { + name: "Datarooms", + href: "/datarooms", + icon: ServerIcon, + current: router.pathname.includes("datarooms"), + active: false, + disabled: false, + }, { name: "Branding", href: "/settings/branding", diff --git a/components/datarooms/add-dataroom-document-modal.tsx b/components/datarooms/add-dataroom-document-modal.tsx new file mode 100644 index 000000000..7e203a599 --- /dev/null +++ b/components/datarooms/add-dataroom-document-modal.tsx @@ -0,0 +1,118 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useTeam } from "@/context/team-context"; +import { useState } from "react"; +import { toast } from "sonner"; +import { mutate } from "swr"; +import { SidebarFolderTreeSelection } from "@/components/sidebar-folders"; +import { useRouter } from "next/router"; + +export function AddDataroomDocumentModal({ + open, + setOpen, + dataroomId, + documentName, +}: { + open: boolean; + setOpen: React.Dispatch>; + dataroomId?: string; + documentName?: string; +}) { + const router = useRouter(); + const [folderId, setFolderId] = useState(""); + const [documentId, setDocumentId] = useState(""); + const [loading, setLoading] = useState(false); + + const teamInfo = useTeam(); + + /** current folder name */ + const currentFolderPath = router.query.name as string[] | undefined; + + const handleSubmit = async (event: any) => { + event.preventDefault(); + event.stopPropagation(); + + if (folderId === "") return; + + setLoading(true); + try { + const response = await fetch( + `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/documents`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + documentId: documentId, + folderPathName: currentFolderPath?.join("/"), + }), + }, + ); + + if (!response.ok) { + const { message } = await response.json(); + setLoading(false); + toast.error(message); + return; + } + + const { newPath, oldPath } = (await response.json()) as { + newPath: string; + oldPath: string; + }; + + toast.success("Document moved successfully!"); + + // mutate(`/api/teams/${teamInfo?.currentTeam?.id}/folders`); + // mutate(`/api/teams/${teamInfo?.currentTeam?.id}/folders${oldPath}`); + // mutate(`/api/teams/${teamInfo?.currentTeam?.id}/folders${newPath}`).then( + // () => { + // router.push(`/documents/tree${newPath}`); + // }, + // ); + } catch (error) { + console.error("Error moving document", error); + toast.error("Failed to move document. Try again."); + } finally { + setLoading(false); + setOpen(false); + } + }; + + return ( + + + + Add Document + Add your document to dataroom + +
+
+ +
+ + + + +
+
+
+ ); +} diff --git a/components/datarooms/add-dataroom-modal.tsx b/components/datarooms/add-dataroom-modal.tsx new file mode 100644 index 000000000..d3c881c43 --- /dev/null +++ b/components/datarooms/add-dataroom-modal.tsx @@ -0,0 +1,113 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useTeam } from "@/context/team-context"; +import { useState } from "react"; +import { toast } from "sonner"; +import { UpgradePlanModal } from "../billing/upgrade-plan-modal"; +import { usePlan } from "@/lib/swr/use-billing"; +import { useAnalytics } from "@/lib/analytics"; +import { mutate } from "swr"; + +export function AddDataroomModal({ children }: { children?: React.ReactNode }) { + const [dataroomName, setDataroomName] = useState(""); + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + + const teamInfo = useTeam(); + const { plan } = usePlan(); + const analytics = useAnalytics(); + + const handleSubmit = async (event: any) => { + event.preventDefault(); + event.stopPropagation(); + + if (dataroomName == "") return; + + setLoading(true); + + try { + const response = await fetch( + `/api/teams/${teamInfo?.currentTeam?.id}/datarooms`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: dataroomName, + }), + }, + ); + + if (!response.ok) { + const { message } = await response.json(); + setLoading(false); + toast.error(message); + return; + } + + analytics.capture("Dataroom Created", { dataroomName: dataroomName }); + toast.success("Dataroom successfully created! 🎉"); + + mutate(`/api/teams/${teamInfo?.currentTeam?.id}/datarooms`); + } catch (error) { + setLoading(false); + toast.error("Error adding folder. Please try again."); + return; + } finally { + setLoading(false); + setOpen(false); + } + }; + + // If the team is on a free plan, show the upgrade modal + if (plan && plan.plan === "free") { + if (children) { + return ( + + {children} + + ); + } + } + + return ( + + {children} + + + Create dataroom + + Start creating a dataroom with a name. + + +
+ + setDataroomName(e.target.value)} + /> + + + +
+
+
+ ); +} diff --git a/components/datarooms/dataroom-breadcrumb.tsx b/components/datarooms/dataroom-breadcrumb.tsx new file mode 100644 index 000000000..9b32d0d5d --- /dev/null +++ b/components/datarooms/dataroom-breadcrumb.tsx @@ -0,0 +1,79 @@ +import React, { useMemo } from "react"; +import Link from "next/link"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { useRouter } from "next/router"; +import { useDataroomFolderWithParents } from "@/lib/swr/use-dataroom"; + +function BreadcrumbComponentBase({ + name, + dataroomId, +}: { + name: string[]; + dataroomId: string; +}) { + const { folders: folderNames } = useDataroomFolderWithParents({ + name, + dataroomId, + }); + + return ( + + + + + Home + + + {folderNames && + folderNames.map((item, index: number, array) => { + return ( + + + {index === array.length - 1 ? ( + + + {item.name} + + + ) : ( + + + + {item.name} + + + + )} + + ); + })} + + + ); +} + +const BreadcrumbComponent = () => { + const router = useRouter(); + const name = router.query.name as string[]; + const dataroomId = router.query.id as string; + + // Use useMemo to memoize the base component with the current name value. + // This way, BreadcrumbComponentBase is only re-rendered when name changes. + const MemoizedBreadcrumbComponent = useMemo(() => { + return ; + }, [name, dataroomId]); + + return MemoizedBreadcrumbComponent; +}; + +export { BreadcrumbComponent }; diff --git a/components/datarooms/dataroom-document-card.tsx b/components/datarooms/dataroom-document-card.tsx new file mode 100644 index 000000000..6e4786d1d --- /dev/null +++ b/components/datarooms/dataroom-document-card.tsx @@ -0,0 +1,148 @@ +import { nFormatter, timeAgo } from "@/lib/utils"; +import Link from "next/link"; +import { type DocumentWithLinksAndLinkCountAndViewCount } from "@/lib/types"; +import { TeamContextType } from "@/context/team-context"; +import BarChart from "@/components/shared/icons/bar-chart"; +import Image from "next/image"; +import NotionIcon from "@/components/shared/icons/notion"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { TrashIcon, MoreVertical, FolderInputIcon } from "lucide-react"; +import { mutate } from "swr"; +import { useCopyToClipboard } from "@/lib/utils/use-copy-to-clipboard"; +import Check from "@/components/shared/icons/check"; +import Copy from "@/components/shared/icons/copy"; +import { useEffect, useRef, useState } from "react"; +import { useTheme } from "next-themes"; +import { MoveToFolderModal } from "@/components/documents/move-folder-modal"; +import { type DataroomFolderDocument } from "@/lib/swr/use-dataroom"; + +type DocumentsCardProps = { + document: DataroomFolderDocument; + teamInfo: TeamContextType | null; +}; +export default function DataroomDocumentCard({ + document, + teamInfo, +}: DocumentsCardProps) { + const { theme, systemTheme } = useTheme(); + const isLight = + theme === "light" || (theme === "system" && systemTheme === "light"); + + const { isCopied, copyToClipboard } = useCopyToClipboard({}); + const [isFirstClick, setIsFirstClick] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [moveFolderOpen, setMoveFolderOpen] = useState(false); + const dropdownRef = useRef(null); + + return ( + <> +
  • +
    +
    + {document.document.type === "notion" ? ( + + ) : ( + File icon + )} +
    + +
    +
    +

    + + {document.document.name} + + +

    +
    +
    +

    {timeAgo(document.createdAt)}

    +

    •

    + {document.document._count.versions > 1 ? ( + <> +

    •

    +

    {`${document.document._count.versions} Versions`}

    + + ) : null} +
    +
    +
    + +
    + { + e.stopPropagation(); + }} + href={`/documents/${document.document.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" + > + +

    + {nFormatter(document.document._count.views)} + views +

    + + + + + + + + Actions + setMoveFolderOpen(true)}> + + Move to folder + + + + {/* handleButtonClick(event, prismaDocument.id)} + className="text-destructive focus:bg-destructive focus:text-destructive-foreground duration-200" + > + {isFirstClick ? ( + "Really delete?" + ) : ( + <> + Delete document + + )} + */} + + +
    +
  • + {moveFolderOpen ? ( + + ) : null} + + ); +} diff --git a/components/datarooms/dataroom-header.tsx b/components/datarooms/dataroom-header.tsx new file mode 100644 index 000000000..3685466e4 --- /dev/null +++ b/components/datarooms/dataroom-header.tsx @@ -0,0 +1,41 @@ +export const DataroomHeader = ({ + title, + description, + actions, +}: { + title: string; + description: string; + actions?: React.ReactNode[]; +}) => { + const actionRows: React.ReactNode[][] = []; + if (actions) { + for (let i = 0; i < actions.length; i += 3) { + actionRows.push(actions.slice(i, i + 3)); + } + } + + return ( +
    +
    +

    + {title} +

    +

    + {description} +

    +
    +
    + {actionRows.map((row, i) => ( +
      + {row.map((action, i) => ( +
    • {action}
    • + ))} +
    + ))} +
    +
    + ); +}; diff --git a/components/documents/add-document-modal.tsx b/components/documents/add-document-modal.tsx index c27b5cf14..0022f6b2a 100644 --- a/components/documents/add-document-modal.tsx +++ b/components/documents/add-document-modal.tsx @@ -24,13 +24,18 @@ import { createNewDocumentVersion, } from "@/lib/documents/create-document"; import { useAnalytics } from "@/lib/analytics"; +import { mutate } from "swr"; export function AddDocumentModal({ newVersion, children, + isDataroom, + dataroomId, }: { newVersion?: boolean; children: React.ReactNode; + isDataroom?: boolean; + dataroomId?: string; }) { const router = useRouter(); const plausible = usePlausible(); @@ -94,6 +99,26 @@ export function AddDocumentModal({ if (response) { const document = await response.json(); + if (isDataroom && dataroomId) { + await addDocumentToDataroom({ + documentId: document.id, + folderPathName: currentFolderPath?.join("/"), + }); + + plausible("documentUploaded"); + analytics.capture("Document Added", { + documentId: document.id, + name: document.name, + numPages: document.numPages, + path: router.asPath, + type: "pdf", + teamId: teamId, + dataroomId: dataroomId, + }); + + return; + } + if (!newVersion) { // copy the link to the clipboard copyToClipboard( @@ -148,6 +173,51 @@ export function AddDocumentModal({ } }; + const addDocumentToDataroom = async ({ + documentId, + folderPathName, + }: { + documentId: string; + folderPathName?: string; + }) => { + try { + const response = await fetch( + `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/documents`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + documentId: documentId, + folderPathName: folderPathName, + }), + }, + ); + + if (!response.ok) { + const { message } = await response.json(); + toast.error(message); + return; + } + + mutate( + `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/documents`, + ); + mutate( + `/api/teams/${teamInfo?.currentTeam?.id}/datarooms/${dataroomId}/folders/documents/${folderPathName}`, + ); + + toast.success("Document added to dataroom successfully! 🎉"); + } catch (error) { + toast.error("Error adding document to dataroom."); + console.error( + "An error occurred while adding document to the dataroom: ", + error, + ); + } + }; + const createNotionFileName = () => { // Extract Notion file name from the URL const urlSegments = (notionLink as string).split("/")[3]; @@ -199,6 +269,27 @@ export function AddDocumentModal({ if (response) { const document = await response.json(); + if (isDataroom && dataroomId) { + await addDocumentToDataroom({ + documentId: document.id, + folderPathName: currentFolderPath?.join("/"), + }); + + plausible("documentUploaded"); + plausible("notionDocumentUploaded"); + analytics.capture("Document Added", { + documentId: document.id, + name: document.name, + numPages: document.numPages, + path: router.asPath, + type: "notion", + teamId: teamId, + dataroomId: dataroomId, + }); + + return; + } + if (!newVersion) { // copy the link to the clipboard copyToClipboard( diff --git a/components/documents/folder-card.tsx b/components/documents/folder-card.tsx index 351c551b0..cb735beb5 100644 --- a/components/documents/folder-card.tsx +++ b/components/documents/folder-card.tsx @@ -13,14 +13,27 @@ import { import { MoreVertical, FolderIcon } from "lucide-react"; import { useRef } from "react"; import { FolderWithCount } from "@/lib/swr/use-documents"; +import { DataroomFolderWithCount } from "@/lib/swr/use-dataroom"; type FolderCardProps = { - folder: FolderWithCount; + folder: FolderWithCount | DataroomFolderWithCount; teamInfo: TeamContextType | null; + isDataroom?: boolean; + dataroomId?: string; }; -export default function FolderCard({ folder, teamInfo }: FolderCardProps) { +export default function FolderCard({ + folder, + teamInfo, + isDataroom, + dataroomId, +}: FolderCardProps) { const dropdownRef = useRef(null); + const folderPath = + isDataroom && dataroomId + ? `/datarooms/${dataroomId}/documents${folder.path}` + : `/documents/tree${folder.path}`; + return (
  • @@ -31,10 +44,7 @@ export default function FolderCard({ folder, teamInfo }: FolderCardProps) {

    - + {folder.name} diff --git a/components/documents/process-status-bar.tsx b/components/documents/process-status-bar.tsx index faa091b64..84038f6aa 100644 --- a/components/documents/process-status-bar.tsx +++ b/components/documents/process-status-bar.tsx @@ -42,14 +42,14 @@ export default function ProcessStatusBar({ return null; } - const progress = Number(statuses[0].data?.progress) * 100 || 0; - const text = String(statuses[0].data?.text) || ""; + const progress = Number(statuses[0]?.data?.progress) * 100 || 0; + const text = String(statuses[0]?.data?.text) || ""; if (run.status === "FAILURE") { return ( >; onAddition?: (folderName: string) => void; + isDataroom?: boolean; + dataroomId?: string; children?: React.ReactNode; }) { const router = useRouter(); @@ -50,10 +54,12 @@ export function AddFolderModal({ if (folderName == "") return; setLoading(true); + const endpointTargetType = + isDataroom && dataroomId ? `datarooms/${dataroomId}/folders` : "folders"; try { const response = await fetch( - `/api/teams/${teamInfo?.currentTeam?.id}/folders`, + `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}`, { method: "POST", headers: { @@ -78,9 +84,9 @@ export function AddFolderModal({ analytics.capture("Folder Added", { folderName: folderName }); toast.success("Folder added successfully! 🎉"); - mutate(`/api/teams/${teamInfo?.currentTeam?.id}/folders`); + mutate(`/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}`); mutate( - `/api/teams/${teamInfo?.currentTeam?.id}/folders${parentFolderPath}`, + `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}${parentFolderPath}`, ); } catch (error) { setLoading(false); @@ -96,7 +102,7 @@ export function AddFolderModal({ if (plan && plan.plan === "free") { if (children) { return ( - + {children} ); diff --git a/components/links/link-sheet/index.tsx b/components/links/link-sheet/index.tsx index 8b9b6a7ed..77c357187 100644 --- a/components/links/link-sheet/index.tsx +++ b/components/links/link-sheet/index.tsx @@ -21,6 +21,7 @@ import { useTeam } from "@/context/team-context"; import { ScrollArea } from "@/components/ui/scroll-area"; import { LinkOptions } from "./link-options"; import { useAnalytics } from "@/lib/analytics"; +import { LinkWithViews } from "@/lib/types"; export const DEFAULT_LINK_PROPS = { id: null, @@ -65,13 +66,16 @@ export type DEFAULT_LINK_TYPE = { export default function LinkSheet({ isOpen, setIsOpen, + linkType, currentLink, + existingLinks, }: { isOpen: boolean; setIsOpen: Dispatch>; + linkType: "DOCUMENT_LINK" | "DATAROOM_LINK"; currentLink?: DEFAULT_LINK_TYPE; + existingLinks?: LinkWithViews[]; }) { - const { links } = useDocumentLinks(); const { domains } = useDomains(); const teamInfo = useTeam(); const analytics = useAnalytics(); @@ -79,7 +83,7 @@ export default function LinkSheet({ const [isLoading, setIsLoading] = useState(false); const router = useRouter(); - const documentId = router.query.id as string; + const targetId = router.query.id as string; useEffect(() => { setData(currentLink || DEFAULT_LINK_PROPS); @@ -120,7 +124,9 @@ export default function LinkSheet({ body: JSON.stringify({ ...data, metaImage: blobUrl, - documentId: documentId, + targetId: targetId, + linkType: linkType, + teamId: teamInfo?.currentTeam?.id, }), }); @@ -133,15 +139,16 @@ export default function LinkSheet({ } const returnedLink = await response.json(); + const endpointTargetType = `${linkType.replace("_LINK", "").toLowerCase()}s`; // "documents" or "datarooms" if (currentLink) { setIsOpen(false); // Update the link in the list of links mutate( - `/api/teams/${teamInfo?.currentTeam?.id}/documents/${encodeURIComponent( - documentId, + `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent( + targetId, )}/links`, - (links || []).map((link) => + (existingLinks || []).map((link) => link.id === currentLink.id ? returnedLink : link, ), false, @@ -151,16 +158,17 @@ export default function LinkSheet({ setIsOpen(false); // Add the new link to the list of links mutate( - `/api/teams/${teamInfo?.currentTeam?.id}/documents/${encodeURIComponent( - documentId, + `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent( + targetId, )}/links`, - [...(links || []), returnedLink], + [...(existingLinks || []), returnedLink], false, ); analytics.capture("Link Added", { linkId: returnedLink.id, - documentId, + targetId, + linkType, customDomain: returnedLink.domainSlug, }); diff --git a/components/links/links-table.tsx b/components/links/links-table.tsx index a2ceb6ecd..3939e1526 100644 --- a/components/links/links-table.tsx +++ b/components/links/links-table.tsx @@ -41,13 +41,17 @@ import { usePlan } from "@/lib/swr/use-billing"; import { useTeam } from "@/context/team-context"; import ProcessStatusBar from "../documents/process-status-bar"; import { Settings2Icon } from "lucide-react"; +import { DocumentVersion } from "@prisma/client"; export default function LinksTable({ + targetType, + links, primaryVersion, }: { - primaryVersion: any; + targetType: "DOCUMENT" | "DATAROOM"; + links?: LinkWithViews[]; + primaryVersion?: DocumentVersion; }) { - const { links } = useDocumentLinks(); const router = useRouter(); const { plan } = usePlan(); const teamInfo = useTeam(); @@ -93,7 +97,7 @@ export default function LinksTable({ const handleArchiveLink = async ( linkId: string, - documentId: string, + targetId: string, isArchived: boolean, ) => { setIsLoading(true); @@ -113,11 +117,12 @@ export default function LinksTable({ } const archivedLink = await response.json(); + const endpointTargetType = `${targetType.toLowerCase()}s`; // "documents" or "datarooms" // Update the archived link in the list of links mutate( - `/api/teams/${teamInfo?.currentTeam?.id}/documents/${encodeURIComponent( - documentId, + `/api/teams/${teamInfo?.currentTeam?.id}/${endpointTargetType}/${encodeURIComponent( + targetId, )}/links`, (links || []).map((link) => (link.id === linkId ? archivedLink : link)), false, @@ -137,6 +142,8 @@ export default function LinksTable({ const hasFreePlan = plan && plan.plan === "free"; + console.log("links", links); + return ( <>
    @@ -182,7 +189,8 @@ export default function LinksTable({ )} > {/* Progress bar */} - {primaryVersion.type !== "notion" && + {primaryVersion && + primaryVersion.type !== "notion" && !primaryVersion.hasPages ? ( handleArchiveLink( link.id, - link.documentId, + link.documentId ?? link.dataroomId ?? "", link.isArchived, ) } @@ -330,7 +338,9 @@ export default function LinksTable({ {archivedLinksCount > 0 && ( @@ -428,7 +438,9 @@ export default function LinksTable({ onClick={() => handleArchiveLink( link.id, - link.documentId, + link.documentId ?? + link.dataroomId ?? + "", link.isArchived, ) } diff --git a/components/navigation-menu.tsx b/components/navigation-menu.tsx new file mode 100644 index 000000000..e1e54751d --- /dev/null +++ b/components/navigation-menu.tsx @@ -0,0 +1,81 @@ +"use client"; + +import Link from "next/link"; +import * as React from "react"; + +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { useRouter } from "next/router"; + +type Props = { + navigation: { + label: string; + href: string; + segment: string | null; + tag?: string; + }[]; + className?: string; +}; + +export const NavMenu: React.FC> = ({ + navigation, + className, +}) => { + return ( + + ); +}; + +const NavItem: React.FC = ({ + label, + href, + segment, + tag, +}) => { + const router = useRouter(); + const active = segment === router.asPath.split("/").pop(); + + return ( +
  • + + {label} + {tag ? ( +
    + {tag} +
    + ) : null} + +
  • + ); +}; diff --git a/components/view/DataroomViewer.tsx b/components/view/DataroomViewer.tsx new file mode 100644 index 000000000..a58edb0c8 --- /dev/null +++ b/components/view/DataroomViewer.tsx @@ -0,0 +1,126 @@ +import { + Brand, + Dataroom, + DataroomDocument, + DataroomFolder, +} from "@prisma/client"; +import Nav from "./nav"; +import { Button } from "../ui/button"; +import FolderCard from "./dataroom/folder-card"; +import DocumentCard from "./dataroom/document-card"; +import { useState } from "react"; +import { ScrollArea } from "../ui/scroll-area"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, +} from "../ui/breadcrumb"; +import Link from "next/link"; + +type DRDocument = { + dataroomDocumentId: string; + folderId: string | null; + id: string; + name: string; + versions: { + id: string; + versionNumber: number; + hasPages: boolean; + }[]; +}; + +export default function DataroomViewer({ + brand, + viewId, + dataroomViewId, + dataroom, + setViewType, + setDocumentData, +}: { + brand: Brand; + viewId: string; + dataroomViewId: string; + dataroom: any; + setViewType: React.Dispatch>; + setDocumentData: React.Dispatch< + React.SetStateAction<{ + id: string; + name: string; + hasPages: boolean; + documentVersionId: string; + documentVersionNumber: number; + } | null> + >; +}) { + const [folderId, setFolderId] = useState(null); + const { documents, folders } = dataroom as { + documents: DRDocument[]; + folders: DataroomFolder[]; + }; + console.log("documents, ", documents); + return ( + <> +