This commit is contained in:
David Nguyen
2026-08-26 12:10:29 +10:00
parent 9dc83bdb06
commit 6db71b13d4
70 changed files with 5964 additions and 892 deletions
@@ -7,7 +7,10 @@ import {
} from '@documenso/lib/types/envelope-editor';
import { trpc } from '@documenso/trpc/react';
import type { TSetEnvelopeFieldsResponse } from '@documenso/trpc/server/envelope-router/set-envelope-fields.types';
import type { TSetEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
import type {
TSetEnvelopeRecipientsRequest,
TSetEnvelopeRecipientsResponse,
} from '@documenso/trpc/server/envelope-router/set-envelope-recipients.types';
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { getRecipientColor } from '@documenso/ui/lib/recipient-colors';
@@ -29,6 +32,14 @@ export type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
type UpdateEnvelopePayload = Pick<TUpdateEnvelopeRequest, 'data' | 'meta'>;
type SetRecipientsPayload = (TSetEnvelopeRecipientsRequest['recipients'][number] & {
/**
* Stable client-side key for the signer row, echoed to the server as
* `clientId` so newly created recipients can adopt their server-assigned id.
*/
formId?: string;
})[];
type EnvelopeEditorProviderValue = {
editorConfig: EnvelopeEditorConfig;
@@ -47,8 +58,8 @@ type EnvelopeEditorProviderValue = {
setLocalEnvelope: (localEnvelope: Partial<TEditorEnvelope>) => void;
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
setRecipientsDebounced: (recipients: SetRecipientsPayload) => void;
setRecipientsAsync: (recipients: SetRecipientsPayload) => Promise<void>;
getRecipientColorKey: (recipientId: number) => TRecipientColor;
@@ -170,6 +181,41 @@ export const EnvelopeEditorProvider = ({
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set());
/**
* Server-assigned ids for signers created during this session, keyed by
* their stable formId. The recipients form only learns real ids on a form
* reset (step navigation), so this map bridges the gap between autosaves.
*/
const recipientIdByFormIdRef = useRef<Map<string, number>>(new Map());
/**
* Merges known server-assigned ids into the outgoing payload. Without
* this, a newly created signer (id-less in the form until the next form
* reset) would be deleted and recreated on every autosave — churning ids,
* signing tokens and the audit log. The formId doubles as the `clientId`
* echoed back by the server.
*/
const withKnownRecipientIds = (localRecipients: SetRecipientsPayload) =>
localRecipients.map((recipient) => ({
...recipient,
id: recipient.id ?? (recipient.formId ? recipientIdByFormIdRef.current.get(recipient.formId) : undefined),
clientId: recipient.formId,
}));
/**
* Records the ids of newly created recipients from the save response. A
* ref — rather than writing ids back into the form — is deliberate: the
* response can land mid interaction, and a form write here re-renders the
* signer rows, which breaks an in-progress recipient drag.
*/
const rememberCreatedRecipientIds = (recipients: TSetEnvelopeRecipientsResponse['data']) => {
for (const recipient of recipients) {
if (recipient.clientId) {
recipientIdByFormIdRef.current.set(recipient.clientId, recipient.id);
}
}
};
const registerExternalFlush = useCallback((key: string, flush: () => Promise<void>) => {
externalFlushCallbacksRef.current.set(key, flush);
@@ -210,7 +256,7 @@ export const EnvelopeEditorProvider = ({
triggerSave: setRecipientsDebounced,
flush: flushSetRecipients,
isPending: isRecipientsMutationPending,
} = useEnvelopeAutosave(async (localRecipients: TSetEnvelopeRecipientsRequest['recipients']) => {
} = useEnvelopeAutosave(async (localRecipients: SetRecipientsPayload) => {
try {
let recipients: TEditorEnvelope['recipients'] = [];
@@ -220,10 +266,12 @@ export const EnvelopeEditorProvider = ({
const response = await setRecipientsMutation.mutateAsync({
envelopeId: currentEnvelope.id,
envelopeType: currentEnvelope.type,
recipients: localRecipients,
recipients: withKnownRecipientIds(localRecipients),
});
recipients = response.data;
rememberCreatedRecipientIds(response.data);
} else {
recipients = mapLocalRecipientsToRecipients({ envelope: currentEnvelope, localRecipients });
}
@@ -252,7 +300,7 @@ export const EnvelopeEditorProvider = ({
}
}, 1000);
const setRecipientsAsync = async (localRecipients: TSetEnvelopeRecipientsRequest['recipients']) => {
const setRecipientsAsync = async (localRecipients: SetRecipientsPayload) => {
setRecipientsDebounced(localRecipients);
await flushSetRecipients();
};