diff --git a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx index e4c703a2f..1852dd147 100644 --- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx @@ -2,11 +2,10 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { 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 type { Recipient } from '@documenso/prisma/client'; -import { RecipientRole } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -19,24 +18,56 @@ import { DialogTitle, DialogTrigger, } 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 { Label } from '@documenso/ui/primitives/label'; -import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons'; -import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useOptionalCurrentTeam } from '~/providers/team'; -const ZAddRecipientsForNewDocumentSchema = z.object({ - recipients: z.array( - z.object({ - email: z.string().email(), - name: z.string(), - role: z.nativeEnum(RecipientRole), - }), - ), -}); +const ZAddRecipientsForNewDocumentSchema = z + .object({ + recipients: z.array( + z.object({ + id: z.number(), + email: z.string().email(), + name: z.string(), + }), + ), + }) + // Display exactly which rows are duplicates. + .superRefine((items, ctx) => { + const uniqueEmails = new Map(); + + 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; @@ -56,33 +87,18 @@ export function UseTemplateDialog({ const team = useOptionalCurrentTeam(); - const { - control, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), defaultValues: { - recipients: - recipients.length > 0 - ? recipients.map((recipient) => ({ - nativeId: recipient.id, - formId: String(recipient.id), - name: recipient.name, - email: recipient.email, - role: recipient.role, - })) - : [ - { - name: '', - email: '', - role: RecipientRole.SIGNER, - }, - ], + recipients: recipients.map((recipient) => ({ + id: recipient.id, + name: recipient.name, + email: recipient.email, + })), }, }); - const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } = + const { mutateAsync: createDocumentFromTemplate } = trpc.template.createDocumentFromTemplate.useMutation(); const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { @@ -109,10 +125,8 @@ export function UseTemplateDialog({ } }; - const onCreateDocumentFromTemplate = handleSubmit(onSubmit); - const { fields: formRecipients } = useFieldArray({ - control, + control: form.control, name: 'recipients', }); @@ -127,120 +141,65 @@ export function UseTemplateDialog({ Document Recipients - Add the recipients to create the template with. + Add the recipients to create the template with -
- {formRecipients.map((recipient, index) => ( -
+
+
-
- +
+ {formRecipients.map((recipient, index) => ( +
+ ( + + {index === 0 && Email} - ( - + + + + + )} /> - )} - /> -
-
- + ( + + {index === 0 && Name} - ( - + + + + + )} /> - )} - /> +
+ ))}
-
- ( - - )} - /> -
- -
- - -
-
- ))} -
- - - - - - - - + + + + + ); diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 8ae5fecaf..08363434f 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,15 +1,14 @@ import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; -import type { RecipientRole } from '@documenso/prisma/client'; export type CreateDocumentFromTemplateOptions = { templateId: number; userId: number; teamId?: number; - recipients?: { + recipients: { + id: number; name?: string; email: string; - role?: RecipientRole; }[]; }; @@ -49,6 +48,20 @@ export const createDocumentFromTemplate = async ({ throw new Error('Template not found.'); } + const finalRecipients = template.Recipient.map((templateRecipient) => { + const foundRecipient = recipients.find((recipient) => recipient.id === templateRecipient.id); + + if (!foundRecipient) { + throw new Error('Recipient not found.'); + } + + return { + name: foundRecipient.name, + email: foundRecipient.email, + role: templateRecipient.role, + }; + }); + const documentData = await prisma.documentData.create({ data: { type: template.templateDocumentData.type, @@ -57,81 +70,55 @@ export const createDocumentFromTemplate = async ({ }, }); - const document = await prisma.document.create({ - data: { - userId, - teamId: template.teamId, - title: template.title, - documentDataId: documentData.id, - Recipient: { - create: template.Recipient.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - token: nanoid(), - })), - }, - }, - - include: { - Recipient: { - orderBy: { - id: 'asc', - }, - }, - documentData: true, - }, - }); - - await prisma.field.createMany({ - 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); - - return { - type: field.type, - page: field.page, - positionX: field.positionX, - positionY: field.positionY, - width: field.width, - height: field.height, - customText: field.customText, - inserted: field.inserted, - documentId: document.id, - recipientId: documentRecipient?.id || null, - }; - }), - }); - - 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, + return await prisma.$transaction(async (tx) => { + const document = await tx.document.create({ + data: { + userId, + teamId: template.teamId, + title: template.title, + documentDataId: documentData.id, + Recipient: { + create: finalRecipients.map((recipient) => ({ email: recipient.email, name: recipient.name, role: recipient.role, token: nanoid(), + })), + }, + }, + include: { + Recipient: { + orderBy: { + id: 'asc', }, - }); - }), - ); - } + }, + documentData: true, + }, + }); - return document; + await tx.field.createMany({ + 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); + + return { + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: field.customText, + inserted: field.inserted, + documentId: document.id, + recipientId: documentRecipient?.id || null, + }; + }), + }); + + return document; + }); }; diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 3f16d7b39..3fd47791f 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,7 +1,5 @@ import { z } from 'zod'; -import { RecipientRole } from '@documenso/prisma/client'; - export const ZCreateTemplateMutationSchema = z.object({ title: z.string().min(1).trim(), teamId: z.number().optional(), @@ -11,15 +9,13 @@ export const ZCreateTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ templateId: z.number(), teamId: z.number().optional(), - recipients: z - .array( - z.object({ - email: z.string().email(), - name: z.string(), - role: z.nativeEnum(RecipientRole), - }), - ) - .optional(), + recipients: z.array( + z.object({ + id: z.number(), + email: z.string().email(), + name: z.string().optional(), + }), + ), }); export const ZDuplicateTemplateMutationSchema = z.object({