Skip to content
Merged
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
3 changes: 1 addition & 2 deletions app/api/cron/domains/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ export async function POST(req: Request) {
const domains = await prisma.domain.findMany({
where: {
slug: {
// exclude domains that belong to us
not: {
contains: "papermark.io",
in: ["papermark.io", "papermark.com"],
},
},
},
Expand Down
203 changes: 144 additions & 59 deletions components/links/link-sheet/agreement-panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";

import LinkItem from "../link-item";

Expand All @@ -50,15 +52,21 @@ export default function AgreementSheet({
isOnlyView = false,
onClose,
}: {
defaultData?: { name: string; link: string; requireName: boolean } | null;
defaultData?: { name: string; link: string; requireName: boolean; contentType?: string; textContent?: string } | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strongly type contentType ('LINK' | 'TEXT') to prevent typos.

Use a literal union instead of string for safety across the app.

-  defaultData?: { name: string; link: string; requireName: boolean; contentType?: string; textContent?: string } | null;
+  defaultData?: { name: string; link: string; requireName: boolean; contentType?: "LINK" | "TEXT"; textContent?: string } | null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaultData?: { name: string; link: string; requireName: boolean; contentType?: string; textContent?: string } | null;
defaultData?: {
name: string;
link: string;
requireName: boolean;
contentType?: "LINK" | "TEXT";
textContent?: string;
} | null;
🤖 Prompt for AI Agents
In components/links/link-sheet/agreement-panel/index.tsx around line 55, the
defaultData prop types use contentType?: string which is error-prone; change
that field to a literal union contentType?: 'LINK' | 'TEXT' to enforce allowed
values, update any local interfaces or type aliases accordingly, and then fix
any usages/assignments across the file (or imported callers) to use the 'LINK'
or 'TEXT' literals so the compiler catches typos.

isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
isOnlyView?: boolean;
onClose?: () => void;
}) {
const teamInfo = useTeam();
const teamId = teamInfo?.currentTeam?.id;
const [data, setData] = useState({ name: "", link: "", requireName: true });
const [data, setData] = useState({
name: "",
link: "",
textContent: "",
contentType: "LINK",
requireName: true
});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentFile, setCurrentFile] = useState<File | null>(null);

Expand All @@ -71,14 +79,22 @@ export default function AgreementSheet({
setData({
name: defaultData?.name || "",
link: defaultData?.link || "",
textContent: defaultData?.textContent || "",
contentType: defaultData?.contentType || "LINK",
requireName: defaultData?.requireName || true,
});
Comment on lines 84 to 85
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

Boolean defaulting bug: false gets coerced to true.

|| true overrides an explicit false. Use nullish coalescing.

-        requireName: defaultData?.requireName || true,
+        requireName: defaultData?.requireName ?? true,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requireName: defaultData?.requireName || true,
});
requireName: defaultData?.requireName ?? true,
});
🤖 Prompt for AI Agents
In components/links/link-sheet/agreement-panel/index.tsx around lines 84-85, the
expression "requireName: defaultData?.requireName || true" incorrectly coerces
an explicit false to true; replace the logical-or with nullish coalescing so it
respects false—i.e. use "defaultData?.requireName ?? true" (ensure optional
chaining remains and the result is typed/handled as a boolean).

}
}, [defaultData]);

