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
recipients: z.array( .object({
z.object({ sendDocument: z.boolean(),
email: z.string().email(), recipients: z.array(
name: z.string(), z.object({
role: z.nativeEnum(RecipientRole), id: z.number(),
}), email: z.string().email(),
), name: z.string(),
}); }),
),
})
// 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: '',
})) email: '',
: [ };
{ }
name: '',
email: '', return {
role: RecipientRole.SIGNER, 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)}>
name={`recipients.${index}.email`} <fieldset className="flex h-full flex-col" disabled={form.formState.isSubmitting}>
render={({ field }) => ( <div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1">
<Input {formRecipients.map((recipient, index) => (
id={`recipient-${recipient.id}-email`} <div className="flex w-full flex-row space-x-4" key={recipient.id}>
type="email" <FormField
className="bg-background mt-2" control={form.control}
disabled={isSubmitting} name={`recipients.${index}.email`}
{...field} render={({ field }) => (
<FormItem className="w-full">
{index === 0 && <FormLabel required>Email</FormLabel>}
<FormControl>
<Input {...field} placeholder={recipients[index].email} />
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
)}
/>
</div>
<div className="flex-1"> <FormField
<Label htmlFor={`recipient-${recipient.id}-name`}>Name</Label> control={form.control}
name={`recipients.${index}.name`}
render={({ field }) => (
<FormItem className="w-full">
{index === 0 && <FormLabel>Name</FormLabel>}
<Controller <FormControl>
control={control} <Input {...field} placeholder={recipients[index].name} />
name={`recipients.${index}.name`} </FormControl>
render={({ field }) => ( <FormMessage />
<Input </FormItem>
id={`recipient-${recipient.id}-name`} )}
type="text"
className="bg-background mt-2"
disabled={isSubmitting}
{...field}
/> />
)} </div>
/> ))}
</div> </div>
<div className="w-[60px]"> {recipients.length > 0 && (
<Controller <div className="mt-4 flex flex-row items-center">
control={control} <FormField
name={`recipients.${index}.role`} control={form.control}
render={({ field: { value, onChange } }) => ( name="sendDocument"
<Select value={value} onValueChange={(x) => onChange(x)}> render={({ field }) => (
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger> <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}
/>
<SelectContent className="" align="end"> <label
<SelectItem value={RecipientRole.SIGNER}> className="text-muted-foreground ml-2 flex items-center text-sm"
<div className="flex items-center"> htmlFor="sendDocument"
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span> >
Signer Send document
</div> <Tooltip>
</SelectItem> <TooltipTrigger type="button">
<InfoIcon className="mx-1 h-4 w-4" />
</TooltipTrigger>
<SelectItem value={RecipientRole.CC}> <TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
<div className="flex items-center"> <p>
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span> The document will be immediately sent to recipients if this is
Receives copy checked.
</div> </p>
</SelectItem>
<SelectItem value={RecipientRole.APPROVER}> <p>Otherwise, the document will be created as a draft.</p>
<div className="flex items-center"> </TooltipContent>
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span> </Tooltip>
Approver </label>
</div> </div>
</SelectItem> </FormItem>
)}
/>
</div>
)}
<SelectItem value={RecipientRole.VIEWER}> <DialogFooter>
<div className="flex items-center"> <DialogClose asChild>
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span> <Button type="button" variant="secondary">
Viewer Close
</div> </Button>
</SelectItem> </DialogClose>
</SelectContent>
</Select>
)}
/>
</div>
<div className="w-full"> <Button type="submit" loading={form.formState.isSubmitting}>
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.email} /> {form.getValues('sendDocument') ? 'Create and send' : 'Create as draft'}
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.name} /> </Button>
</div> </DialogFooter>
</div> </fieldset>
))} </form>
</div> </Form>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
<Button
type="button"
loading={isCreatingDocumentFromTemplate}
disabled={isCreatingDocumentFromTemplate}
onClick={onCreateDocumentFromTemplate}
>
Create Document
</Button>
</DialogFooter>
</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.object({ z.array(
name: z.string().min(1), z.object({
email: z.string().email().min(1), id: z.number(),
role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER), name: z.string().min(1),
}), email: z.string().email().min(1),
), }),
),
z.array(
z.object({
name: z.string().min(1),
email: z.string().email().min(1),
}),
),
]),
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:
name?: string; | RecipientWithId[]
email: string; | {
role?: RecipientRole; name?: string;
}[]; email: string;
}[];
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: {
Field: true, include: {
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,85 +123,82 @@ export const createDocumentFromTemplate = async ({
}, },
}); });
const document = await prisma.document.create({ return await prisma.$transaction(async (tx) => {
data: { const document = await tx.document.create({
userId, data: {
teamId: template.teamId, userId,
title: template.title, teamId: template.teamId,
documentDataId: documentData.id, title: template.title,
Recipient: { documentDataId: documentData.id,
create: template.Recipient.map((recipient) => ({ Recipient: {
email: recipient.email, createMany: {
name: recipient.name, data: finalRecipients.map((recipient) => ({
role: recipient.role, email: recipient.email,
token: nanoid(), name: recipient.name,
})), role: recipient.role,
}, token: nanoid(),
}, })),
},
include: {
Recipient: {
orderBy: {
id: 'asc',
}, },
}, },
documentData: true, include: {
}, Recipient: {
}); orderBy: {
id: 'asc',
},
},
documentData: true,
},
});
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(
type: field.type, fields.map((field) => ({
page: field.page, documentId: document.id,
positionX: field.positionX, recipientId: recipient.id,
positionY: field.positionY, type: field.type,
width: field.width, page: field.page,
height: field.height, positionX: field.positionX,
customText: field.customText, positionY: field.positionY,
inserted: field.inserted, width: field.width,
height: field.height,
customText: '',
inserted: false,
})),
);
});
await tx.field.createMany({
data: fieldsToCreate,
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
documentId: document.id, documentId: document.id,
recipientId: documentRecipient.id, user,
}; requestMetadata,
}), data: {
}); title: document.title,
},
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(),
},
});
}), }),
); });
}
return document; await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: document,
userId,
teamId,
});
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), }),
}), ),
) sendDocument: z.boolean().optional(),
.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,
}); });