fix: sort CC recipients last

This commit is contained in:
Catalin Pit
2026-06-04 11:59:45 +03:00
parent 0ecde7ac1e
commit 699d7657b4
@@ -7,7 +7,11 @@ import { useOptionalSession } from '@documenso/lib/client-only/providers/session
import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema'; import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth'; import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients'; import {
isCcRecipient,
normalizeRecipientSigningOrders,
canRecipientBeModified as utilCanRecipientBeModified,
} from '@documenso/lib/utils/recipients';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select'; import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/recipient-action-auth-select';
import { import {
@@ -154,17 +158,23 @@ export const EnvelopeEditorRecipientForm = () => {
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT); return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
}, [watchedSigners]); }, [watchedSigners]);
const normalizeSigningOrders = (signers: typeof watchedSigners) => { const normalizeSigningOrders = (signers: typeof watchedSigners, options: { preserveOrder?: boolean } = {}) => {
return signers return normalizeRecipientSigningOrders(signers, {
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0)) ...options,
.map((signer, index) => ({ ...signer, signingOrder: index + 1 })); canUpdateRecipient: (signer) => canRecipientBeModified(signer.id),
});
}; };
const { const activeRecipientCount = watchedSigners.filter((signer) => !isCcRecipient(signer)).length;
append: appendSigner,
fields: signers, const isAssistantLastActiveRecipient = (signers: typeof watchedSigners) => {
remove: removeSigner, const activeRecipients = signers.filter((signer) => !isCcRecipient(signer));
} = useFieldArray({ const lastActiveRecipient = activeRecipients[activeRecipients.length - 1];
return lastActiveRecipient?.role === RecipientRole.ASSISTANT;
};
const { fields: signers, remove: removeSigner } = useFieldArray({
control, control,
name: 'signers', name: 'signers',
keyName: 'nativeId', keyName: 'nativeId',
@@ -207,14 +217,31 @@ export const EnvelopeEditorRecipientForm = () => {
return utilCanRecipientBeModified(recipient, fields); return utilCanRecipientBeModified(recipient, fields);
}; };
const appendNormalizedSigner = (signer: (typeof watchedSigners)[number], shouldFocus = false) => {
const updatedSigners = normalizeSigningOrders([...form.getValues('signers'), signer]);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
if (shouldFocus) {
const signerIndex = updatedSigners.findIndex((updatedSigner) => updatedSigner.formId === signer.formId);
if (signerIndex !== -1) {
requestAnimationFrame(() => form.setFocus(`signers.${signerIndex}.email`));
}
}
};
const onAddSigner = () => { const onAddSigner = () => {
appendSigner({ appendNormalizedSigner({
formId: nanoid(12), formId: nanoid(12),
name: '', name: '',
email: '', email: '',
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
actionAuth: [], actionAuth: [],
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1, signingOrder: activeRecipientCount + 1,
}); });
}; };
@@ -322,18 +349,16 @@ export const EnvelopeEditorRecipientForm = () => {
form.setFocus(`signers.${emptySignerIndex}.email`); form.setFocus(`signers.${emptySignerIndex}.email`);
} else { } else {
appendSigner( appendNormalizedSigner(
{ {
formId: nanoid(12), formId: nanoid(12),
name: currentEditorName ?? '', name: currentEditorName ?? '',
email: currentEditorEmail ?? '', email: currentEditorEmail ?? '',
role: RecipientRole.SIGNER, role: RecipientRole.SIGNER,
actionAuth: [], actionAuth: [],
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1, signingOrder: activeRecipientCount + 1,
},
{
shouldFocus: true,
}, },
true,
); );
void form.trigger('signers'); void form.trigger('signers');
@@ -368,18 +393,14 @@ export const EnvelopeEditorRecipientForm = () => {
items.splice(insertIndex, 0, reorderedSigner); items.splice(insertIndex, 0, reorderedSigner);
const updatedSigners = items.map((signer, index) => ({ const updatedSigners = normalizeSigningOrders(items, { preserveOrder: true });
...signer,
signingOrder: !canRecipientBeModified(signer.id) ? signer.signingOrder : index + 1,
}));
form.setValue('signers', updatedSigners, { form.setValue('signers', updatedSigners, {
shouldValidate: true, shouldValidate: true,
shouldDirty: true, shouldDirty: true,
}); });
const lastSigner = updatedSigners[updatedSigners.length - 1]; if (isAssistantLastActiveRecipient(updatedSigners)) {
if (lastSigner.role === RecipientRole.ASSISTANT) {
toast({ toast({
title: t`Warning: Assistant as last signer`, title: t`Warning: Assistant as last signer`,
description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`, description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
@@ -410,18 +431,19 @@ export const EnvelopeEditorRecipientForm = () => {
return; return;
} }
const updatedSigners = currentSigners.map((signer, idx) => ({ const updatedSigners = normalizeSigningOrders(
...signer, currentSigners.map((signer, idx) => ({
role: idx === index ? role : signer.role, ...signer,
signingOrder: !canRecipientBeModified(signer.id) ? signer.signingOrder : idx + 1, role: idx === index ? role : signer.role,
})); })),
);
form.setValue('signers', updatedSigners, { form.setValue('signers', updatedSigners, {
shouldValidate: true, shouldValidate: true,
shouldDirty: true, shouldDirty: true,
}); });
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) { if (role === RecipientRole.ASSISTANT && isAssistantLastActiveRecipient(updatedSigners)) {
toast({ toast({
title: t`Warning: Assistant as last signer`, title: t`Warning: Assistant as last signer`,
description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`, description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
@@ -446,22 +468,32 @@ export const EnvelopeEditorRecipientForm = () => {
const currentSigners = form.getValues('signers'); const currentSigners = form.getValues('signers');
const signer = currentSigners[index]; const signer = currentSigners[index];
// Remove signer from current position and insert at new position if (isCcRecipient(signer)) {
const remainingSigners = currentSigners.filter((_, idx) => idx !== index); return;
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1); }
remainingSigners.splice(newPosition, 0, signer);
const updatedSigners = remainingSigners.map((s, idx) => ({ const signersWithSigningOrder = currentSigners.filter((s) => !isCcRecipient(s));
...s, const signersWithoutSigningOrder = currentSigners.filter((s) => isCcRecipient(s));
signingOrder: !canRecipientBeModified(s.id) ? s.signingOrder : idx + 1, const currentSigningOrderIndex = signersWithSigningOrder.findIndex((s) => s.formId === signer.formId);
}));
if (currentSigningOrderIndex === -1) {
return;
}
const [reorderedSigner] = signersWithSigningOrder.splice(currentSigningOrderIndex, 1);
const newPosition = Math.min(Math.max(0, newOrder - 1), signersWithSigningOrder.length);
signersWithSigningOrder.splice(newPosition, 0, reorderedSigner);
const updatedSigners = normalizeSigningOrders([...signersWithSigningOrder, ...signersWithoutSigningOrder], {
preserveOrder: true,
});
form.setValue('signers', updatedSigners, { form.setValue('signers', updatedSigners, {
shouldValidate: true, shouldValidate: true,
shouldDirty: true, shouldDirty: true,
}); });
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) { if (signer.role === RecipientRole.ASSISTANT && isAssistantLastActiveRecipient(updatedSigners)) {
toast({ toast({
title: t`Warning: Assistant as last signer`, title: t`Warning: Assistant as last signer`,
description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`, description: t`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
@@ -475,10 +507,12 @@ export const EnvelopeEditorRecipientForm = () => {
setShowSigningOrderConfirmation(false); setShowSigningOrderConfirmation(false);
const currentSigners = form.getValues('signers'); const currentSigners = form.getValues('signers');
const updatedSigners = currentSigners.map((signer) => ({ const updatedSigners = normalizeSigningOrders(
...signer, currentSigners.map((signer) => ({
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role, ...signer,
})); role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
})),
);
form.setValue('signers', updatedSigners, { form.setValue('signers', updatedSigners, {
shouldValidate: true, shouldValidate: true,
@@ -781,6 +815,7 @@ export const EnvelopeEditorRecipientForm = () => {
isDragDisabled={ isDragDisabled={
!isSigningOrderSequential || !isSigningOrderSequential ||
isSubmitting || isSubmitting ||
isCcRecipient(signer) ||
!canRecipientBeModified(signer.id) || !canRecipientBeModified(signer.id) ||
!signer.signingOrder !signer.signingOrder
} }
@@ -804,7 +839,11 @@ export const EnvelopeEditorRecipientForm = () => {
})} })}
> >
<div className="flex flex-row items-center gap-x-2"> <div className="flex flex-row items-center gap-x-2">
{isSigningOrderSequential && ( {isSigningOrderSequential && isCcRecipient(signer) && (
<div className="mt-auto h-10 w-[4.25rem] flex-shrink-0" />
)}
{isSigningOrderSequential && !isCcRecipient(signer) && (
<FormField <FormField
control={form.control} control={form.control}
name={`signers.${index}.signingOrder`} name={`signers.${index}.signingOrder`}
@@ -820,7 +859,7 @@ export const EnvelopeEditorRecipientForm = () => {
<FormControl> <FormControl>
<Input <Input
type="number" type="number"
max={signers.length} max={activeRecipientCount}
data-testid="signing-order-input" data-testid="signing-order-input"
className={cn( className={cn(
'w-10 text-center', 'w-10 text-center',
@@ -961,7 +1000,6 @@ export const EnvelopeEditorRecipientForm = () => {
onValueChange={(value) => { onValueChange={(value) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole); handleRoleChange(index, value as RecipientRole);
field.onChange(value);
}} }}
disabled={ disabled={
snapshot.isDragging || isSubmitting || !canRecipientBeModified(signer.id) snapshot.isDragging || isSubmitting || !canRecipientBeModified(signer.id)