const handleClose = (open: boolean) => {
setIsOpen(open);
setData({ name: "", link: "", requireName: true });
setData({
name: "",
link: "",
textContent: "",
contentType: "LINK",
requireName: true
});
setCurrentFile(null);
setIsLoading(false);
if (onClose) {
Expand Down Expand Up @@ -184,28 +200,42 @@ export default function AgreementSheet({
return;
}

// Validate URL before submitting
try {
agreementUrlSchema.parse(data.link);
} catch (error) {
if (error instanceof z.ZodError) {
const firstError = error.errors[0];
toast.error(firstError?.message || "Please enter a valid URL");
// Validate based on content type
if (data.contentType === "LINK") {
// Validate URL
try {
agreementUrlSchema.parse(data.link);
} catch (error) {
if (error instanceof z.ZodError) {
const firstError = error.errors[0];
toast.error(firstError?.message || "Please enter a valid URL");
return;
}
}
} else if (data.contentType === "TEXT") {
// Validate text content
if (!data.textContent.trim()) {
toast.error("Please enter agreement text content");
return;
}
}

setIsLoading(true);

try {
const submitData = {
name: data.name,
contentType: data.contentType,
content: data.contentType === "LINK" ? data.link : data.textContent,
requireName: data.requireName,
};

const response = await fetch(`/api/teams/${teamId}/agreements`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...data,
}),
body: JSON.stringify(submitData),
});
Comment on lines 233 to 239
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Guard POST against missing teamId.

Same concern as upload flow.

Add before fetch:

+    if (!teamId) {
+      toast.error("No active team found.");
+      return;
+    }

Run to confirm no other ! assertions remain:


🏁 Script executed:

#!/bin/bash
rg -nP --type tsx -C1 '\bteamId!?\b'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for non-null assertions on teamId in .ts and .tsx files
rg -nP '\bteamId\s*!' -g '*.ts' -g '*.tsx'

Length of output: 4058


Add guard for missing teamId in agreement-panel

In components/links/link-sheet/agreement-panel/index.tsx you still have non-null assertions on teamId at lines 137, 151 and the fetch at 233. Add before each usage:

+  if (!teamId) {
+    toast.error("No active team found.");
+    return;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(`/api/teams/${teamId}/agreements`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...data,
}),
body: JSON.stringify(submitData),
});
if (!teamId) {
toast.error("No active team found.");
return;
}
const response = await fetch(`/api/teams/${teamId}/agreements`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(submitData),
});
🤖 Prompt for AI Agents
In components/links/link-sheet/agreement-panel/index.tsx around lines 137, 151
and 233–239, you are using non-null assertions on teamId and calling fetch with
teamId without guarding for it; add an explicit guard before each usage (e.g.,
if (!teamId) { handle the missing teamId path — return early, disable the
action, or surface an error) so you never call fetch or access teamId when it's
undefined, and update UI/error handling accordingly to avoid runtime exceptions.


if (!response.ok) {
Expand All @@ -220,7 +250,13 @@ export default function AgreementSheet({
} finally {
setIsLoading(false);
setIsOpen(false);
setData({ name: "", link: "", requireName: true });
setData({
name: "",
link: "",
textContent: "",
contentType: "LINK",
requireName: true
});
}
};

Expand Down Expand Up @@ -281,56 +317,102 @@ export default function AgreementSheet({
</div>

<div className="space-y-4">
{/* Content Type Selection */}
<div className="w-full space-y-2">
<Label htmlFor="link">Link to an agreement</Label>
<Input
className={`flex w-full rounded-md border-0 bg-background py-1.5 text-foreground shadow-sm ring-1 ring-inset placeholder:text-muted-foreground focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
!isUrlValid
? "ring-red-500 focus:ring-red-500"
: "ring-input focus:ring-gray-400"
}`}
id="link"
type="text" // Changed from "url" to avoid browser validation conflicts
name="link"
required
autoComplete="off"
data-1p-ignore
placeholder="https://www.papermark.com/nda"
value={data.link || ""}
onChange={(e) => {
const newValue = e.target.value;
setData({
...data,
link: newValue,
});
// Validate on change with debouncing
validateUrl(newValue);
}}
onBlur={(e) => {
// Validate on blur for immediate feedback
validateUrl(e.target.value);
}}
<Label>Agreement Content Type</Label>
<RadioGroup
value={data.contentType}
onValueChange={(value) => setData({...data, contentType: value})}
disabled={isOnlyView}
/>
{/* Display validation error */}
{urlError && (
<p className="mt-1 text-sm text-red-500">{urlError}</p>
)}
className="flex flex-col space-y-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="LINK" id="link-type" />
<Label htmlFor="link-type">Link to agreement document</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="TEXT" id="text-type" />
<Label htmlFor="text-type">Text content</Label>
</div>
</RadioGroup>
</div>

{!isOnlyView ? (
<div className="space-y-12">
<div className="space-y-2 pb-6">
<Label>Or upload an agreement</Label>
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<DocumentUpload
currentFile={currentFile}
setCurrentFile={setCurrentFile}
/>
{/* Link Content */}
{data.contentType === "LINK" && (
<div className="w-full space-y-2">
<Label htmlFor="link">Link to an agreement</Label>
<Input
className={`flex w-full rounded-md border-0 bg-background py-1.5 text-foreground shadow-sm ring-1 ring-inset placeholder:text-muted-foreground focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
!isUrlValid
? "ring-red-500 focus:ring-red-500"
: "ring-input focus:ring-gray-400"
}`}
id="link"
type="text"
name="link"
required={data.contentType === "LINK"}
autoComplete="off"
data-1p-ignore
placeholder="https://www.papermark.com/nda"
value={data.link || ""}
onChange={(e) => {
const newValue = e.target.value;
setData({
...data,
link: newValue,
});
validateUrl(newValue);
}}
onBlur={(e) => {
validateUrl(e.target.value);
}}
disabled={isOnlyView}
/>
{urlError && (
<p className="mt-1 text-sm text-red-500">{urlError}</p>
)}

{!isOnlyView && (
<div className="space-y-12">
<div className="space-y-2 pb-6">
<Label>Or upload an agreement</Label>
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<DocumentUpload
currentFile={currentFile}
setCurrentFile={setCurrentFile}
/>
</div>
</div>
</div>
</div>
)}
</div>
) : null}
)}

{/* Text Content */}
{data.contentType === "TEXT" && (
<div className="w-full space-y-2">
<Label htmlFor="textContent">Agreement Text</Label>
<Textarea
className="flex w-full rounded-md border-0 bg-background py-1.5 text-foreground shadow-sm ring-1 ring-inset ring-input placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-gray-400 sm:text-sm sm:leading-6"
id="textContent"
name="textContent"
required={data.contentType === "TEXT"}
placeholder="By accessing this document, you agree to maintain confidentiality of all information contained herein and not to share, copy, or distribute any content without prior written consent..."
value={data.textContent || ""}
onChange={(e) =>
setData({
...data,
textContent: e.target.value,
})
}
disabled={isOnlyView}
rows={6}
/>
<p className="text-xs text-muted-foreground">
This text will be displayed to users as a compliance agreement before they can access the content.
</p>
</div>
)}
</div>
</div>
<SheetFooter
Expand All @@ -345,7 +427,10 @@ export default function AgreementSheet({
<Button
type="submit"
loading={isLoading}
disabled={!isUrlValid && data.link.trim() !== ""}
disabled={
(data.contentType === "LINK" && !isUrlValid && data.link.trim() !== "") ||
(data.contentType === "TEXT" && !data.textContent.trim())
}
>
Create Agreement
</Button>
Expand Down
42 changes: 26 additions & 16 deletions components/view/access-form/agreement-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,67 @@ import { Dispatch, SetStateAction } from "react";

import { Brand, DataroomBrand } from "@prisma/client";

import { Checkbox } from "@/components/ui/checkbox";

import { determineTextColor } from "@/lib/utils/determine-text-color";

import { Checkbox } from "@/components/ui/checkbox";

import { DEFAULT_ACCESS_FORM_TYPE } from ".";

export default function AgreementSection({
data,
setData,
agreementContent,
agreementName,
agreementContentType,
brand,
useCustomAccessForm,
}: {
data: DEFAULT_ACCESS_FORM_TYPE;
setData: Dispatch<SetStateAction<DEFAULT_ACCESS_FORM_TYPE>>;
agreementContent: string;
agreementName: string;
agreementContentType?: string;
brand?: Partial<Brand> | Partial<DataroomBrand> | null;
useCustomAccessForm?: boolean;
}) {
const handleCheckChange = (checked: boolean) => {
setData((prevData) => ({ ...prevData, hasConfirmedAgreement: checked }));
};

const isTextContent = agreementContentType === "TEXT";

return (
<div className="relative flex items-start space-x-2 pt-5">
<Checkbox
id="agreement"
onCheckedChange={handleCheckChange}
className="mt-0.5 border border-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-300 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-white data-[state=checked]:bg-black data-[state=checked]:text-white"
className="border border-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-300 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-white data-[state=checked]:bg-black data-[state=checked]:text-white"
/>
<label
className="text-sm font-normal leading-5 text-white peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
style={{
color: determineTextColor(brand?.accentColor),
}}
>
I have reviewed and agree to the terms of this{" "}
<a
href={`${agreementContent}`}
target="_blank"
rel="noreferrer noopener"
className="underline hover:text-gray-200"
style={{
color: determineTextColor(brand?.accentColor),
}}
>
{agreementName}
</a>
.
{isTextContent ? (
<span className="whitespace-pre-line">{agreementContent}</span>
) : (
<>
I have reviewed and agree to the terms of this{" "}
<a
href={`${agreementContent}`}
target="_blank"
rel="noreferrer noopener"
className="underline hover:text-gray-200"
style={{
color: determineTextColor(brand?.accentColor),
}}
>
{agreementName}
</a>
.
</>
)}
</label>
</div>
);
Expand Down
Loading