feat: allow empty placeholder emails on templates (#1930)

Allow users to create template placeholders without the placeholder
emails.
This commit is contained in:
Catalin Pit
2025-08-12 13:41:23 +03:00
committed by GitHub
parent cc05af2062
commit deb3a63fb8
8 changed files with 108 additions and 83 deletions

View File

@ -24,6 +24,7 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
@ -593,15 +594,21 @@ export const AddTemplateFieldsFormPartial = ({
selectedSignerStyles?.comboxBoxTrigger,
)}
>
{selectedSigner?.email && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{selectedSigner?.email &&
!isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
<span className="flex-1 truncate text-left">
{selectedSigner?.name} ({selectedSigner?.email})
</span>
)}
{selectedSigner?.email &&
isTemplateRecipientEmailPlaceholder(selectedSigner.email) && (
<span className="flex-1 truncate text-left">{selectedSigner?.name}</span>
)}
{!selectedSigner?.email && (
<span className="gradie flex-1 truncate text-left">
{selectedSigner?.email}
No recipient selected
</span>
)}
@ -657,15 +664,22 @@ export const AddTemplateFieldsFormPartial = ({
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{recipient.name &&
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && (
<span title={recipient.email}>{recipient.email}</span>
)}
{recipient.name &&
isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={recipient.name}>{recipient.name}</span>
)}
{!recipient.name &&
!isTemplateRecipientEmailPlaceholder(recipient.email) && (
<span title={recipient.email}>{recipient.email}</span>
)}
</span>
</CommandItem>
))}

View File

@ -14,6 +14,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
@ -247,62 +248,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
[form, watchedSigners, toast],
);
const triggerDragAndDrop = useCallback(
(fromIndex: number, toIndex: number) => {
if (!$sensorApi.current) {
return;
}
const draggableId = signers[fromIndex].id;
const preDrag = $sensorApi.current.tryGetLock(draggableId);
if (!preDrag) {
return;
}
const drag = preDrag.snapLift();
setTimeout(() => {
// Move directly to the target index
if (fromIndex < toIndex) {
for (let i = fromIndex; i < toIndex; i++) {
drag.moveDown();
}
} else {
for (let i = fromIndex; i > toIndex; i--) {
drag.moveUp();
}
}
setTimeout(() => {
drag.drop();
}, 500);
}, 0);
},
[signers],
);
const updateSigningOrders = useCallback(
(newIndex: number, oldIndex: number) => {
const updatedSigners = form.getValues('signers').map((signer, index) => {
if (index === oldIndex) {
return { ...signer, signingOrder: newIndex + 1 };
} else if (index >= newIndex && index < oldIndex) {
return { ...signer, signingOrder: (signer.signingOrder ?? index + 1) + 1 };
} else if (index <= newIndex && index > oldIndex) {
return { ...signer, signingOrder: Math.max(1, (signer.signingOrder ?? index + 1) - 1) };
}
return signer;
});
updatedSigners.forEach((signer, index) => {
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
});
},
[form],
);
const handleSigningOrderChange = useCallback(
(index: number, newOrderString: string) => {
const trimmedOrderString = newOrderString.trim();
@ -592,7 +537,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>
<FormLabel>
<Trans>Email</Trans>
</FormLabel>
)}
@ -602,6 +547,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
type="email"
placeholder={_(msg`Email`)}
{...field}
value={
isTemplateRecipientEmailPlaceholder(field.value)
? ''
: field.value
}
disabled={
field.disabled ||
isSubmitting ||

View File

@ -1,6 +1,7 @@
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX } from '@documenso/lib/constants/template';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
export const ZAddTemplatePlacholderRecipientsFormSchema = z
@ -10,7 +11,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
name: z.string(),
name: z.string().min(1, { message: 'Name is required' }),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
@ -21,12 +22,25 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const nonPlaceholderEmails = schema.signers
.map((signer) => signer.email.toLowerCase())
.filter((email) => !TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX.test(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: 'Signers must have unique emails', path: ['signers__root'] },
)
.refine(
/*
Since placeholder emails are empty, we need to check that the names are unique.
If we don't do this, the app will add duplicate signers and merge them in the next step, where you add fields.
*/
(schema) => {
const names = schema.signers.map((signer) => signer.name.trim());
return new Set(names).size === names.length;
},
{ message: 'Signers must have unique names', path: ['signers__root'] },
);
export type TAddTemplatePlacholderRecipientsFormSchema = z.infer<