Skip to content

Commit b434596

Browse files
authored
Merge pull request #1847 from mfts/feat/conversations
feat(ee): add publishable q&a conversations
2 parents a0f7925 + adcab06 commit b434596

File tree

30 files changed

+3476
-846
lines changed

30 files changed

+3476
-846
lines changed

app/(ee)/api/faqs/route.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
import { z } from "zod";
4+
5+
import { verifyDataroomSession } from "@/lib/auth/dataroom-auth";
6+
import prisma from "@/lib/prisma";
7+
8+
// Validation schema for query parameters
9+
const visitorFAQParamsSchema = z.object({
10+
linkId: z.string().cuid("Invalid link ID format"),
11+
dataroomId: z.string().cuid("Invalid dataroom ID format"),
12+
documentId: z.string().cuid("Invalid document ID format").nullish(), // This is actually dataroomDocumentId
13+
});
14+
15+
export interface VisitorFAQResponse {
16+
id: string;
17+
editedQuestion: string;
18+
answer: string;
19+
documentPageNumber?: number;
20+
documentVersionNumber?: number;
21+
createdAt: string;
22+
document?: {
23+
name: string;
24+
};
25+
}
26+
27+
// GET /api/faqs?linkId=xxx&dataroomId=xxx - List published FAQs for visitors
28+
export async function GET(req: NextRequest) {
29+
try {
30+
const searchParams = req.nextUrl.searchParams;
31+
32+
// Validate query parameters
33+
const paramValidation = visitorFAQParamsSchema.safeParse({
34+
linkId: searchParams.get("linkId"),
35+
dataroomId: searchParams.get("dataroomId"),
36+
documentId: searchParams.get("documentId"), // This is actually dataroomDocumentId
37+
});
38+
39+
if (!paramValidation.success) {
40+
return NextResponse.json(
41+
{
42+
error: "Invalid parameters",
43+
details: paramValidation.error.errors[0]?.message,
44+
},
45+
{ status: 400 },
46+
);
47+
}
48+
49+
const { linkId, dataroomId, documentId } = paramValidation.data;
50+
51+
// Verify dataroom session
52+
const session = await verifyDataroomSession(req, linkId, dataroomId);
53+
if (!session) {
54+
return NextResponse.json(
55+
{ error: "Unauthorized - invalid or expired session" },
56+
{ status: 401 },
57+
);
58+
}
59+
60+
// Build where clause based on visibility filters
61+
const whereClause: any = {
62+
dataroomId,
63+
status: "PUBLISHED",
64+
};
65+
66+
// Apply visibility filters
67+
const visibilityFilters: any[] = [
68+
{ visibilityMode: "PUBLIC_DATAROOM" }, // Always include dataroom-wide FAQs
69+
];
70+
71+
if (linkId) {
72+
visibilityFilters.push({
73+
visibilityMode: "PUBLIC_LINK",
74+
linkId: linkId,
75+
});
76+
}
77+
78+
if (documentId) {
79+
visibilityFilters.push({
80+
visibilityMode: "PUBLIC_DOCUMENT",
81+
dataroomDocumentId: documentId,
82+
});
83+
}
84+
85+
whereClause.OR = visibilityFilters;
86+
87+
// Fetch published FAQs
88+
const faqs = await prisma.dataroomFaqItem.findMany({
89+
where: whereClause,
90+
select: {
91+
id: true,
92+
editedQuestion: true,
93+
answer: true,
94+
documentPageNumber: true,
95+
documentVersionNumber: true,
96+
createdAt: true,
97+
dataroomDocument: {
98+
select: {
99+
document: {
100+
select: {
101+
name: true,
102+
},
103+
},
104+
},
105+
},
106+
},
107+
orderBy: { createdAt: "desc" },
108+
});
109+
110+
// Format response
111+
const response: VisitorFAQResponse[] = faqs.map((faq: any) => ({
112+
id: faq.id,
113+
editedQuestion: faq.editedQuestion,
114+
answer: faq.answer,
115+
documentPageNumber: faq.documentPageNumber || undefined,
116+
documentVersionNumber: faq.documentVersionNumber || undefined,
117+
createdAt: faq.createdAt.toISOString(),
118+
document: faq.dataroomDocument?.document
119+
? {
120+
name: faq.dataroomDocument.document.name,
121+
}
122+
: undefined,
123+
}));
124+
125+
return NextResponse.json(response);
126+
} catch (error) {
127+
console.error("Error fetching visitor FAQs:", error);
128+
return NextResponse.json(
129+
{ error: "Internal server error" },
130+
{ status: 500 },
131+
);
132+
}
133+
}

