Compare commits

...

11 Commits

8 changed files with 382 additions and 248 deletions

View File

@ -1,14 +1,16 @@
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Plus } from 'lucide-react'; import { InfoIcon, Plus } from 'lucide-react';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import * as z from 'zod'; import * as z from 'zod';
import { TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX } from '@documenso/lib/constants/template';
import { AppError } from '@documenso/lib/errors/app-error';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@ -19,24 +21,59 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons'; import type { Toast } from '@documenso/ui/primitives/use-toast';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
const ZAddRecipientsForNewDocumentSchema = z.object({ const ZAddRecipientsForNewDocumentSchema = z
.object({
sendDocument: z.boolean(),
recipients: z.array( recipients: z.array(
z.object({ z.object({
id: z.number(),
email: z.string().email(), email: z.string().email(),
name: z.string(), name: z.string(),
role: z.nativeEnum(RecipientRole),
}), }),
), ),
}); })
// Display exactly which rows are duplicates.
.superRefine((items, ctx) => {
const uniqueEmails = new Map<string, number>();
for (const [index, recipients] of items.recipients.entries()) {
const email = recipients.email.toLowerCase();
const firstFoundIndex = uniqueEmails.get(email);
if (firstFoundIndex === undefined) {
uniqueEmails.set(email, index);
continue;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Emails must be unique',
path: ['recipients', index, 'email'],
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Emails must be unique',
path: ['recipients', firstFoundIndex, 'email'],
});
}
});
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>; type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
@ -56,33 +93,31 @@ export function UseTemplateDialog({
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const { const form = useForm<TAddRecipientsForNewDocumentSchema>({
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TAddRecipientsForNewDocumentSchema>({
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
defaultValues: { defaultValues: {
recipients: sendDocument: false,
recipients.length > 0 recipients: recipients.map((recipient) => {
? recipients.map((recipient) => ({ const isRecipientPlaceholder = recipient.email.match(TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX);
nativeId: recipient.id,
formId: String(recipient.id), if (isRecipientPlaceholder) {
name: recipient.name, return {
email: recipient.email, id: recipient.id,
role: recipient.role,
}))
: [
{
name: '', name: '',
email: '', email: '',
role: RecipientRole.SIGNER, };
}, }
],
return {
id: recipient.id,
name: recipient.name,
email: recipient.email,
};
}),
}, },
}); });
const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } = const { mutateAsync: createDocumentFromTemplate } =
trpc.template.createDocumentFromTemplate.useMutation(); trpc.template.createDocumentFromTemplate.useMutation();
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
@ -91,6 +126,7 @@ export function UseTemplateDialog({
templateId, templateId,
teamId: team?.id, teamId: team?.id,
recipients: data.recipients, recipients: data.recipients,
sendDocument: data.sendDocument,
}); });
toast({ toast({
@ -101,18 +137,24 @@ export function UseTemplateDialog({
router.push(`${documentRootPath}/${id}`); router.push(`${documentRootPath}/${id}`);
} catch (err) { } catch (err) {
toast({ const error = AppError.parseError(err);
const toastPayload: Toast = {
title: 'Error', title: 'Error',
description: 'An error occurred while creating document from template.', description: 'An error occurred while creating document from template.',
variant: 'destructive', variant: 'destructive',
}); };
if (error.code === 'DOCUMENT_SEND_FAILED') {
toastPayload.description = 'The document was created but could not be sent to recipients.';
}
toast(toastPayload);
} }
}; };
const onCreateDocumentFromTemplate = handleSubmit(onSubmit);
const { fields: formRecipients } = useFieldArray({ const { fields: formRecipients } = useFieldArray({
control, control: form.control,
name: 'recipients', name: 'recipients',
}); });
@ -126,121 +168,110 @@ export function UseTemplateDialog({
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Document Recipients</DialogTitle> <DialogTitle>Create document from template</DialogTitle>
<DialogDescription>Add the recipients to create the template with.</DialogDescription> <DialogDescription>
{recipients.length === 0
? 'A draft document will be created'
: 'Add the recipients to create the document with'}
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col space-y-4">
{formRecipients.map((recipient, index) => (
<div
key={recipient.id}
data-native-id={recipient.id}
className="flex flex-wrap items-end gap-x-4"
>
<div className="flex-1">
<Label htmlFor={`recipient-${recipient.id}-email`}>
Email
<span className="text-destructive ml-1 inline-block font-medium">*</span>
</Label>
<Controller <Form {...form}>
control={control} <form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
<div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1">
{formRecipients.map((recipient, index) => (
<div className="flex w-full flex-row space-x-4" key={recipient.id}>
<FormField
control={form.control}
name={`recipients.${index}.email`} name={`recipients.${index}.email`}
render={({ field }) => ( render={({ field }) => (
<Input <FormItem className="w-full">
id={`recipient-${recipient.id}-email`} {index === 0 && <FormLabel required>Email</FormLabel>}
type="email"
className="bg-background mt-2" <FormControl>
disabled={isSubmitting} <Input {...field} placeholder={recipients[index].email} />
{...field} </FormControl>
/> <FormMessage />
</FormItem>
)} )}
/> />
</div>
<div className="flex-1"> <FormField
<Label htmlFor={`recipient-${recipient.id}-name`}>Name</Label> control={form.control}
<Controller
control={control}
name={`recipients.${index}.name`} name={`recipients.${index}.name`}
render={({ field }) => ( render={({ field }) => (
<Input <FormItem className="w-full">
id={`recipient-${recipient.id}-name`} {index === 0 && <FormLabel>Name</FormLabel>}
type="text"
className="bg-background mt-2" <FormControl>
disabled={isSubmitting} <Input {...field} placeholder={recipients[index].name} />
{...field} </FormControl>
/> <FormMessage />
</FormItem>
)} )}
/> />
</div> </div>
<div className="w-[60px]">
<Controller
control={control}
name={`recipients.${index}.role`}
render={({ field: { value, onChange } }) => (
<Select value={value} onValueChange={(x) => onChange(x)}>
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
<SelectContent className="" align="end">
<SelectItem value={RecipientRole.SIGNER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
Signer
</div>
</SelectItem>
<SelectItem value={RecipientRole.CC}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
Receives copy
</div>
</SelectItem>
<SelectItem value={RecipientRole.APPROVER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
Approver
</div>
</SelectItem>
<SelectItem value={RecipientRole.VIEWER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
Viewer
</div>
</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="w-full">
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.email} />
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.name} />
</div>
</div>
))} ))}
</div> </div>
<DialogFooter className="justify-end"> {recipients.length > 0 && (
<div className="mt-4 flex flex-row items-center">
<FormField
control={form.control}
name="sendDocument"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center">
<Checkbox
id="sendDocument"
className="h-5 w-5"
checkClassName="dark:text-white text-primary"
checked={field.value}
onCheckedChange={field.onChange}
/>
<label
className="text-muted-foreground ml-2 flex items-center text-sm"
htmlFor="sendDocument"
>
Send document
<Tooltip>
<TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<p>
The document will be immediately sent to recipients if this is
checked.
</p>
<p>Otherwise, the document will be created as a draft.</p>
</TooltipContent>
</Tooltip>
</label>
</div>
</FormItem>
)}
/>
</div>
)}
<DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button type="button" variant="secondary"> <Button type="button" variant="secondary">
Close Close
</Button> </Button>
</DialogClose> </DialogClose>
<Button <Button type="submit" loading={form.formState.isSubmitting}>
type="button" {form.getValues('sendDocument') ? 'Create and send' : 'Create as draft'}
loading={isCreatingDocumentFromTemplate}
disabled={isCreatingDocumentFromTemplate}
onClick={onCreateDocumentFromTemplate}
>
Create Document
</Button> </Button>
</DialogFooter> </DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -100,13 +100,21 @@ export type TCreateDocumentMutationResponseSchema = z.infer<
export const ZCreateDocumentFromTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
title: z.string().min(1), title: z.string().min(1),
recipients: z.array( recipients: z.union([
z.array(
z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email().min(1),
}),
),
z.array(
z.object({ z.object({
name: z.string().min(1), name: z.string().min(1),
email: z.string().email().min(1), email: z.string().email().min(1),
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER),
}), }),
), ),
]),
meta: z meta: z
.object({ .object({
subject: z.string(), subject: z.string(),

View File

@ -189,7 +189,14 @@ test('[TEMPLATES]: use template', async ({ page }) => {
// Use personal template. // Use personal template.
await page.getByRole('button', { name: 'Use Template' }).click(); await page.getByRole('button', { name: 'Use Template' }).click();
await page.getByRole('button', { name: 'Create Document' }).click();
// Enter template values.
await page.getByPlaceholder('recipient.1@documenso.com').click();
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
await page.getByPlaceholder('Recipient 1').click();
await page.getByPlaceholder('Recipient 1').fill('name');
await page.getByRole('button', { name: 'Create as draft' }).click();
await page.waitForURL(/documents/); await page.waitForURL(/documents/);
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
await page.waitForURL('/documents'); await page.waitForURL('/documents');
@ -200,7 +207,14 @@ test('[TEMPLATES]: use template', async ({ page }) => {
// Use team template. // Use team template.
await page.getByRole('button', { name: 'Use Template' }).click(); await page.getByRole('button', { name: 'Use Template' }).click();
await page.getByRole('button', { name: 'Create Document' }).click();
// Enter template values.
await page.getByPlaceholder('recipient.1@documenso.com').click();
await page.getByPlaceholder('recipient.1@documenso.com').fill(teamMemberUser.email);
await page.getByPlaceholder('Recipient 1').click();
await page.getByPlaceholder('Recipient 1').fill('name');
await page.getByRole('button', { name: 'Create as draft' }).click();
await page.waitForURL(/\/t\/.+\/documents/); await page.waitForURL(/\/t\/.+\/documents/);
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
await page.waitForURL(`/t/${team.url}/documents`); await page.waitForURL(`/t/${team.url}/documents`);

View File

@ -0,0 +1 @@
export const TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i;

View File

@ -1,16 +1,35 @@
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { RecipientRole } from '@documenso/prisma/client'; import type { Field } from '@documenso/prisma/client';
import { type Recipient, WebhookTriggerEvents } from '@documenso/prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
type RecipientWithId = {
id: number;
name?: string;
email: string;
};
type FinalRecipient = Pick<Recipient, 'name' | 'email' | 'role'> & {
templateRecipientId: number;
fields: Field[];
};
export type CreateDocumentFromTemplateOptions = { export type CreateDocumentFromTemplateOptions = {
templateId: number; templateId: number;
userId: number; userId: number;
teamId?: number; teamId?: number;
recipients?: { recipients:
| RecipientWithId[]
| {
name?: string; name?: string;
email: string; email: string;
role?: RecipientRole;
}[]; }[];
requestMetadata?: RequestMetadata;
}; };
export const createDocumentFromTemplate = async ({ export const createDocumentFromTemplate = async ({
@ -18,7 +37,14 @@ export const createDocumentFromTemplate = async ({
userId, userId,
teamId, teamId,
recipients, recipients,
requestMetadata,
}: CreateDocumentFromTemplateOptions) => { }: CreateDocumentFromTemplateOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const template = await prisma.template.findUnique({ const template = await prisma.template.findUnique({
where: { where: {
id: templateId, id: templateId,
@ -39,8 +65,11 @@ export const createDocumentFromTemplate = async ({
}), }),
}, },
include: { include: {
Recipient: true, Recipient: {
include: {
Field: true, Field: true,
},
},
templateDocumentData: true, templateDocumentData: true,
}, },
}); });
@ -49,6 +78,43 @@ export const createDocumentFromTemplate = async ({
throw new Error('Template not found.'); throw new Error('Template not found.');
} }
if (recipients.length !== template.Recipient.length) {
throw new Error('Invalid number of recipients.');
}
let finalRecipients: FinalRecipient[] = [];
if (recipients.length > 0 && Object.prototype.hasOwnProperty.call(recipients[0], 'id')) {
finalRecipients = template.Recipient.map((templateRecipient) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const foundRecipient = (recipients as RecipientWithId[]).find(
(recipient) => recipient.id === templateRecipient.id,
);
if (!foundRecipient) {
throw new Error('Recipient not found.');
}
return {
templateRecipientId: templateRecipient.id,
fields: templateRecipient.Field,
name: foundRecipient.name ?? '',
email: foundRecipient.email,
role: templateRecipient.role,
};
});
} else {
// Backwards compatible logic for /v1/ API where we use the index to associate
// the provided recipient with the template recipient.
finalRecipients = recipients.map((recipient, index) => ({
templateRecipientId: template.Recipient[index].id,
fields: template.Recipient[index].Field,
name: recipient.name ?? '',
email: recipient.email,
role: template.Recipient[index].role,
}));
}
const documentData = await prisma.documentData.create({ const documentData = await prisma.documentData.create({
data: { data: {
type: template.templateDocumentData.type, type: template.templateDocumentData.type,
@ -57,14 +123,16 @@ export const createDocumentFromTemplate = async ({
}, },
}); });
const document = await prisma.document.create({ return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
data: { data: {
userId, userId,
teamId: template.teamId, teamId: template.teamId,
title: template.title, title: template.title,
documentDataId: documentData.id, documentDataId: documentData.id,
Recipient: { Recipient: {
create: template.Recipient.map((recipient) => ({ createMany: {
data: finalRecipients.map((recipient) => ({
email: recipient.email, email: recipient.email,
name: recipient.name, name: recipient.name,
role: recipient.role, role: recipient.role,
@ -72,7 +140,7 @@ export const createDocumentFromTemplate = async ({
})), })),
}, },
}, },
},
include: { include: {
Recipient: { Recipient: {
orderBy: { orderBy: {
@ -83,59 +151,54 @@ export const createDocumentFromTemplate = async ({
}, },
}); });
await prisma.field.createMany({ let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
data: template.Field.map((field) => {
const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId);
const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email); Object.values(finalRecipients).forEach(({ email, fields }) => {
const recipient = document.Recipient.find((recipient) => recipient.email === email);
if (!documentRecipient) { if (!recipient) {
throw new Error('Recipient not found.'); throw new Error('Recipient not found.');
} }
return { fieldsToCreate = fieldsToCreate.concat(
fields.map((field) => ({
documentId: document.id,
recipientId: recipient.id,
type: field.type, type: field.type,
page: field.page, page: field.page,
positionX: field.positionX, positionX: field.positionX,
positionY: field.positionY, positionY: field.positionY,
width: field.width, width: field.width,
height: field.height, height: field.height,
customText: field.customText, customText: '',
inserted: field.inserted, inserted: false,
documentId: document.id, })),
recipientId: documentRecipient.id,
};
}),
});
if (recipients && recipients.length > 0) {
document.Recipient = await Promise.all(
recipients.map(async (recipient, index) => {
const existingRecipient = document.Recipient.at(index);
return await prisma.recipient.upsert({
where: {
documentId_email: {
documentId: document.id,
email: existingRecipient?.email ?? recipient.email,
},
},
update: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
},
create: {
documentId: document.id,
email: recipient.email,
name: recipient.name,
role: recipient.role,
token: nanoid(),
},
});
}),
); );
} });
await tx.field.createMany({
data: fieldsToCreate,
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id,
user,
requestMetadata,
data: {
title: document.title,
},
}),
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: document,
userId,
teamId,
});
return document; return document;
});
}; };

View File

@ -1,10 +1,14 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError } from '@documenso/lib/errors/app-error';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createTemplate } from '@documenso/lib/server-only/template/create-template'; import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template'; import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { Document } from '@documenso/prisma/client';
import { authenticatedProcedure, router } from '../trpc'; import { authenticatedProcedure, router } from '../trpc';
import { import {
@ -49,19 +53,34 @@ export const templateRouter = router({
throw new Error('You have reached your document limit.'); throw new Error('You have reached your document limit.');
} }
return await createDocumentFromTemplate({ const requestMetadata = extractNextApiRequestMetadata(ctx.req);
let document: Document = await createDocumentFromTemplate({
templateId, templateId,
teamId, teamId,
userId: ctx.user.id, userId: ctx.user.id,
recipients: input.recipients, recipients: input.recipients,
requestMetadata,
}); });
if (input.sendDocument) {
document = await sendDocument({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return document;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
throw new TRPCError({ throw AppError.parseErrorToTRPCError(err);
code: 'BAD_REQUEST',
message: 'We were unable to create this document. Please try again later.',
});
} }
}), }),

View File

@ -1,7 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { RecipientRole } from '@documenso/prisma/client';
export const ZCreateTemplateMutationSchema = z.object({ export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(), title: z.string().min(1).trim(),
teamId: z.number().optional(), teamId: z.number().optional(),
@ -11,15 +9,14 @@ export const ZCreateTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
templateId: z.number(), templateId: z.number(),
teamId: z.number().optional(), teamId: z.number().optional(),
recipients: z recipients: z.array(
.array(
z.object({ z.object({
id: z.number(),
email: z.string().email(), email: z.string().email(),
name: z.string(), name: z.string().optional(),
role: z.nativeEnum(RecipientRole),
}), }),
) ),
.optional(), sendDocument: z.boolean().optional(),
}); });
export const ZDuplicateTemplateMutationSchema = z.object({ export const ZDuplicateTemplateMutationSchema = z.object({

View File

@ -103,6 +103,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
appendSigner({ appendSigner({
formId: nanoid(12), formId: nanoid(12),
name: `Recipient ${placeholderRecipientCount}`, name: `Recipient ${placeholderRecipientCount}`,
// Update TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX if this is ever changed.
email: `recipient.${placeholderRecipientCount}@documenso.com`, email: `recipient.${placeholderRecipientCount}@documenso.com`,
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
}); });