Merge branch 'main' into reattach-pdf

This commit is contained in:
Prajwal Kulkarni
2024-02-09 20:51:15 +05:30
committed by GitHub
21 changed files with 177 additions and 84 deletions

View File

@@ -151,7 +151,7 @@ export const EditDocumentForm = ({
}; };
const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => {
const { subject, message, timezone, dateFormat } = data.meta; const { subject, message, timezone, dateFormat, redirectUrl } = data.meta;
try { try {
await sendDocument({ await sendDocument({
@@ -159,8 +159,9 @@ export const EditDocumentForm = ({
meta: { meta: {
subject, subject,
message, message,
timezone,
dateFormat, dateFormat,
timezone,
redirectUrl,
}, },
}); });

View File

@@ -114,7 +114,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
<DropdownMenuContent className="w-52" align="start" forceMount> <DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel> <DropdownMenuLabel>Action</DropdownMenuLabel>
{recipient?.role !== RecipientRole.CC && ( {recipient && recipient?.role !== RecipientRole.CC && (
<DropdownMenuItem disabled={!recipient || isComplete} asChild> <DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}> <Link href={`/sign/${recipient?.token}`}>
{recipient?.role === RecipientRole.VIEWER && ( {recipient?.role === RecipientRole.VIEWER && (

View File

@@ -26,9 +26,10 @@ export type SigningFormProps = {
document: Document; document: Document;
recipient: Recipient; recipient: Recipient;
fields: Field[]; fields: Field[];
redirectUrl?: string | null;
}; };
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
const router = useRouter(); const router = useRouter();
const analytics = useAnalytics(); const analytics = useAnalytics();
const { data: session } = useSession(); const { data: session } = useSession();
@@ -74,7 +75,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
router.push(`/sign/${recipient.token}/complete`); redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
}; };
return ( return (

View File

@@ -118,7 +118,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => {
<span className="text-muted-foreground">({recipient.email})</span> <span className="text-muted-foreground">({recipient.email})</span>
</DialogTitle> </DialogTitle>
<div className="py-4"> <div>
<Label htmlFor="signature">Full Name</Label> <Label htmlFor="signature">Full Name</Label>
<Input <Input

View File

@@ -8,7 +8,6 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
@@ -49,15 +48,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
viewedDocument({ token }).catch(() => null), viewedDocument({ token }).catch(() => null),
]); ]);
const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null);
if (!document || !document.documentData || !recipient) { if (!document || !document.documentData || !recipient) {
return notFound(); return notFound();
} }
const truncatedTitle = truncateTitle(document.title); const truncatedTitle = truncateTitle(document.title);
const { documentData } = document; const { documentData, documentMeta } = document;
const { user } = await getServerComponentSession(); const { user } = await getServerComponentSession();
@@ -65,7 +62,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
document.status === DocumentStatus.COMPLETED || document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED recipient.signingStatus === SigningStatus.SIGNED
) { ) {
redirect(`/sign/${token}/complete`); documentMeta?.redirectUrl
? redirect(documentMeta.redirectUrl)
: redirect(`/sign/${token}/complete`);
} }
if (documentMeta?.password) { if (documentMeta?.password) {
@@ -133,7 +132,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
</Card> </Card>
<div className="col-span-12 lg:col-span-5 xl:col-span-4"> <div className="col-span-12 lg:col-span-5 xl:col-span-4">
<SigningForm document={document} recipient={recipient} fields={fields} /> <SigningForm
document={document}
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
/>
</div> </div>
</div> </div>

13
package-lock.json generated
View File

@@ -14503,6 +14503,7 @@
"version": "6.9.7", "version": "6.9.7",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
"integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==", "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
"peer": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
@@ -19495,14 +19496,14 @@
"@react-email/section": "0.0.10", "@react-email/section": "0.0.10",
"@react-email/tailwind": "0.0.9", "@react-email/tailwind": "0.0.9",
"@react-email/text": "0.0.6", "@react-email/text": "0.0.6",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.9",
"react-email": "^1.9.5", "react-email": "^1.9.5",
"resend": "^2.0.0" "resend": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*", "@documenso/tsconfig": "*",
"@types/nodemailer": "^6.4.8", "@types/nodemailer": "^6.4.14",
"tsup": "^7.1.0" "tsup": "^7.1.0"
} }
}, },
@@ -19520,6 +19521,14 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"packages/email/node_modules/nodemailer": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz",
"integrity": "sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==",
"engines": {
"node": ">=6.0.0"
}
},
"packages/eslint-config": { "packages/eslint-config": {
"name": "@documenso/eslint-config", "name": "@documenso/eslint-config",
"version": "0.0.0", "version": "0.0.0",

View File

@@ -35,14 +35,14 @@
"@react-email/section": "0.0.10", "@react-email/section": "0.0.10",
"@react-email/tailwind": "0.0.9", "@react-email/tailwind": "0.0.9",
"@react-email/text": "0.0.6", "@react-email/text": "0.0.6",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.9",
"react-email": "^1.9.5", "react-email": "^1.9.5",
"resend": "^2.0.0" "resend": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",
"@documenso/tsconfig": "*", "@documenso/tsconfig": "*",
"@types/nodemailer": "^6.4.8", "@types/nodemailer": "^6.4.14",
"tsup": "^7.1.0" "tsup": "^7.1.0"
} }
} }