components/datarooms/dataroom-navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const DataroomNavigation = ({ dataroomId }: { dataroomId?: string }) => {
2626
segment: "analytics",
2727
},
2828
{
29-
label: "Q&A Conversations",
29+
label: "Q&A",
3030
href: `/datarooms/${dataroomId}/conversations`,
3131
segment: "conversations",
3232
limited: !limits?.conversationsInDataroom,

components/layouts/breadcrumb.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ const SingleDataroomBreadcrumb = ({ path }: { path: string }) => {
165165
return "Permissions";
166166
case "/datarooms/[id]/analytics":
167167
return "Analytics";
168+
case "/datarooms/[id]/conversations/faqs":
169+
return "FAQ";
168170
case "/datarooms/[id]/conversations":
171+
case "/datarooms/[id]/conversations/[conversationId]":
169172
return "Conversations";
170173
case "/datarooms/[id]/settings/notifications":
171174
return "Notifications";

components/ui/badge.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const badgeVariants = cva(
2727
"border-transparent bg-orange-50 text-orange-700 hover:bg-orange-100 dark:bg-orange-900/30 dark:text-orange-400 dark:hover:bg-orange-900/50",
2828
watermark:
2929
"border-transparent bg-indigo-50 text-indigo-700 hover:bg-indigo-100 dark:bg-indigo-900/30 dark:text-indigo-400 dark:hover:bg-indigo-900/50",
30+
notification:
31+
"px-1.5 rounded-sm bg-secondary text-secondary-foreground border-transparent",
3032
},
3133
},
3234
defaultVariants: {

components/view/dataroom/nav-dataroom.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Link from "next/link";
33
import React, { useEffect, useState } from "react";
44

55
import { DataroomBrand } from "@prisma/client";
6-
import { BadgeInfoIcon, Download, MessagesSquareIcon } from "lucide-react";
6+
import { BadgeInfoIcon, Download } from "lucide-react";
77
import { toast } from "sonner";
88

99
import { formatDate } from "@/lib/utils";
@@ -172,22 +172,9 @@ export default function DataroomNav({
172172
</TooltipProvider>
173173
) : null}
174174
{conversationsEnabled && (
175-
<TooltipProvider delayDuration={100}>
176-
<Tooltip>
177-
<TooltipTrigger asChild>
178-
<Button
179-
onClick={() => setShowConversations(!showConversations)}
180-
className="size-8 bg-gray-900 text-white hover:bg-gray-900/80 sm:size-10"
181-
size="icon"
182-
>
183-
<MessagesSquareIcon className="h-5 w-5" />
184-
</Button>
185-
</TooltipTrigger>
186-
<TooltipContent side="bottom">
187-
<p>Toggle conversations</p>
188-
</TooltipContent>
189-
</Tooltip>
190-
</TooltipProvider>
175+
<Button onClick={() => setShowConversations(!showConversations)}>
176+
View FAQ
177+
</Button>
191178
)}
192179
{allowDownload && allowBulkDownload ? (
193180
<ButtonTooltip content="Download Dataroom">

components/view/nav.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -291,22 +291,12 @@ export default function Nav({
291291
)}
292292
{/* Conversation toggle button for dataroom documents */}
293293
{isDataroom && conversationsEnabled && (
294-
<TooltipProvider delayDuration={100}>
295-
<Tooltip>
296-
<TooltipTrigger asChild>
297-
<Button
298-
onClick={() => setShowConversations(!showConversations)}
299-
className="size-8 bg-gray-900 text-white hover:bg-gray-900/80 sm:size-10"
300-
size="icon"
301-
>
302-
<MessageCircle className="h-5 w-5" />
303-
</Button>
304-
</TooltipTrigger>
305-
<TooltipContent side="bottom">
306-
<p>Toggle conversations</p>
307-
</TooltipContent>
308-
</Tooltip>
309-
</TooltipProvider>
294+
<Button
295+
onClick={() => setShowConversations(!showConversations)}
296+
className="bg-gray-900 text-white hover:bg-gray-900/80"
297+
>
298+
View FAQ
299+
</Button>
310300
)}
311301
{embeddedLinks && embeddedLinks.length > 0 ? (
312302
<DropdownMenu>

0 commit comments

Comments
 (0)