feat: remove email requirement for recipients (#2040)

This commit is contained in:
Lucas Smith
2025-09-23 17:13:52 +10:00
committed by GitHub
parent ed4dfc9b55
commit 3c646d9475
50 changed files with 1133 additions and 433 deletions
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
@@ -46,7 +47,7 @@ import { Form } from '../form/form';
import { RecipientSelector } from '../recipient-selector';
import { useStep } from '../stepper';
import { useToast } from '../use-toast';
import type { TAddFieldsFormSchema } from './add-fields.types';
import { type TAddFieldsFormSchema, ZAddFieldsFormSchema } from './add-fields.types';
import {
DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent,
@@ -75,6 +76,7 @@ export type FieldFormType = {
pageWidth: number;
pageHeight: number;
signerEmail: string;
recipientId: number;
fieldMeta?: FieldMeta;
};
@@ -127,9 +129,11 @@ export const AddFieldsFormPartial = ({
pageHeight: Number(field.height),
signerEmail:
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
recipientId: field.recipientId,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
})),
},
resolver: zodResolver(ZAddFieldsFormSchema),
});
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
@@ -323,6 +327,7 @@ export const AddFieldsFormPartial = ({
const field = {
formId: nanoid(12),
nativeId: undefined,
type: selectedField,
pageNumber,
pageX,
@@ -330,6 +335,7 @@ export const AddFieldsFormPartial = ({
pageWidth: fieldPageWidth,
pageHeight: fieldPageHeight,
signerEmail: selectedSigner.email,
recipientId: selectedSigner.id,
fieldMeta: undefined,
};
@@ -414,6 +420,7 @@ export const AddFieldsFormPartial = ({
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
recipientId: selectedSigner?.id ?? lastActiveField.recipientId,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
@@ -438,6 +445,7 @@ export const AddFieldsFormPartial = ({
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
recipientId: selectedSigner?.id ?? lastActiveField.recipientId,
pageNumber,
};
@@ -470,6 +478,7 @@ export const AddFieldsFormPartial = ({
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
recipientId: selectedSigner?.id ?? copiedField.recipientId,
pageX: copiedField.pageX + 3,
pageY: copiedField.pageY + 3,
});
@@ -663,7 +672,7 @@ export const AddFieldsFormPartial = ({
{isDocumentPdfLoaded &&
localFields.map((field, index) => {
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
const hasFieldError =
emptyCheckboxFields.find((f) => f.formId === field.formId) ||
emptyRadioFields.find((f) => f.formId === field.formId) ||
@@ -10,6 +10,7 @@ export const ZAddFieldsFormSchema = z.object({
nativeId: z.number().optional(),
type: z.nativeEnum(FieldType),
signerEmail: z.string().min(1),
recipientId: z.number().min(1),
pageNumber: z.number().min(1),
pageX: z.number().min(0),
pageY: z.number().min(0),
@@ -53,6 +53,10 @@ import {
import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types';
type AutoSaveResponse = {
recipients: Recipient[];
};
export type AddSignersFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
@@ -60,7 +64,7 @@ export type AddSignersFormProps = {
signingOrder?: DocumentSigningOrder | null;
allowDictateNextSigner?: boolean;
onSubmit: (_data: TAddSignersFormSchema) => void;
onAutoSave: (_data: TAddSignersFormSchema) => Promise<void>;
onAutoSave: (_data: TAddSignersFormSchema) => Promise<AutoSaveResponse>;
isDocumentPdfLoaded: boolean;
};
@@ -208,7 +212,44 @@ export const AddSignersFormPartial = ({
const formData = form.getValues();
scheduleSave(formData);
scheduleSave(formData, (response) => {
// Sync the response recipients back to form state to prevent duplicates
if (response?.recipients) {
const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => {
// Find the matching recipient from the response using nativeId
const matchingRecipient = response.recipients.find(
(recipient) => recipient.id === signer.nativeId,
);
if (matchingRecipient) {
// Update the signer with the server-returned data, especially the ID
return {
...signer,
nativeId: matchingRecipient.id,
};
}
// For new signers without nativeId, match by email and update with server ID
if (!signer.nativeId) {
const newRecipient = response.recipients.find(
(recipient) => recipient.email === signer.email,
);
if (newRecipient) {
return {
...signer,
nativeId: newRecipient.id,
};
}
}
return signer;
});
// Update the form state with the synced data
form.setValue('signers', updatedSigners, { shouldValidate: false });
}
});
};
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
@@ -4,33 +4,23 @@ import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
export const ZAddSignersFormSchema = z
.object({
signers: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z
.string()
.email({ message: msg`Invalid email`.id })
.min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
}),
),
signingOrder: z.nativeEnum(DocumentSigningOrder),
allowDictateNextSigner: z.boolean().default(false),
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: msg`Signers must have unique emails`.id, path: ['signers__root'] },
);
export const ZAddSignersFormSchema = z.object({
signers: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z
.string()
.email({ message: msg`Invalid email`.id })
.min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
}),
),
signingOrder: z.nativeEnum(DocumentSigningOrder),
allowDictateNextSigner: z.boolean().default(false),
});
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
@@ -299,6 +299,8 @@ export const FieldItem = ({
}}
ref={$el}
data-field-id={field.nativeId}
data-field-type={field.type}
data-recipient-id={field.recipientId}
>
<FieldContent field={field} />
+9 -13
View File
@@ -8,19 +8,14 @@ import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
export const ZDocumentFlowFormSchema = z.object({
title: z.string().min(1),
signers: z
.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
name: z.string(),
}),
)
.refine((signers) => {
const emails = signers.map((signer) => signer.email);
return new Set(emails).size === emails.length;
}, 'Signers must have unique emails'),
signers: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
name: z.string(),
}),
),
fields: z.array(
z.object({
@@ -28,6 +23,7 @@ export const ZDocumentFlowFormSchema = z.object({
nativeId: z.number().optional(),
type: z.nativeEnum(FieldType),
signerEmail: z.string().min(1).optional(),
recipientId: z.number().min(1),
pageNumber: z.number().min(1),
pageX: z.number().min(0),
pageY: z.number().min(0),