mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
fix: sort CC recipients last (#2930)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isAssistantLastSigner, normalizeRecipientSigningOrders, sortRecipientsForSigningOrder } from './recipients';
|
||||
|
||||
describe('recipient signing order helpers', () => {
|
||||
it('sorts CC recipients after ordered active recipients', () => {
|
||||
const recipients = [
|
||||
{ id: 1, role: RecipientRole.CC, signingOrder: 1 },
|
||||
{ id: 2, role: RecipientRole.SIGNER, signingOrder: 2 },
|
||||
{ id: 3, role: RecipientRole.APPROVER, signingOrder: 1 },
|
||||
];
|
||||
|
||||
expect(sortRecipientsForSigningOrder(recipients).map((recipient) => recipient.id)).toEqual([3, 2, 1]);
|
||||
});
|
||||
|
||||
it('keeps original order when recipients have the same signing order', () => {
|
||||
const recipients = [
|
||||
{ id: 2, role: RecipientRole.SIGNER, signingOrder: 1 },
|
||||
{ id: 1, role: RecipientRole.APPROVER, signingOrder: 1 },
|
||||
];
|
||||
|
||||
expect(sortRecipientsForSigningOrder(recipients).map((recipient) => recipient.id)).toEqual([2, 1]);
|
||||
});
|
||||
|
||||
it('sorts and normalizes active recipient signing order and removes it from CC recipients', () => {
|
||||
const recipients = [
|
||||
{ id: 1, role: RecipientRole.CC, signingOrder: 1 },
|
||||
{ id: 2, role: RecipientRole.SIGNER, signingOrder: 4 },
|
||||
{ id: 3, role: RecipientRole.APPROVER, signingOrder: 2 },
|
||||
];
|
||||
|
||||
expect(normalizeRecipientSigningOrders(sortRecipientsForSigningOrder(recipients))).toEqual([
|
||||
{ id: 3, role: RecipientRole.APPROVER, signingOrder: 1 },
|
||||
{ id: 2, role: RecipientRole.SIGNER, signingOrder: 2 },
|
||||
{ id: 1, role: RecipientRole.CC, signingOrder: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves caller order while normalizing signing order', () => {
|
||||
const recipients = [
|
||||
{ id: 2, role: RecipientRole.ASSISTANT, signingOrder: 2 },
|
||||
{ id: 1, role: RecipientRole.SIGNER, signingOrder: 1 },
|
||||
{ id: 3, role: RecipientRole.CC, signingOrder: 1 },
|
||||
];
|
||||
|
||||
expect(normalizeRecipientSigningOrders(recipients)).toEqual([
|
||||
{ id: 2, role: RecipientRole.ASSISTANT, signingOrder: 1 },
|
||||
{ id: 1, role: RecipientRole.SIGNER, signingOrder: 2 },
|
||||
{ id: 3, role: RecipientRole.CC, signingOrder: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
it('checks whether the last non-CC recipient is an assistant', () => {
|
||||
expect(
|
||||
isAssistantLastSigner([
|
||||
{ role: RecipientRole.SIGNER },
|
||||
{ role: RecipientRole.ASSISTANT },
|
||||
{ role: RecipientRole.CC },
|
||||
]),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isAssistantLastSigner([
|
||||
{ role: RecipientRole.ASSISTANT },
|
||||
{ role: RecipientRole.SIGNER },
|
||||
{ role: RecipientRole.CC },
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
||||
import type { Envelope } from '@prisma/client';
|
||||
import { type Field, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import type { Envelope, Field, Recipient } from '@prisma/client';
|
||||
import { RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
import { AppError, AppErrorCode } from '../errors/app-error';
|
||||
@@ -15,6 +15,58 @@ import { zEmail } from './zod';
|
||||
*/
|
||||
export const RECIPIENT_ROLES_THAT_REQUIRE_FIELDS = [RecipientRole.SIGNER] as const;
|
||||
|
||||
// signingOrder isn't required when submitting the recipient form (Zod: z.number().optional())
|
||||
type RecipientWithSigningOrder = Pick<Recipient, 'role'> & Partial<Pick<Recipient, 'signingOrder'>>;
|
||||
|
||||
export const isCcRecipient = (recipient: Pick<Recipient, 'role'>) => {
|
||||
return recipient.role === RecipientRole.CC;
|
||||
};
|
||||
|
||||
export const isAssistantLastSigner = (recipients: Pick<Recipient, 'role'>[]) => {
|
||||
const nonCcRecipients = recipients.filter((recipient) => !isCcRecipient(recipient));
|
||||
const lastNonCcRecipient = nonCcRecipients[nonCcRecipients.length - 1];
|
||||
|
||||
return lastNonCcRecipient?.role === RecipientRole.ASSISTANT;
|
||||
};
|
||||
|
||||
export const sortRecipientsForSigningOrder = <T extends RecipientWithSigningOrder>(recipients: T[]): T[] => {
|
||||
return [...recipients].sort((r1, r2) => {
|
||||
const r1IsCcRecipient = isCcRecipient(r1);
|
||||
const r2IsCcRecipient = isCcRecipient(r2);
|
||||
|
||||
// CC recipients always sort after non-CC recipients.
|
||||
if (r1IsCcRecipient !== r2IsCcRecipient) {
|
||||
return r1IsCcRecipient ? 1 : -1;
|
||||
}
|
||||
|
||||
// Order by signing order; missing orders sort last.
|
||||
const r1SigningOrder = r1.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const r2SigningOrder = r2.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
return r1SigningOrder - r2SigningOrder;
|
||||
});
|
||||
};
|
||||
|
||||
export const normalizeRecipientSigningOrders = <T extends RecipientWithSigningOrder>(
|
||||
recipients: T[],
|
||||
canUpdateRecipient: (recipient: T) => boolean = () => true,
|
||||
): Array<T & { signingOrder?: number }> => {
|
||||
const nonCcRecipients = recipients.filter((recipient) => !isCcRecipient(recipient));
|
||||
const ccRecipients = recipients.filter((recipient) => isCcRecipient(recipient));
|
||||
|
||||
const normalizedNonCcRecipients = nonCcRecipients.map((recipient, index) => ({
|
||||
...recipient,
|
||||
signingOrder: canUpdateRecipient(recipient) ? index + 1 : (recipient.signingOrder ?? index + 1),
|
||||
}));
|
||||
|
||||
const normalizedCcRecipients = ccRecipients.map((recipient) => ({
|
||||
...recipient,
|
||||
signingOrder: undefined,
|
||||
}));
|
||||
|
||||
return [...normalizedNonCcRecipients, ...normalizedCcRecipients];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns recipients who are missing required fields for their role.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user