View File

@@ -0,0 +1,2 @@
export const URL_REGEX =
/^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i;

View File

@@ -9,6 +9,7 @@ export type CreateDocumentMetaOptions = {
timezone?: string; timezone?: string;
password?: string; password?: string;
dateFormat?: string; dateFormat?: string;
redirectUrl?: string;
userId: number; userId: number;
}; };
@@ -20,6 +21,7 @@ export const upsertDocumentMeta = async ({
documentId, documentId,
userId, userId,
password, password,
redirectUrl,
}: CreateDocumentMetaOptions) => { }: CreateDocumentMetaOptions) => {
await prisma.document.findFirstOrThrow({ await prisma.document.findFirstOrThrow({
where: { where: {
@@ -48,17 +50,19 @@ export const upsertDocumentMeta = async ({
create: { create: {
subject, subject,
message, message,
password,
dateFormat, dateFormat,
timezone, timezone,
password,
documentId, documentId,
redirectUrl,
}, },
update: { update: {
subject, subject,
message, message,
dateFormat,
password, password,
dateFormat,
timezone, timezone,
redirectUrl,
}, },
}); });
}; };

View File

@@ -39,6 +39,7 @@ export const duplicateDocumentById = async ({
dateFormat: true, dateFormat: true,
password: true, password: true,
timezone: true, timezone: true,
redirectUrl: true,
}, },
}, },
}, },

View File

@@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({
include: { include: {
User: true, User: true,
documentData: true, documentData: true,
documentMeta: true,
}, },
}); });

View File

