diff --git a/packages/lib/client-only/hooks/use-editor-recipients.ts b/packages/lib/client-only/hooks/use-editor-recipients.ts index b1fce27e0..88a0f1238 100644 --- a/packages/lib/client-only/hooks/use-editor-recipients.ts +++ b/packages/lib/client-only/hooks/use-editor-recipients.ts @@ -9,7 +9,8 @@ import type { UseFormReturn } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { isCcRecipient, normalizeRecipientSigningOrders, sortRecipientsForSigningOrder } from '../../utils/recipients'; +import { normalizeGroupedSigningOrders } from '../../utils/recipient-groups'; +import { isCcRecipient, sortRecipientsForSigningOrder } from '../../utils/recipients'; const LocalRecipientSchema = z.object({ formId: z.string().min(1), @@ -65,6 +66,24 @@ export const ZEditorRecipientsFormSchema = z }); } }); + + const seenSigningOrders = new Set(); + + data.signers.forEach((signer, index) => { + if (signer.role === RecipientRole.CC || typeof signer.signingOrder !== 'number') { + return; + } + + if (seenSigningOrders.has(signer.signingOrder)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'CSC envelopes do not support recipient signing groups.', + path: ['signers', index, 'signingOrder'], + }); + } + + seenSigningOrders.add(signer.signingOrder); + }); }); export type TEditorRecipientsFormSchema = z.infer; @@ -101,7 +120,7 @@ export const useEditorRecipients = ({ envelope }: EditorRecipientsProps): UseEdi const signers: TLocalRecipient[] = formRecipients.length > 0 - ? normalizeRecipientSigningOrders(sortRecipientsForSigningOrder(formRecipients)) + ? normalizeGroupedSigningOrders(sortRecipientsForSigningOrder(formRecipients)) : [ { formId: initialId, diff --git a/packages/lib/utils/recipients.ts b/packages/lib/utils/recipients.ts index a82a1e251..505389f4b 100644 --- a/packages/lib/utils/recipients.ts +++ b/packages/lib/utils/recipients.ts @@ -1,9 +1,10 @@ import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field'; import type { Envelope, Field, Recipient } from '@prisma/client'; -import { RecipientRole, SigningStatus } from '@prisma/client'; +import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { AppError, AppErrorCode } from '../errors/app-error'; +import type { TEditorEnvelope } from '../types/envelope-editor'; import type { TRecipientLite } from '../types/recipient'; import { extractLegacyIds } from '../universal/id'; import { zEmail } from './zod'; @@ -141,6 +142,32 @@ export const canRecipientBeModified = ( return true; }; +/** + * Editor-level wrapper around `canRecipientBeModified`. + * + * Template recipients and unsaved (id-less) recipients can always be modified. + */ +export const canEditorRecipientBeModified = ( + envelope: Pick, + recipientId?: number, +) => { + if (envelope.type === EnvelopeType.TEMPLATE) { + return true; + } + + if (recipientId === undefined) { + return true; + } + + const recipient = envelope.recipients.find((r) => r.id === recipientId); + + if (!recipient) { + return false; + } + + return canRecipientBeModified(recipient, envelope.fields); +}; + /** * Whether a recipient can have their fields modified by the document owner. *