fix: reviewed

This commit is contained in:
David Nguyen
2026-08-30 22:40:18 +10:00
parent 040517a3a0
commit 46c00878f5
3 changed files with 36 additions and 9 deletions
@@ -323,14 +323,17 @@ export const EnvelopeEditorRecipientForm = () => {
const { data } = validatedFormValues;
// Weird edge case where the whole envelope is created via API
// with no signing order. If they come to this page it will show an error
// since they aren't equal and the recipient is no longer editable.
// Locked recipients hold persisted values the server refuses to rewrite,
// e.g. an envelope created via API with no signing order where a recipient
// has already signed. Restore their PERSISTED order so form normalization
// drift never submits a "changed" locked recipient the server rejects.
const envelopeRecipients = data.signers.map((recipient) => {
if (!canRecipientBeModified(recipient.id)) {
const persistedRecipient = recipients.find((envelopeRecipient) => envelopeRecipient.id === recipient.id);
return {
...recipient,
signingOrder: recipient.signingOrder,
signingOrder: persistedRecipient?.signingOrder ?? undefined,
};
}
return recipient;
@@ -356,7 +359,7 @@ export const EnvelopeEditorRecipientForm = () => {
signer.email !== recipient.email ||
signer.name !== recipient.name ||
signer.role !== recipient.role ||
signer.signingOrder !== recipient.signingOrder ||
(signer.signingOrder ?? null) !== (recipient.signingOrder ?? null) ||
!isDeepEqual(signerActionAuth, recipientActionAuth)
);
});
@@ -3,14 +3,14 @@ import { ZRecipientActionAuthTypesSchema, ZRecipientAuthOptionsSchema } from '@d
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
import { zodResolver } from '@hookform/resolvers/zod';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { DocumentSigningOrder, EnvelopeType, RecipientRole } from '@prisma/client';
import { useId } from 'react';
import type { UseFormReturn } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { normalizeGroupedSigningOrders } from '../../utils/recipient-groups';
import { isCcRecipient, sortRecipientsForSigningOrder } from '../../utils/recipients';
import { canRecipientBeModified, isCcRecipient, sortRecipientsForSigningOrder } from '../../utils/recipients';
const LocalRecipientSchema = z.object({
formId: z.string().min(1),
@@ -154,6 +154,24 @@ export const useEditorRecipients = ({ envelope }: EditorRecipientsProps): UseEdi
const sourceRecipients = sortRecipientsForSigningOrder(recipients || envelope.recipients);
// Locked recipients hold persisted values the server refuses to rewrite.
// Initialization must never assign or renumber their signing orders —
// doing so makes the very first autosave submit a "changed" locked
// recipient, which the server rejects on every subsequent save.
const isRecipientLocked = (recipientId: number) => {
if (envelope.type === EnvelopeType.TEMPLATE) {
return false;
}
const persistedRecipient = sourceRecipients.find((recipient) => recipient.id === recipientId);
if (!persistedRecipient) {
return false;
}
return !canRecipientBeModified(persistedRecipient, envelope.fields);
};
// A recipient without a persisted order means "last" everywhere else — the
// server sorts NULLS LAST. Continue numbering after the highest existing
// order rather than guessing from array position: a guess can land on a
@@ -170,6 +188,10 @@ export const useEditorRecipients = ({ envelope }: EditorRecipientsProps): UseEdi
signingOrderByRecipientId.set(recipient.id, undefined);
} else if (typeof recipient.signingOrder === 'number') {
signingOrderByRecipientId.set(recipient.id, recipient.signingOrder);
} else if (isRecipientLocked(recipient.id)) {
// A locked null order must round-trip as-is: a synthetic number would
// read as a change to a recipient the server refuses to modify.
signingOrderByRecipientId.set(recipient.id, undefined);
} else {
fallbackOrder += 1;
signingOrderByRecipientId.set(recipient.id, fallbackOrder);
@@ -188,7 +210,7 @@ export const useEditorRecipients = ({ envelope }: EditorRecipientsProps): UseEdi
const signers: TLocalRecipient[] =
formRecipients.length > 0
? normalizeGroupedSigningOrders(formRecipients)
? normalizeGroupedSigningOrders(formRecipients, (formRecipient) => !isRecipientLocked(formRecipient.id))
: [
{
formId: initialId,
@@ -348,7 +348,9 @@ const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: Recipie
recipient.email !== newRecipientData.email ||
recipient.name !== newRecipientData.name ||
recipient.role !== newRecipientData.role ||
recipient.signingOrder !== newRecipientData.signingOrder ||
// Null and undefined both mean "no order": the request schema cannot
// carry null, so a persisted null arrives as undefined
(recipient.signingOrder ?? null) !== (newRecipientData.signingOrder ?? null) ||
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
);