@@ -46,6 +46,10 @@ export const setFieldsForDocument = async ({
throw new Error('Document not found'); throw new Error('Document not found');
} }
if (document.completedAt) {
throw new Error('Document already complete');
}
const existingFields = await prisma.field.findMany({ const existingFields = await prisma.field.findMany({
where: { where: {
documentId, documentId,

View File

@@ -44,6 +44,10 @@ export const setRecipientsForDocument = async ({
throw new Error('Document not found'); throw new Error('Document not found');
} }
if (document.completedAt) {
throw new Error('Document already complete');
}
const normalizedRecipients = recipients.map((recipient) => ({ const normalizedRecipients = recipients.map((recipient) => ({
...recipient, ...recipient,
email: recipient.email.toLowerCase(), email: recipient.email.toLowerCase(),
@@ -77,8 +81,9 @@ export const setRecipientsForDocument = async ({
}) })
.filter((recipient) => { .filter((recipient) => {
return ( return (
recipient._persisted?.sendStatus !== SendStatus.SENT && recipient._persisted?.role === RecipientRole.CC ||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED (recipient._persisted?.sendStatus !== SendStatus.SENT &&
recipient._persisted?.signingStatus !== SigningStatus.SIGNED)
); );
}); });
@@ -96,6 +101,7 @@ export const setRecipientsForDocument = async ({
email: recipient.email, email: recipient.email,
role: recipient.role, role: recipient.role,
documentId, documentId,
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus: signingStatus:
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED, recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
}, },

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "redirectUrl" TEXT;

View File

@@ -199,6 +199,7 @@ model DocumentMeta {
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String?
} }
enum ReadStatus { enum ReadStatus {

View File

@@ -215,13 +215,14 @@ export const documentRouter = router({
try { try {
const { documentId, meta } = input; const { documentId, meta } = input;
if (meta.message || meta.subject || meta.timezone || meta.dateFormat) { if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) {
await upsertDocumentMeta({ await upsertDocumentMeta({
documentId, documentId,
subject: meta.subject, subject: meta.subject,
message: meta.message, message: meta.message,
dateFormat: meta.dateFormat, dateFormat: meta.dateFormat,
timezone: meta.timezone, timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
userId: ctx.user.id, userId: ctx.user.id,
}); });
} }

View File

@@ -1,5 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client'; import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
export const ZGetDocumentByIdQuerySchema = z.object({ export const ZGetDocumentByIdQuerySchema = z.object({
@@ -73,6 +74,12 @@ export const ZSendDocumentMutationSchema = z.object({
message: z.string(), message: z.string(),
timezone: z.string(), timezone: z.string(),
dateFormat: z.string(), dateFormat: z.string(),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || URL_REGEX.test(value), {
message: 'Please enter a valid URL',
}),
}), }),
}); });

View File

@@ -307,6 +307,13 @@ export const AddFieldsFormPartial = ({
return recipientsByRole; return recipientsByRole;
}, [recipients]); }, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
);
}, [recipientsByRole]);
return ( return (
<> <>
<DocumentFlowFormContainerHeader <DocumentFlowFormContainerHeader
@@ -385,13 +392,10 @@ export const AddFieldsFormPartial = ({
</span> </span>
</CommandEmpty> </CommandEmpty>
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => ( {recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
<CommandGroup key={roleIndex}> <CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium"> <div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{ {`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
}
</div> </div>
{recipients.length === 0 && ( {recipients.length === 0 && (
@@ -406,7 +410,7 @@ export const AddFieldsFormPartial = ({
{recipients.map((recipient) => ( {recipients.map((recipient) => (
<CommandItem <CommandItem
key={recipient.id} key={recipient.id}
className={cn('px-4 last:mb-1 [&:not(:first-child)]:mt-1', { className={cn('px-2 last:mb-1 [&:not(:first-child)]:mt-1', {
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT, 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
})} })}
onSelect={() => { onSelect={() => {
@@ -416,7 +420,7 @@ export const AddFieldsFormPartial = ({
> >
<span <span
className={cn('text-foreground/70 truncate', { className={cn('text-foreground/70 truncate', {
'text-foreground': recipient === selectedSigner, 'text-foreground/80': recipient === selectedSigner,
})} })}
> >
{recipient.name && ( {recipient.name && (

View File

@@ -105,7 +105,10 @@ export const AddSignersFormPartial = ({
} }
return recipients.some( return recipients.some(
(recipient) => recipient.id === id && recipient.sendStatus === SendStatus.SENT, (recipient) =>
recipient.id === id &&
recipient.sendStatus === SendStatus.SENT &&
recipient.role !== RecipientRole.CC,
); );
}; };

View File

@@ -2,6 +2,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Info } from 'lucide-react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@@ -23,6 +25,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@documenso/ui/primitives/select'; } from '@documenso/ui/primitives/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { Combobox } from '../combobox'; import { Combobox } from '../combobox';
import { FormErrorMessage } from '../form/form-error-message'; import { FormErrorMessage } from '../form/form-error-message';
@@ -30,7 +33,7 @@ import { Input } from '../input';
import { Label } from '../label'; import { Label } from '../label';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import { Textarea } from '../textarea'; import { Textarea } from '../textarea';
import type { TAddSubjectFormSchema } from './add-subject.types'; import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
@@ -69,8 +72,10 @@ export const AddSubjectFormPartial = ({
message: document.documentMeta?.message ?? '', message: document.documentMeta?.message ?? '',
timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
redirectUrl: document.documentMeta?.redirectUrl ?? '',
}, },
}, },
resolver: zodResolver(ZAddSubjectFormSchema),
}); });
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = handleSubmit(onSubmit);
@@ -163,64 +168,94 @@ export const AddSubjectFormPartial = ({
</ul> </ul>
</div> </div>
{hasDateField && ( <Accordion type="multiple" className="mt-8 border-none">
<Accordion type="multiple" className="mt-8 border-none"> <AccordionItem value="advanced-options" className="border-none">
<AccordionItem value="advanced-options" className="border-none"> <AccordionTrigger className="mb-2 border-b text-left hover:no-underline">
<AccordionTrigger className="mb-2 border-b text-left hover:no-underline"> Advanced Options
Advanced Options </AccordionTrigger>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 text-sm leading-relaxed"> <AccordionContent className="text-muted-foreground -mx-1 flex max-w-prose flex-col px-1 pt-2 text-sm leading-relaxed">
<div className="mt-2 flex flex-col"> {hasDateField && (
<Label htmlFor="date-format"> <>
Date Format <span className="text-muted-foreground">(Optional)</span> <div className="flex flex-col">
</Label> <Label htmlFor="date-format">
Date Format <span className="text-muted-foreground">(Optional)</span>
</Label>
<Controller <Controller
control={control} control={control}
name={`meta.dateFormat`} name={`meta.dateFormat`}
disabled={documentHasBeenSent} disabled={documentHasBeenSent}
render={({ field: { value, onChange, disabled } }) => ( render={({ field: { value, onChange, disabled } }) => (
<Select value={value} onValueChange={onChange} disabled={disabled}> <Select value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger className="bg-background mt-2"> <SelectTrigger className="bg-background mt-2">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{DATE_FORMATS.map((format) => ( {DATE_FORMATS.map((format) => (
<SelectItem key={format.key} value={format.value}> <SelectItem key={format.key} value={format.value}>
{format.label} {format.label}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
)} )}
/> />
</div>
<div className="mt-4 flex flex-col">
<Label htmlFor="time-zone">
Time Zone <span className="text-muted-foreground">(Optional)</span>
</Label>
<Controller
control={control}
name={`meta.timezone`}
render={({ field: { value, onChange } }) => (
<Combobox
className="bg-background"
options={TIME_ZONES}
value={value}
onChange={(value) => value && onChange(value)}
disabled={documentHasBeenSent}
/>
)}
/>
</div>
</>
)}
<div className="flex flex-col">
<div className="flex flex-col gap-y-4">
<div>
<Label htmlFor="redirectUrl" className="flex items-center">
Redirect URL{' '}
<Tooltip>
<TooltipTrigger>
<Info className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add a URL to redirect the user to once the document is signed
</TooltipContent>
</Tooltip>
</Label>
<Input
id="redirectUrl"
type="url"
className="bg-background my-2"
{...register('meta.redirectUrl')}
/>
<FormErrorMessage className="mt-2" error={errors.meta?.redirectUrl} />
</div>
</div> </div>
</div>
<div className="mt-4 flex flex-col"> </AccordionContent>
<Label htmlFor="time-zone"> </AccordionItem>
Time Zone <span className="text-muted-foreground">(Optional)</span> </Accordion>
</Label>
<Controller
control={control}
name={`meta.timezone`}
render={({ field: { value, onChange } }) => (
<Combobox
className="bg-background"
options={TIME_ZONES}
value={value}
onChange={(value) => value && onChange(value)}
disabled={documentHasBeenSent}
/>
)}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)}
</div> </div>
</div> </div>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>

View File

@@ -2,6 +2,7 @@ import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
export const ZAddSubjectFormSchema = z.object({ export const ZAddSubjectFormSchema = z.object({
meta: z.object({ meta: z.object({
@@ -9,6 +10,12 @@ export const ZAddSubjectFormSchema = z.object({
message: z.string(), message: z.string(),
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE), timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT), dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || URL_REGEX.test(value), {
message: 'Please enter a valid URL',
}),
}), }),
}); });