From 4189a34de0139b2921e396a6be235c26adfca34f Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 14 Feb 2025 10:32:18 +0000 Subject: [PATCH] feat: dictate next signers in signing ordeR --- .../documents/[id]/edit-document.tsx | 23 +- .../src/app/(signing)/sign/[token]/form.tsx | 110 +++-- .../(signing)/sign/[token]/sign-dialog.tsx | 457 ++++++++++++++---- .../sign/[token]/step-sign-dialog.tsx | 413 ++++++++++++++++ .../document-meta/upsert-document-meta.ts | 4 + .../document/complete-document-with-token.ts | 63 +++ packages/lib/types/document.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + .../trpc/server/document-router/router.ts | 10 +- .../trpc/server/document-router/schema.ts | 1 + .../trpc/server/recipient-router/router.ts | 11 +- .../trpc/server/recipient-router/schema.ts | 6 + .../primitives/document-flow/add-signers.tsx | 32 ++ .../document-flow/add-signers.types.ts | 1 + 15 files changed, 985 insertions(+), 150 deletions(-) create mode 100644 apps/web/src/app/(signing)/sign/[token]/step-sign-dialog.tsx create mode 100644 packages/prisma/migrations/20250213164804_add_modify_next_signer_meta/migration.sql diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 7977aa2c6..8ca10a624 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -73,7 +73,7 @@ export const EditDocumentForm = ({ const { recipients, fields } = document; - const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({ + const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, onSuccess: (newData) => { utils.document.getDocumentWithDetailsById.setData( @@ -85,19 +85,6 @@ export const EditDocumentForm = ({ }, }); - const { mutateAsync: setSigningOrderForDocument } = - trpc.document.setSigningOrderForDocument.useMutation({ - ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, - onSuccess: (newData) => { - utils.document.getDocumentWithDetailsById.setData( - { - documentId: initialDocument.id, - }, - (oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }), - ); - }, - }); - const { mutateAsync: addFields } = trpc.field.addFields.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, onSuccess: ({ fields: newFields }) => { @@ -216,9 +203,12 @@ export const EditDocumentForm = ({ const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => { try { await Promise.all([ - setSigningOrderForDocument({ + updateDocument({ documentId: document.id, - signingOrder: data.signingOrder, + meta: { + signingOrder: data.signingOrder, + modifyNextSigner: data.modifyNextSigner, + }, }), setRecipients({ @@ -391,6 +381,7 @@ export const EditDocumentForm = ({ documentFlow={documentFlow.signers} recipients={recipients} signingOrder={document.documentMeta?.signingOrder} + modifyNextSigner={document.documentMeta?.modifyNextSigner} fields={fields} isDocumentEnterprise={isDocumentEnterprise} onSubmit={onAddSignersFormSubmit} diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 47033cf11..6d102d98d 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -15,7 +15,12 @@ import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { Recipient } from '@documenso/prisma/client'; -import { type Field, FieldType, RecipientRole } from '@documenso/prisma/client'; +import { + DocumentSigningOrder, + type Field, + FieldType, + RecipientRole, +} from '@documenso/prisma/client'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; @@ -42,6 +47,11 @@ export type SigningFormProps = { setSelectedSignerId?: (id: number | null) => void; }; +type SigningFormData = { + email?: string; + name?: string; +}; + export const SigningForm = ({ document, recipient, @@ -77,7 +87,7 @@ export const SigningForm = ({ }, }); - const { handleSubmit, formState } = useForm(); + const { handleSubmit, formState } = useForm(); // Keep the loading state going if successful since the redirect may take some time. const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful; @@ -102,20 +112,58 @@ export const SigningForm = ({ validateFieldsInserted(fieldsRequiringValidation); }; - const onFormSubmit = async () => { - setValidateUninsertedFields(true); + const completeDocument = async ( + authOptions?: TRecipientActionAuth, + nextSigner?: { email: string; name: string }, + ) => { + const payload = { + token: recipient.token, + documentId: document.id, + authOptions, + ...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}), + }; - const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation); + await completeDocumentWithToken(payload); - if (hasSignatureField && !signatureValid) { - return; + analytics.capture('App: Recipient has completed signing', { + signerId: recipient.id, + documentId: document.id, + timestamp: new Date().toISOString(), + }); + + redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`); + }; + + const onFormSubmit = async (data: SigningFormData) => { + try { + setValidateUninsertedFields(true); + + const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation); + + if (hasSignatureField && !signatureValid) { + throw new Error('Please provide a valid signature'); + } + + if (!isFieldsValid) { + throw new Error('Please complete all required fields'); + } + + const nextSigner = + data.email && data.name + ? { + email: data.email, + name: data.name, + } + : undefined; + + await completeDocument(undefined, nextSigner); + } catch (error) { + toast({ + title: 'Error', + description: 'An error occurred while completing the document. Please try again.', + variant: 'destructive', + }); } - - if (!isFieldsValid) { - return; - } - - await completeDocument(); }; const onAssistantFormSubmit = () => { @@ -143,22 +191,6 @@ export const SigningForm = ({ } }; - const completeDocument = async (authOptions?: TRecipientActionAuth) => { - await completeDocumentWithToken({ - token: recipient.token, - documentId: document.id, - authOptions, - }); - - analytics.capture('App: Recipient has completed signing', { - signerId: recipient.id, - documentId: document.id, - timestamp: new Date().toISOString(), - }); - - redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`); - }; - return (
{ + await handleSubmit(async (formData) => + onFormSubmit({ ...formData, ...nextSigner }), + )(); + }} documentTitle={document.title} fields={fields} fieldsValidated={fieldsValidated} role={recipient.role} disabled={!isRecipientsTurn} + canModifyNextSigner={ + document.documentMeta?.modifyNextSigner && + document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL + } />
@@ -383,12 +423,20 @@ export const SigningForm = ({ { + await handleSubmit(async (formData) => + onFormSubmit({ ...formData, ...nextSigner }), + )(); + }} documentTitle={document.title} fields={fields} fieldsValidated={fieldsValidated} role={recipient.role} disabled={!isRecipientsTurn} + canModifyNextSigner={ + document.documentMeta?.modifyNextSigner && + document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL + } /> diff --git a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx index 1bbffa9e6..c3029210e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -1,18 +1,35 @@ +'use client'; + import { useMemo, useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; import { Trans } from '@lingui/macro'; +import { ArrowRight } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; import type { Field } from '@documenso/prisma/client'; import { RecipientRole } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, + DialogClose, DialogContent, DialogFooter, DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; import { SigningDisclosure } from '~/components/general/signing-disclosure'; @@ -21,12 +38,35 @@ export type SignDialogProps = { documentTitle: string; fields: Field[]; fieldsValidated: () => void | Promise; - onSignatureComplete: () => void | Promise; + onSignatureComplete: (nextSigner?: { email: string; name: string }) => void | Promise; role: RecipientRole; disabled?: boolean; + canModifyNextSigner?: boolean; }; -export const SignDialog = ({ +const formSchema = z.object({ + nextSigner: z + .object({ + email: z.string().email({ message: 'Please enter a valid email address' }).optional(), + name: z.string().min(1, { message: 'Name is required' }).optional(), + }) + .refine( + (data) => { + if (data.name) { + return !!data.email; + } + return true; + }, + { + message: 'Email is required when name is provided', + path: ['email'], + }, + ), +}); + +type TFormSchema = z.infer; + +export function SignDialog({ isSubmitting, documentTitle, fields, @@ -34,7 +74,9 @@ export const SignDialog = ({ onSignatureComplete, role, disabled = false, -}: SignDialogProps) => { + canModifyNextSigner = false, +}: SignDialogProps) { + const [step, setStep] = useState(1); const [showDialog, setShowDialog] = useState(false); const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]); @@ -47,104 +89,325 @@ export const SignDialog = ({ setShowDialog(open); }; + const totalSteps = 2; + + const handleContinue = () => { + if (step < totalSteps) { + setStep(step + 1); + } + }; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + nextSigner: { + email: '', + name: '', + }, + }, + }); + + const onFormSubmit = async (data: TFormSchema) => { + try { + await fieldsValidated(); + + if (!canModifyNextSigner || !data.nextSigner.email) { + await onSignatureComplete(); + return; + } + + await onSignatureComplete({ + email: data.nextSigner.email.trim().toLowerCase(), + name: data.nextSigner.name?.trim() ?? '', + }); + + setShowDialog(false); + form.reset(); + } catch (err) { + console.error(err); + } + }; + return ( - - - - - - - -
- {role === RecipientRole.VIEWER && Complete Viewing} - {role === RecipientRole.SIGNER && Complete Signing} - {role === RecipientRole.APPROVER && Complete Approval} -
-
- -
- {role === RecipientRole.VIEWER && ( - - - - You are about to complete viewing " - - {documentTitle} - - ". - -
Are you sure? -
-
- )} - {role === RecipientRole.SIGNER && ( - - - - You are about to complete signing " - - {documentTitle} - - ". - -
Are you sure? -
-
- )} - {role === RecipientRole.APPROVER && ( - - - - You are about to complete approving{' '} - - "{documentTitle}" - - . - -
Are you sure? -
-
- )} -
- - - - -
+ <> + {!canModifyNextSigner ? ( + + - - -
-
-
-
+ + + + +
+ {role === RecipientRole.VIEWER && Complete Viewing} + {role === RecipientRole.SIGNER && Complete Signing} + {role === RecipientRole.APPROVER && Complete Approval} +
+
+ +
+ {role === RecipientRole.VIEWER && ( + + + + You are about to complete viewing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.SIGNER && ( + + + + You are about to complete signing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.APPROVER && ( + + + + You are about to complete approving{' '} + + "{documentTitle}" + + . + +
Are you sure? +
+
+ )} +
+ + + + +
+ + + +
+
+
+ + ) : ( + { + if (open) setStep(1); + }} + > + + + + + + {step === 1 && ( +
+ Modify Next Signer +
+ )} + + {step === 2 && ( +
+ {role === RecipientRole.VIEWER && Complete Viewing} + {role === RecipientRole.SIGNER && Complete Signing} + {role === RecipientRole.APPROVER && Complete Approval} +
+ )} +
+ + {step === 1 && ( +
+ + ( + + + Next Signer Email + + + + + + + )} + /> + + ( + + + Next Signer Name + + + + + + + )} + /> + + + )} + + {step === 2 && ( + <> +
+ {role === RecipientRole.VIEWER && ( + + + + You are about to complete viewing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.SIGNER && ( + + + + You are about to complete signing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.APPROVER && ( + + + + You are about to complete approving{' '} + + "{documentTitle}" + + . + +
Are you sure? +
+
+ )} +
+ + + + )} + +
+
+ {[...Array(totalSteps)].map((_, index) => ( +
+ + + + + + {step === 1 && ( + + )} + + {step === 2 && ( + + )} + +
+
+
+ )} + ); -}; +} diff --git a/apps/web/src/app/(signing)/sign/[token]/step-sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/step-sign-dialog.tsx new file mode 100644 index 000000000..cf765d90e --- /dev/null +++ b/apps/web/src/app/(signing)/sign/[token]/step-sign-dialog.tsx @@ -0,0 +1,413 @@ +'use client'; + +import { useMemo, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { Trans } from '@lingui/macro'; +import { ArrowRight } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers'; +import type { Field } from '@documenso/prisma/client'; +import { RecipientRole } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; + +import { SigningDisclosure } from '~/components/general/signing-disclosure'; + +export type StepSignDialogProps = { + isSubmitting: boolean; + documentTitle: string; + fields: Field[]; + fieldsValidated: () => void | Promise; + onSignatureComplete: (nextSigner?: { email: string; name: string }) => void | Promise; + role: RecipientRole; + disabled?: boolean; + canModifyNextSigner?: boolean; +}; + +const formSchema = z.object({ + nextSigner: z + .object({ + email: z.string().email({ message: 'Please enter a valid email address' }).optional(), + name: z.string().min(1, { message: 'Name is required' }).optional(), + }) + .refine( + (data) => { + if (data.name) { + return !!data.email; + } + return true; + }, + { + message: 'Email is required when name is provided', + path: ['email'], + }, + ), +}); + +type TFormSchema = z.infer; + +export default function StepSignDialog({ + isSubmitting, + documentTitle, + fields, + fieldsValidated, + onSignatureComplete, + role, + disabled = false, + canModifyNextSigner = false, +}: StepSignDialogProps) { + const [step, setStep] = useState(1); + const [showDialog, setShowDialog] = useState(false); + + const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]); + + const handleOpenChange = (open: boolean) => { + if (isSubmitting || !isComplete) { + return; + } + + setShowDialog(open); + }; + + const totalSteps = 2; + + const handleContinue = () => { + if (step < totalSteps) { + setStep(step + 1); + } + }; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + nextSigner: { + email: '', + name: '', + }, + }, + }); + + const onFormSubmit = async (data: TFormSchema) => { + try { + await fieldsValidated(); + + if (!canModifyNextSigner || !data.nextSigner.email) { + await onSignatureComplete(); + return; + } + + await onSignatureComplete({ + email: data.nextSigner.email.trim().toLowerCase(), + name: data.nextSigner.name?.trim() ?? '', + }); + + setShowDialog(false); + form.reset(); + } catch (err) { + console.error(err); + } + }; + + return ( + <> + {!canModifyNextSigner ? ( + + + + + + + +
+ {role === RecipientRole.VIEWER && Complete Viewing} + {role === RecipientRole.SIGNER && Complete Signing} + {role === RecipientRole.APPROVER && Complete Approval} +
+
+ +
+ {role === RecipientRole.VIEWER && ( + + + + You are about to complete viewing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.SIGNER && ( + + + + You are about to complete signing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.APPROVER && ( + + + + You are about to complete approving{' '} + + "{documentTitle}" + + . + +
Are you sure? +
+
+ )} +
+ + + + +
+ + + +
+
+
+
+ ) : ( + { + if (open) setStep(1); + }} + > + + + + + + {step === 1 && ( +
+ Modify Next Signer +
+ )} + + {step === 2 && ( +
+ {role === RecipientRole.VIEWER && Complete Viewing} + {role === RecipientRole.SIGNER && Complete Signing} + {role === RecipientRole.APPROVER && Complete Approval} +
+ )} +
+ + {step === 1 && ( +
+ + ( + + + Next Signer Email + + + + + + + )} + /> + + ( + + + Next Signer Name + + + + + + + )} + /> + + + )} + + {step === 2 && ( + <> +
+ {role === RecipientRole.VIEWER && ( + + + + You are about to complete viewing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.SIGNER && ( + + + + You are about to complete signing " + + {documentTitle} + + ". + +
Are you sure? +
+
+ )} + {role === RecipientRole.APPROVER && ( + + + + You are about to complete approving{' '} + + "{documentTitle}" + + . + +
Are you sure? +
+
+ )} +
+ + + + )} + +
+
+ {[...Array(totalSteps)].map((_, index) => ( +
+ + + + + + {step === 1 && ( + + )} + + {step === 2 && ( + + )} + +
+
+
+ )} + + ); +} diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts index bec1a6404..ceef8e13d 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -28,6 +28,7 @@ export type CreateDocumentMetaOptions = { distributionMethod?: DocumentDistributionMethod; typedSignatureEnabled?: boolean; language?: SupportedLanguageCodes; + modifyNextSigner?: boolean; requestMetadata: ApiRequestMetadata; }; @@ -46,6 +47,7 @@ export const upsertDocumentMeta = async ({ distributionMethod, typedSignatureEnabled, language, + modifyNextSigner, requestMetadata, }: CreateDocumentMetaOptions) => { const document = await prisma.document.findFirst({ @@ -98,6 +100,7 @@ export const upsertDocumentMeta = async ({ distributionMethod, typedSignatureEnabled, language, + modifyNextSigner, }, update: { subject, @@ -111,6 +114,7 @@ export const upsertDocumentMeta = async ({ distributionMethod, typedSignatureEnabled, language, + modifyNextSigner, }, }); diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 8db7a7abe..9710652d1 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -28,6 +28,10 @@ export type CompleteDocumentWithTokenOptions = { userId?: number; authOptions?: TRecipientActionAuth; requestMetadata?: RequestMetadata; + nextSigner?: { + email: string; + name: string; + }; }; const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => { @@ -51,11 +55,56 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio }); }; +export const delegateNextSigner = async ({ + documentId, + currentRecipientId, + nextSigner, +}: { + documentId: number; + currentRecipientId: number; + nextSigner: { email: string; name: string }; +}) => { + const document = await prisma.document.findUnique({ + where: { id: documentId }, + include: { + recipients: { + orderBy: [{ signingOrder: 'asc' }, { id: 'asc' }], + }, + }, + }); + + if (!document) { + throw new Error('Document not found'); + } + + const currentRecipient = document.recipients.find((r) => r.id === currentRecipientId); + const nextRecipient = document.recipients.find( + (r) => r.signingOrder === (currentRecipient?.signingOrder ?? 0) + 1, + ); + + if (!nextRecipient) { + throw new Error('Next recipient not found'); + } + + await prisma.recipient.update({ + where: { id: nextRecipient.id }, + data: { + email: nextSigner.email, + name: nextSigner.name, + }, + }); + + return nextRecipient; +}; + export const completeDocumentWithToken = async ({ token, documentId, requestMetadata, + nextSigner, }: CompleteDocumentWithTokenOptions) => { + console.log('completeDocumentWithToken == document-router', token, documentId, nextSigner); + const document = await getDocument({ token, documentId }); if (document.status !== DocumentStatus.PENDING) { @@ -112,6 +161,20 @@ export const completeDocumentWithToken = async ({ // throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values'); // } + if ( + nextSigner && + document.documentMeta?.modifyNextSigner && + document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL + ) { + console.log('delegateNextSigner == document-router', document.id, recipient.id, nextSigner); + + await delegateNextSigner({ + documentId: document.id, + currentRecipientId: recipient.id, + nextSigner, + }); + } + await prisma.$transaction(async (tx) => { await tx.recipient.update({ where: { diff --git a/packages/lib/types/document.ts b/packages/lib/types/document.ts index 830842e83..9c1f525f1 100644 --- a/packages/lib/types/document.ts +++ b/packages/lib/types/document.ts @@ -55,6 +55,7 @@ export const ZDocumentSchema = DocumentSchema.pick({ typedSignatureEnabled: true, language: true, emailSettings: true, + modifyNextSigner: true, }).nullable(), recipients: ZRecipientLiteSchema.array(), fields: ZFieldSchema.array(), diff --git a/packages/prisma/migrations/20250213164804_add_modify_next_signer_meta/migration.sql b/packages/prisma/migrations/20250213164804_add_modify_next_signer_meta/migration.sql new file mode 100644 index 000000000..52dc880b4 --- /dev/null +++ b/packages/prisma/migrations/20250213164804_add_modify_next_signer_meta/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "DocumentMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 0cdb1521e..437904988 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -390,6 +390,7 @@ model DocumentMeta { document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) redirectUrl String? signingOrder DocumentSigningOrder @default(PARALLEL) + modifyNextSigner Boolean @default(false) typedSignatureEnabled Boolean @default(true) language String @default("en") distributionMethod DocumentDistributionMethod @default(EMAIL) diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 2a151b7c1..73bb2a295 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -266,15 +266,14 @@ export const documentRouter = router({ /** * @public - * - * Todo: Refactor to updateDocument. */ - setSettingsForDocument: authenticatedProcedure + updateDocument: authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/document/update', summary: 'Update document', + description: 'Update an existing document', tags: ['Document'], }, }) @@ -286,9 +285,9 @@ export const documentRouter = router({ const userId = ctx.user.id; - if (Object.values(meta).length > 0) { + if (Object.keys(meta).length > 0) { await upsertDocumentMeta({ - userId: ctx.user.id, + userId, teamId, documentId, subject: meta.subject, @@ -301,6 +300,7 @@ export const documentRouter = router({ distributionMethod: meta.distributionMethod, signingOrder: meta.signingOrder, emailSettings: meta.emailSettings, + modifyNextSigner: meta.modifyNextSigner, requestMetadata: ctx.metadata, }); } diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 887bc48e2..769117016 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -251,6 +251,7 @@ export const ZUpdateDocumentRequestSchema = z.object({ language: ZDocumentMetaLanguageSchema.optional(), typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), emailSettings: ZDocumentEmailSettingsSchema.optional(), + modifyNextSigner: z.boolean().optional(), }) .optional(), }); diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index b323ba5f0..4637aba59 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -437,13 +437,22 @@ export const recipientRouter = router({ completeDocumentWithToken: procedure .input(ZCompleteDocumentWithTokenMutationSchema) .mutation(async ({ input, ctx }) => { - const { token, documentId, authOptions } = input; + const { token, documentId, authOptions, nextSigner } = input; + + console.log( + 'completeDocumentWithToken == recipient-router', + token, + documentId, + authOptions, + nextSigner, + ); return await completeDocumentWithToken({ token, documentId, authOptions, userId: ctx.user?.id, + nextSigner, requestMetadata: extractNextApiRequestMetadata(ctx.req), }); }), diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index 7e05eb002..3fc877ebe 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -212,6 +212,12 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({ token: z.string(), documentId: z.number(), authOptions: ZRecipientActionAuthSchema.optional(), + nextSigner: z + .object({ + email: z.string().email(), + name: z.string(), + }) + .optional(), }); export type TCompleteDocumentWithTokenMutationSchema = z.infer< diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index 5ece17491..ba3cc5d7a 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -49,6 +49,7 @@ export type AddSignersFormProps = { recipients: Recipient[]; fields: Field[]; signingOrder?: DocumentSigningOrder | null; + modifyNextSigner?: boolean | null; isDocumentEnterprise: boolean; onSubmit: (_data: TAddSignersFormSchema) => void; isDocumentPdfLoaded: boolean; @@ -59,6 +60,7 @@ export const AddSignersFormPartial = ({ recipients, fields, signingOrder, + modifyNextSigner, isDocumentEnterprise, onSubmit, isDocumentPdfLoaded, @@ -107,6 +109,7 @@ export const AddSignersFormPartial = ({ ) : defaultRecipients, signingOrder: signingOrder || DocumentSigningOrder.PARALLEL, + modifyNextSigner: modifyNextSigner ?? false, }, }); @@ -404,6 +407,35 @@ export const AddSignersFormPartial = ({ )} /> + + {isSigningOrderSequential && ( + ( + + + { + field.onChange(checked); + }} + disabled={isSubmitting || hasDocumentBeenSent} + /> + + + + Modify next signer + + + )} + /> + )} + {