fix: reviewed

This commit is contained in:
David Nguyen
2026-08-26 16:01:30 +10:00
parent 63c01fb814
commit b20a726571
23 changed files with 543 additions and 130 deletions
@@ -11,6 +11,7 @@ import {
import type { TTemplate } from '@documenso/lib/types/template';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
import { getNextDictatableRecipient } from '@documenso/lib/utils/recipient-groups';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
@@ -223,27 +224,10 @@ export const DirectTemplateSigningForm = ({
return undefined;
}
const sortedRecipients = template.recipients.sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
}
if (a.signingOrder === null) {
return 1;
}
if (b.signingOrder === null) {
return -1;
}
if (a.signingOrder === b.signingOrder) {
return a.id - b.id;
}
return a.signingOrder - b.signingOrder;
return getNextDictatableRecipient({
recipients: template.recipients,
currentRecipientId: directRecipient.id,
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === directRecipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: undefined;
}, [template.templateMeta?.signingOrder, template.recipients, directRecipient.id]);
return (
@@ -435,7 +419,7 @@ export const DirectTemplateSigningForm = ({
fields={localFields}
fieldsValidated={fieldsValidated}
recipient={directRecipient}
allowDictateNextSigner={nextRecipient && template.templateMeta?.allowDictateNextSigner}
allowDictateNextSigner={Boolean(nextRecipient && template.templateMeta?.allowDictateNextSigner)}
defaultNextSigner={nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined}
/>
</div>
@@ -100,8 +100,10 @@ export const DocumentSigningCompleteDialog = ({
const { isNameLocked, isEmailLocked } = useEmbedSigningContext() || {};
const canDictateNextSigner = allowDictateNextSigner && Boolean(defaultNextSigner);
const form = useForm<TNextSignerFormSchema>({
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
resolver: canDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
defaultValues: {
name: defaultNextSigner?.name ?? '',
email: defaultNextSigner?.email ?? '',
@@ -317,7 +319,7 @@ export const DocumentSigningCompleteDialog = ({
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
{allowDictateNextSigner && defaultNextSigner && (
{canDictateNextSigner && (
<div className="mb-4 flex flex-col gap-4">
<div className="flex flex-col gap-4 md:flex-row">
<FormField
@@ -39,7 +39,11 @@ export type DocumentSigningFormProps = {
}) => Promise<void>;
isSubmitting: boolean;
fieldsValidated: () => void;
nextRecipient?: RecipientWithFields;
/**
* The dictatable next recipient, decided server-side. Only their identity
* is needed — for the dictation flag and the prefilled inputs.
*/
nextRecipient?: Pick<Recipient, 'name' | 'email'>;
};
export const DocumentSigningForm = ({
@@ -84,6 +88,10 @@ export const DocumentSigningForm = ({
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
}, [fieldsRequiringValidation, recipient]);
const allowDictateNextSigner = Boolean(nextRecipient && document.documentMeta?.allowDictateNextSigner);
const defaultNextSigner = nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined;
const localFieldsValidated = () => {
setValidateUninsertedFields(true);
fieldsValidated();
@@ -151,10 +159,8 @@ export const DocumentSigningForm = ({
completeDocument({ nextSigner, accessAuthOptions })
}
recipient={recipient}
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
defaultNextSigner={
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
}
allowDictateNextSigner={allowDictateNextSigner}
defaultNextSigner={defaultNextSigner}
/>
</div>
</div>
@@ -223,8 +229,8 @@ export const DocumentSigningForm = ({
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
onConfirm={handleAssistantConfirmDialogSubmit}
isSubmitting={isAssistantSubmitting}
allowDictateNextSigner={nextRecipient && document.documentMeta?.allowDictateNextSigner}
defaultNextSigner={nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined}
allowDictateNextSigner={allowDictateNextSigner}
defaultNextSigner={defaultNextSigner}
/>
</form>
) : (
@@ -291,10 +297,8 @@ export const DocumentSigningForm = ({
})
}
recipient={recipient}
allowDictateNextSigner={nextRecipient && document.documentMeta?.allowDictateNextSigner}
defaultNextSigner={
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
}
allowDictateNextSigner={allowDictateNextSigner}
defaultNextSigner={defaultNextSigner}
/>
</div>
</>
@@ -23,7 +23,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Trans } from '@lingui/react/macro';
import type { Field } from '@prisma/client';
import type { Field, Recipient } from '@prisma/client';
import { FieldType, RecipientRole } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useMemo, useState } from 'react';
@@ -61,6 +61,12 @@ export type DocumentSigningPageViewV1Props = {
completedFields: CompletedField[];
isRecipientsTurn: boolean;
allRecipients?: RecipientWithFields[];
/**
* The dictatable next recipient, computed server-side over the FULL
* recipient list — must not be re-derived from the role-scoped
* `allRecipients`.
*/
nextRecipient?: Pick<Recipient, 'name' | 'email'>;
branding: DocumentSigningBranding;
includeSenderDetails: boolean;
};
@@ -72,6 +78,7 @@ export const DocumentSigningPageViewV1 = ({
completedFields,
isRecipientsTurn,
allRecipients = [],
nextRecipient,
includeSenderDetails,
branding,
}: DocumentSigningPageViewV1Props) => {
@@ -142,34 +149,6 @@ export const DocumentSigningPageViewV1 = ({
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
const targetSigner = recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null;
const nextRecipient = useMemo(() => {
if (!documentMeta?.signingOrder || documentMeta.signingOrder !== 'SEQUENTIAL') {
return undefined;
}
const sortedRecipients = [...allRecipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
}
if (a.signingOrder === null) {
return 1;
}
if (b.signingOrder === null) {
return -1;
}
if (a.signingOrder === b.signingOrder) {
return a.id - b.id;
}
return a.signingOrder - b.signingOrder;
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: undefined;
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted);
const hasPendingFields = pendingFields.length > 0;
@@ -6,6 +6,7 @@ import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/enve
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { isFieldUnsignedAndRequired, isRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
import { getNextDictatableRecipient } from '@documenso/lib/utils/recipient-groups';
import { trpc } from '@documenso/trpc/react';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { EnvelopeType, type Field, FieldType, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
@@ -290,32 +291,14 @@ export const EnvelopeSigningProvider = ({
.filter((field) => field.inserted);
const nextRecipient = useMemo(() => {
if (!envelope.documentMeta.signingOrder || envelope.documentMeta.signingOrder !== 'SEQUENTIAL') {
if (envelope.documentMeta.signingOrder !== 'SEQUENTIAL') {
return null;
}
const sortedRecipients = [...envelope.recipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
}
if (a.signingOrder === null) {
return 1;
}
if (b.signingOrder === null) {
return -1;
}
if (a.signingOrder === b.signingOrder) {
return a.id - b.id;
}
return a.signingOrder - b.signingOrder;
return getNextDictatableRecipient({
recipients: envelope.recipients,
currentRecipientId: recipient.id,
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: null;
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
const signField = async (
@@ -43,6 +43,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw new Response('Not Found', { status: 404 });
}
if (document.internalVersion !== 1) {
throw redirect(`${documentRootPath}/${document.envelopeId}/edit`);
}
const documentVisibility = document.visibility;
const currentTeamMemberRole = team.currentTeamRole;
const isRecipient = document.recipients.find((recipient) => recipient.email === user.email);
@@ -42,6 +42,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
throw redirect(templateRootPath);
}
if (template.internalVersion !== 1) {
throw redirect(`${templateRootPath}/${template.envelopeId}/edit`);
}
return superLoaderJson({
template: {
...template,