From deb3a63fb8ff6e5b7326cd5b2fd1a07a37c80df4 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Tue, 12 Aug 2025 13:41:23 +0300 Subject: [PATCH] feat: allow empty placeholder emails on templates (#1930) Allow users to create template placeholders without the placeholder emails. --- .../dialogs/template-use-dialog.tsx | 7 +- .../template-page-view-recipients.tsx | 16 ++++- packages/lib/constants/template.ts | 4 ++ .../trpc/server/recipient-router/schema.ts | 21 +++++- .../document/document-read-only-fields.tsx | 17 ++++- .../template-flow/add-template-fields.tsx | 42 ++++++++---- .../add-template-placeholder-recipients.tsx | 64 ++----------------- ...d-template-placeholder-recipients.types.ts | 20 +++++- 8 files changed, 108 insertions(+), 83 deletions(-) diff --git a/apps/remix/app/components/dialogs/template-use-dialog.tsx b/apps/remix/app/components/dialogs/template-use-dialog.tsx index 242d146b9..3d4e7ba61 100644 --- a/apps/remix/app/components/dialogs/template-use-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-use-dialog.tsx @@ -15,6 +15,7 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX, TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX, + isTemplateRecipientEmailPlaceholder, } from '@documenso/lib/constants/template'; import { AppError } from '@documenso/lib/errors/app-error'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -279,7 +280,11 @@ export function TemplateUseDialog({ diff --git a/apps/remix/app/components/general/template/template-page-view-recipients.tsx b/apps/remix/app/components/general/template/template-page-view-recipients.tsx index 0a65b3a09..3896baf11 100644 --- a/apps/remix/app/components/general/template/template-page-view-recipients.tsx +++ b/apps/remix/app/components/general/template/template-page-view-recipients.tsx @@ -6,6 +6,8 @@ import { PenIcon, PlusIcon } from 'lucide-react'; import { Link } from 'react-router'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; +import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template'; +import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; export type TemplatePageViewRecipientsProps = { @@ -53,8 +55,18 @@ export const TemplatePageViewRecipients = ({ {recipients.map((recipient) => (
  • {recipient.email}

    } + avatarFallback={ + isTemplateRecipientEmailPlaceholder(recipient.email) + ? extractInitials(recipient.name) + : recipient.email.slice(0, 1).toUpperCase() + } + primaryText={ + isTemplateRecipientEmailPlaceholder(recipient.email) ? ( +

    {recipient.name}

    + ) : ( +

    {recipient.email}

    + ) + } secondaryText={

    {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)} diff --git a/packages/lib/constants/template.ts b/packages/lib/constants/template.ts index 7b6848a6b..05b9795c0 100644 --- a/packages/lib/constants/template.ts +++ b/packages/lib/constants/template.ts @@ -3,6 +3,10 @@ import { msg } from '@lingui/core/macro'; export const TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX = /recipient\.\d+@documenso\.com/i; export const TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX = /Recipient \d+/i; +export const isTemplateRecipientEmailPlaceholder = (email: string) => { + return TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(email); +}; + export const DIRECT_TEMPLATE_DOCUMENTATION = [ { title: msg`Enable Direct Link Signing`, diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index 060b3a031..e7344a9da 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -1,6 +1,7 @@ import { RecipientRole } from '@prisma/client'; import { z } from 'zod'; +import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template'; import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthSchema, @@ -186,7 +187,18 @@ export const ZSetTemplateRecipientsRequestSchema = z recipients: z.array( z.object({ nativeId: z.number().optional(), - email: z.string().toLowerCase().email().min(1), + email: z + .string() + .toLowerCase() + .refine( + (email) => { + return ( + isTemplateRecipientEmailPlaceholder(email) || + z.string().email().safeParse(email).success + ); + }, + { message: 'Please enter a valid email address' }, + ), name: z.string(), role: z.nativeEnum(RecipientRole), signingOrder: z.number().optional(), @@ -196,9 +208,12 @@ export const ZSetTemplateRecipientsRequestSchema = z }) .refine( (schema) => { - const emails = schema.recipients.map((recipient) => recipient.email); + // Filter out placeholder emails and only check uniqueness for actual emails + const nonPlaceholderEmails = schema.recipients + .map((recipient) => recipient.email) + .filter((email) => !isTemplateRecipientEmailPlaceholder(email)); - return new Set(emails).size === emails.length; + return new Set(nonPlaceholderEmails).size === nonPlaceholderEmails.length; }, // Dirty hack to handle errors when .root is populated for an array type { message: 'Recipients must have unique emails', path: ['recipients__root'] }, diff --git a/packages/ui/components/document/document-read-only-fields.tsx b/packages/ui/components/document/document-read-only-fields.tsx index 80e998c26..0786357d1 100644 --- a/packages/ui/components/document/document-read-only-fields.tsx +++ b/packages/ui/components/document/document-read-only-fields.tsx @@ -7,6 +7,7 @@ import { SigningStatus } from '@prisma/client'; import { Clock, EyeOffIcon } from 'lucide-react'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template'; import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; @@ -21,6 +22,18 @@ import { PopoverHover } from '@documenso/ui/primitives/popover'; import { getRecipientColorStyles } from '../../lib/recipient-colors'; import { FieldContent } from '../../primitives/document-flow/field-content'; +const getRecipientDisplayText = (recipient: { name: string; email: string }) => { + if (recipient.name && !isTemplateRecipientEmailPlaceholder(recipient.email)) { + return `${recipient.name} (${recipient.email})`; + } + + if (recipient.name && isTemplateRecipientEmailPlaceholder(recipient.email)) { + return recipient.name; + } + + return recipient.email; +}; + export type DocumentReadOnlyFieldsProps = { fields: DocumentField[]; documentMeta?: Pick; @@ -145,9 +158,7 @@ export const DocumentReadOnlyFields = ({

    - {field.recipient.name - ? `${field.recipient.name} (${field.recipient.email})` - : field.recipient.email}{' '} + {getRecipientDisplayText(field.recipient)}