Skip to content
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
8 changes: 4 additions & 4 deletions components/datarooms/folders/view-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const FolderComponent = memo(
}: {
folder: DataroomFolderWithDocuments;
folderId: string | null;
setFolderId: React.Dispatch<React.SetStateAction<string | null>>;
setFolderId: (id: string | null) => void;
folderPath: Set<string> | null;
dataroomIndexEnabled?: boolean;
}) => {
Expand Down Expand Up @@ -182,7 +182,7 @@ const HomeLink = memo(
setFolderId,
}: {
folderId: string | null;
setFolderId: React.Dispatch<React.SetStateAction<string | null>>;
setFolderId: (id: string | null) => void;
}) => {
const { usesLightText, palette } = useViewerSurfaceTheme();

Expand Down Expand Up @@ -238,7 +238,7 @@ const SidebarFolders = ({
folders: DataroomFolder[];
documents: DataroomDocumentWithVersion[];
folderId: string | null;
setFolderId: React.Dispatch<React.SetStateAction<string | null>>;
setFolderId: (id: string | null) => void;
dataroomIndexEnabled?: boolean;
}) => {
const { usesLightText, palette } = useViewerSurfaceTheme();
Expand Down Expand Up @@ -303,7 +303,7 @@ export function ViewFolderTree({
}: {
folders: DataroomFolder[];
documents: DataroomDocumentWithVersion[];
setFolderId: React.Dispatch<React.SetStateAction<string | null>>;
setFolderId: (id: string | null) => void;
folderId: string | null;
dataroomIndexEnabled?: boolean;
}) {
Expand Down
94 changes: 65 additions & 29 deletions components/view/dataroom/dataroom-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ export type DEFAULT_DATAROOM_VIEW_TYPE = {
dataroomName?: string;
};

export type PreValidatedSession = {
viewId: string;
viewerEmail?: string;
viewerId?: string;
conversationsEnabled?: boolean;
enableVisitorUpload?: boolean;
isTeamMember?: boolean;
agentsEnabled?: boolean;
dataroomName?: string;
};

export default function DataroomView({
link,
userEmail,
Expand All @@ -64,6 +75,8 @@ export default function DataroomView({
preview,
dataroomIndexEnabled,
textSelectionEnabled,
initialFolderId,
preValidatedSession,
}: {
link: LinkWithDataroom;
userEmail: string | null | undefined;
Expand All @@ -80,6 +93,8 @@ export default function DataroomView({
logoOnAccessForm?: boolean;
dataroomIndexEnabled?: boolean;
textSelectionEnabled?: boolean;
initialFolderId?: string | null;
preValidatedSession?: PreValidatedSession | null;
}) {
useDisablePrint();
const {
Expand All @@ -93,14 +108,28 @@ export default function DataroomView({

const analytics = useAnalytics();
const router = useRouter();
const [folderId, setFolderId] = useState<string | null>(null);
const hasPreValidatedSession = !!preValidatedSession;
const [folderId, setFolderId] = useState<string | null>(
initialFolderId ?? null,
);

const didMount = useRef<boolean>(false);
const [submitted, setSubmitted] = useState<boolean>(false);
const [submitted, setSubmitted] = useState<boolean>(hasPreValidatedSession);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [viewData, setViewData] = useState<DEFAULT_DATAROOM_VIEW_TYPE>({
viewId: "",
});
const [viewData, setViewData] = useState<DEFAULT_DATAROOM_VIEW_TYPE>(
hasPreValidatedSession
? {
viewId: preValidatedSession.viewId,
viewerEmail: preValidatedSession.viewerEmail,
viewerId: preValidatedSession.viewerId,
conversationsEnabled: preValidatedSession.conversationsEnabled,
enableVisitorUpload: preValidatedSession.enableVisitorUpload,
isTeamMember: preValidatedSession.isTeamMember,
agentsEnabled: preValidatedSession.agentsEnabled,
dataroomName: preValidatedSession.dataroomName,
}
: { viewId: "" },
);
const [data, setData] = useState<DEFAULT_ACCESS_FORM_TYPE>(
DEFAULT_ACCESS_FORM_DATA,
);
Expand All @@ -118,8 +147,8 @@ export default function DataroomView({
? brand?.accentColor
: "#ffffff";

const handleSubmission = async (): Promise<void> => {
setIsLoading(true);
const handleSubmission = async (background = false): Promise<void> => {
if (!background) setIsLoading(true);
const response = await fetch("/api/views-dataroom", {
method: "POST",
headers: {
Expand All @@ -144,8 +173,10 @@ export default function DataroomView({
const fetchData = await response.json();

if (fetchData.type === "email-verification") {
setVerificationRequested(true);
setIsLoading(false);
if (!background) {
setVerificationRequested(true);
setIsLoading(false);
}
} else {
const {
viewId,
Expand Down Expand Up @@ -174,14 +205,7 @@ export default function DataroomView({
teamId: link.teamId,
});

// set the verification token to the cookie
if (verificationToken) {
// Cookies.set("pm_vft", verificationToken, {
// path: router.asPath.split("?")[0],
// expires: 1,
// sameSite: "strict",
// secure: true,
// });
setCode(null);
}

Expand All @@ -196,23 +220,27 @@ export default function DataroomView({
agentsEnabled,
dataroomName,
});
setSubmitted(true);
setVerificationRequested(false);
setIsLoading(false);
if (!background) {
setSubmitted(true);
setVerificationRequested(false);
setIsLoading(false);
}
}
} else {
const data = await response.json();
toast.error(data.message);
const responseData = await response.json();
if (!background) {
toast.error(responseData.message);
}

if (data.resetVerification) {
if (responseData.resetVerification) {
const currentPath = router.asPath.split("?")[0];

Cookies.remove("pm_vft", { path: currentPath });
setVerificationToken(null);
setCode(null);
setIsInvalidCode(true);
if (!background) setIsInvalidCode(true);
}
setIsLoading(false);
if (!background) setIsLoading(false);
}
};

Expand All @@ -223,10 +251,18 @@ export default function DataroomView({
await handleSubmission();
};

// If token is present, run handle submit which will verify token and get document
// If link is not submitted and does not have email / password protection, show the access form
// For pre-validated sessions: fire background API call to record the view
// without blocking the UI (content is already visible)
useEffect(() => {
if (hasPreValidatedSession && !didMount.current) {
didMount.current = true;
handleSubmission(true);
}
}, [hasPreValidatedSession]);
Comment on lines +254 to +261
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Background submission may cause redundant API calls and analytics events.

When preValidatedSession is provided, this effect fires handleSubmission(true) which:

  1. Makes a POST to /api/views-dataroom that should be idempotent (session exists → no duplicate view created)
  2. Calls analytics.capture("Link Viewed", ...) (lines 197-206) which may duplicate the view event if already tracked server-side
  3. Overwrites viewData state (lines 212-222) with API response, potentially replacing the already-correct preValidatedSession values

Consider either:

  • Skipping the full submission flow for pre-validated sessions (just call analytics)
  • Or ensuring the API returns early for existing sessions without side effects
🔍 Verification: Check if API handles existing sessions correctly
#!/bin/bash
# Search for how the API handles dataroomSession presence
rg -n -A 10 'dataroomSession' --type=ts -g 'app/api/views-dataroom/*'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/view/dataroom/dataroom-view.tsx` around lines 254 - 261, The
background effect that calls handleSubmission(true) when hasPreValidatedSession
is true can cause duplicate API calls, duplicate analytics events, and overwrite
existing preValidatedSession state; update the useEffect (and surrounding logic
using hasPreValidatedSession, preValidatedSession, and viewData) to
short-circuit for pre-validated sessions: either (A) skip calling
handleSubmission and instead call analytics.capture("Link Viewed", ...) only,
preserving viewData, or (B) make handleSubmission detect a preValidatedSession
and return early (no POST to /api/views-dataroom, no state overwrite) while
still optionally emitting analytics; ensure the change references useEffect,
handleSubmission, preValidatedSession, analytics.capture, and viewData so the
API call and state update are avoided for already-validated sessions.


// Normal flow: verify token or auto-submit when not protected
useEffect(() => {
if (!didMount.current) {
if (!hasPreValidatedSession && !didMount.current) {
if ((!submitted && !isProtected) || token || preview || previewToken) {
handleSubmission();
didMount.current = true;
Expand All @@ -250,8 +286,8 @@ export default function DataroomView({
);
}

// If link is not submitted and does not have email / password protection, show the access form
if (!submitted && isProtected) {
// Show access form if not submitted, not pre-validated, and link is protected
if (!submitted && !hasPreValidatedSession && isProtected) {
return (
<AccessForm
data={data}
Expand Down
77 changes: 67 additions & 10 deletions components/view/viewer/dataroom-viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRouter } from "next/router";

import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import React from "react";

import { ViewerChatPanel } from "@/ee/features/ai/components/viewer-chat-panel";
Expand Down Expand Up @@ -62,12 +62,12 @@ import {

const ViewerBreadcrumbItem = ({
folder,
setFolderId,
onNavigate,
isLast,
dataroomIndexEnabled,
}: {
folder: any;
setFolderId: (id: string | null) => void;
onNavigate: (id: string | null) => void;
isLast: boolean;
dataroomIndexEnabled?: boolean;
}) => {
Expand All @@ -90,7 +90,7 @@ const ViewerBreadcrumbItem = ({

return (
<BreadcrumbLink
onClick={() => setFolderId(folder.id)}
onClick={() => onNavigate(folder.id)}
className="cursor-pointer capitalize text-[var(--viewer-muted-text)] hover:text-[var(--viewer-text)]"
style={HIERARCHICAL_DISPLAY_STYLE}
>
Expand Down Expand Up @@ -184,6 +184,63 @@ export default function DataroomViewer({
const router = useRouter();
const searchQuery = (router.query.search as string)?.toLowerCase() || "";

const { domain, slug, previewToken: queryPreviewToken } = router.query as {
domain?: string;
slug?: string;
previewToken?: string;
};

const buildFolderUrl = useCallback(
(targetFolderId: string | null) => {
const queryString = queryPreviewToken
? `?previewToken=${queryPreviewToken}&preview=1`
: "";

if (domain && slug) {
return targetFolderId
? `/${slug}/f/${targetFolderId}${queryString}`
: `/${slug}${queryString}`;
}
return targetFolderId
? `/view/${linkId}/f/${targetFolderId}${queryString}`
: `/view/${linkId}${queryString}`;
},
[linkId, domain, slug, queryPreviewToken],
);

const handleFolderChange = useCallback(
(newFolderId: string | null) => {
setFolderId(newFolderId);
window.history.pushState(
{ folderId: newFolderId },
"",
buildFolderUrl(newFolderId),
);
},
[setFolderId, buildFolderUrl],
);

useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
const stateFolderId = event.state?.folderId ?? null;
setFolderId(stateFolderId);
};

window.addEventListener("popstate", handlePopState);
return () => window.removeEventListener("popstate", handlePopState);
}, [setFolderId]);

// Replace the initial history entry so back-button state is consistent
useEffect(() => {
if (folderId) {
window.history.replaceState(
{ folderId },
"",
buildFolderUrl(folderId),
);
}
}, []);

// Tab state: "documents" (normal view) or "my-uploads" (visitor's uploads)
const [activeTab, setActiveTab] = useState<"documents" | "my-uploads">(
"documents",
Expand Down Expand Up @@ -426,7 +483,7 @@ export default function DataroomViewer({
key={item.id}
folder={item}
dataroomId={dataroom?.id}
setFolderId={setFolderId}
setFolderId={handleFolderChange}
isPreview={!!isPreview}
linkId={linkId}
viewId={viewId}
Expand Down Expand Up @@ -534,7 +591,7 @@ export default function DataroomViewer({
<ViewFolderTree
folders={folders}
documents={documents}
setFolderId={setFolderId}
setFolderId={handleFolderChange}
folderId={folderId}
dataroomIndexEnabled={dataroomIndexEnabled}
/>
Expand Down Expand Up @@ -580,7 +637,7 @@ export default function DataroomViewer({
<ViewFolderTree
folders={folders}
documents={documents}
setFolderId={setFolderId}
setFolderId={handleFolderChange}
folderId={folderId}
dataroomIndexEnabled={dataroomIndexEnabled}
/>
Expand All @@ -600,7 +657,7 @@ export default function DataroomViewer({
<BreadcrumbList className="text-[var(--viewer-muted-text)]">
<BreadcrumbItem key={"root"}>
<BreadcrumbLink
onClick={() => setFolderId(null)}
onClick={() => handleFolderChange(null)}
className="cursor-pointer text-[var(--viewer-muted-text)] hover:text-[var(--viewer-text)]"
>
Home
Expand All @@ -615,7 +672,7 @@ export default function DataroomViewer({
<BreadcrumbItem>
<ViewerBreadcrumbItem
folder={folder}
setFolderId={setFolderId}
onNavigate={handleFolderChange}
isLast={index === breadcrumbFolders.length - 1}
dataroomIndexEnabled={dataroomIndexEnabled}
/>
Expand Down Expand Up @@ -735,7 +792,7 @@ export default function DataroomViewer({
linkId={linkId}
showFolderPath
onNavigateToFolder={(id) => {
setFolderId(id);
handleFolderChange(id);
setActiveTab("documents");
}}
/>
Expand Down
Loading