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 8eafcbb24..7000e795a 100644 --- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx @@ -5,6 +5,7 @@ import { InfoIcon, Plus } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; 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 { trpc } from '@documenso/trpc/react'; @@ -96,11 +97,23 @@ export function UseTemplateDialog({ resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), defaultValues: { sendDocument: false, - recipients: recipients.map((recipient) => ({ - id: recipient.id, - name: '', - email: '', - })), + recipients: recipients.map((recipient) => { + const isRecipientPlaceholder = recipient.email.match(TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX); + + if (isRecipientPlaceholder) { + return { + id: recipient.id, + name: '', + email: '', + }; + } + + return { + id: recipient.id, + name: recipient.name, + email: recipient.email, + }; + }), }, }); @@ -253,11 +266,7 @@ export function UseTemplateDialog({ diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index c94d46119..7d75c4f65 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -196,7 +196,7 @@ test('[TEMPLATES]: use template', async ({ page }) => { await page.getByPlaceholder('Recipient 1').click(); await page.getByPlaceholder('Recipient 1').fill('name'); - await page.getByRole('button', { name: 'Review' }).click(); + await page.getByRole('button', { name: 'Create as draft' }).click(); await page.waitForURL(/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL('/documents'); @@ -214,7 +214,7 @@ test('[TEMPLATES]: use template', async ({ page }) => { await page.getByPlaceholder('Recipient 1').click(); await page.getByPlaceholder('Recipient 1').fill('name'); - await page.getByRole('button', { name: 'Review' }).click(); + await page.getByRole('button', { name: 'Create as draft' }).click(); await page.waitForURL(/\/t\/.+\/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL(`/t/${team.url}/documents`); diff --git a/packages/lib/constants/template.ts b/packages/lib/constants/template.ts new file mode 100644 index 000000000..80dee97cf --- /dev/null +++ b/packages/lib/constants/template.ts @@ -0,0 +1 @@ +export const TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i; 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 a16416a44..6a1ed7399 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,6 +1,12 @@ import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; -import type { Recipient } 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; @@ -8,6 +14,11 @@ type RecipientWithId = { email: string; }; +type FinalRecipient = Pick & { + templateRecipientId: number; + fields: Field[]; +}; + export type CreateDocumentFromTemplateOptions = { templateId: number; userId: number; @@ -18,6 +29,7 @@ export type CreateDocumentFromTemplateOptions = { name?: string; email: string; }[]; + requestMetadata?: RequestMetadata; }; export const createDocumentFromTemplate = async ({ @@ -25,7 +37,14 @@ export const createDocumentFromTemplate = async ({ userId, teamId, recipients, + requestMetadata, }: CreateDocumentFromTemplateOptions) => { + const user = await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); + const template = await prisma.template.findUnique({ where: { id: templateId, @@ -46,10 +65,9 @@ export const createDocumentFromTemplate = async ({ }), }, include: { - Recipient: true, - Field: { + Recipient: { include: { - Recipient: true, + Field: true, }, }, templateDocumentData: true, @@ -64,7 +82,7 @@ export const createDocumentFromTemplate = async ({ throw new Error('Invalid number of recipients.'); } - let finalRecipients: Pick[] = []; + let finalRecipients: FinalRecipient[] = []; if (recipients.length > 0 && Object.prototype.hasOwnProperty.call(recipients[0], 'id')) { finalRecipients = template.Recipient.map((templateRecipient) => { @@ -78,6 +96,8 @@ export const createDocumentFromTemplate = async ({ } return { + templateRecipientId: templateRecipient.id, + fields: templateRecipient.Field, name: foundRecipient.name ?? '', email: foundRecipient.email, role: templateRecipient.role, @@ -87,6 +107,8 @@ export const createDocumentFromTemplate = async ({ // 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, @@ -109,12 +131,14 @@ export const createDocumentFromTemplate = async ({ title: template.title, documentDataId: documentData.id, Recipient: { - create: finalRecipients.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - token: nanoid(), - })), + createMany: { + data: finalRecipients.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + role: recipient.role, + token: nanoid(), + })), + }, }, }, include: { @@ -127,31 +151,54 @@ export const createDocumentFromTemplate = async ({ }, }); - await tx.field.createMany({ - data: template.Field.map((field) => { - const documentRecipient = document.Recipient.find( - (recipient) => recipient.email === field.Recipient.email, - ); + let fieldsToCreate: Omit[] = []; - if (!documentRecipient) { - throw new Error('Recipient not found.'); - } + Object.values(finalRecipients).forEach(({ email, fields }) => { + const recipient = document.Recipient.find((recipient) => recipient.email === email); - return { + if (!recipient) { + throw new Error('Recipient not found.'); + } + + fieldsToCreate = fieldsToCreate.concat( + fields.map((field) => ({ + documentId: document.id, + recipientId: recipient.id, 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, - }; + 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, + user, + requestMetadata, + data: { + title: document.title, + }, }), }); + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_CREATED, + data: document, + userId, + teamId, + }); + return document; }); }; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 07917d600..3cca69548 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -53,11 +53,14 @@ export const templateRouter = router({ throw new Error('You have reached your document limit.'); } + const requestMetadata = extractNextApiRequestMetadata(ctx.req); + let document: Document = await createDocumentFromTemplate({ templateId, teamId, userId: ctx.user.id, recipients: input.recipients, + requestMetadata, }); if (input.sendDocument) { @@ -65,7 +68,7 @@ export const templateRouter = router({ documentId: document.id, userId: ctx.user.id, teamId, - requestMetadata: extractNextApiRequestMetadata(ctx.req), + requestMetadata, }).catch((err) => { console.error(err); diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx index d285fbe44..cd48158c4 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -103,6 +103,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({ appendSigner({ formId: nanoid(12), name: `Recipient ${placeholderRecipientCount}`, + // Update TEMPLATE_RECIPIENT_PLACEHOLDER_REGEX if this is ever changed. email: `recipient.${placeholderRecipientCount}@documenso.com`, role: RecipientRole.SIGNER, });