mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: dictate next signer (#1719)
Adds next recipient dictation functionality to document signing flow, allowing assistants and signers to update the next recipient's information during the signing process. ## Related Issue N/A ## Changes Made - Added form handling for next recipient dictation in signing dialogs - Implemented UI for updating next recipient information - Added e2e tests covering dictation scenarios: - Regular signing with dictation enabled - Assistant role with dictation - Parallel signing flow - Disabled dictation state ## Testing Performed - Added comprehensive e2e tests covering: - Sequential signing with dictation - Assistant role dictation - Parallel signing without dictation - Form validation and state management - Tested on Chrome and Firefox - Verified recipient state updates in database
This commit is contained in:
@ -9,7 +9,7 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, Plus, Trash } from 'lucide-react';
|
||||
import { GripVerticalIcon, HelpCircle, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
@ -29,6 +29,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { useStep } from '../stepper';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||
import { useToast } from '../use-toast';
|
||||
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||
import { ZAddSignersFormSchema } from './add-signers.types';
|
||||
@ -48,6 +49,7 @@ export type AddSignersFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
isDocumentEnterprise: boolean;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
@ -58,6 +60,7 @@ export const AddSignersFormPartial = ({
|
||||
recipients,
|
||||
fields,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
isDocumentEnterprise,
|
||||
onSubmit,
|
||||
isDocumentPdfLoaded,
|
||||
@ -104,6 +107,7 @@ export const AddSignersFormPartial = ({
|
||||
)
|
||||
: defaultRecipients,
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -354,6 +358,7 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
@ -389,6 +394,11 @@ export const AddSignersFormPartial = ({
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
);
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
@ -403,6 +413,50 @@ export const AddSignersFormPartial = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowDictateNextSigner"
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={isSubmitting || hasDocumentBeenSent || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
htmlFor="allowDictateNextSigner"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Allow signers to dictate next signer</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-80 p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
When enabled, signers can choose who should sign next in the sequence
|
||||
instead of following the predefined order.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DragDropContext
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={[
|
||||
|
||||
@ -25,6 +25,7 @@ export const ZAddSignersFormSchema = z
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean().default(false),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
|
||||
@ -9,7 +9,7 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { TemplateDirectLink } from '@prisma/client';
|
||||
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
@ -47,10 +47,11 @@ export type AddTemplatePlaceholderRecipientsFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
templateDirectLink: TemplateDirectLink | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
templateDirectLink?: TemplateDirectLink | null;
|
||||
isEnterprise: boolean;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
};
|
||||
|
||||
export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
@ -60,6 +61,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
templateDirectLink,
|
||||
fields,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
isDocumentPdfLoaded,
|
||||
onSubmit,
|
||||
}: AddTemplatePlaceholderRecipientsFormProps) => {
|
||||
@ -112,6 +114,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
defaultValues: {
|
||||
signers: generateDefaultFormSigners(),
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@ -119,6 +122,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
form.reset({
|
||||
signers: generateDefaultFormSigners(),
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -377,6 +381,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
@ -416,6 +421,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
);
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
@ -431,6 +441,49 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowDictateNextSigner"
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={isSubmitting || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
htmlFor="allowDictateNextSigner"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Allow signers to dictate next signer</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-80 p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
When enabled, signers can choose who should sign next in the sequence
|
||||
instead of following the predefined order.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Drag and drop context */}
|
||||
<DragDropContext
|
||||
onDragEnd={onDragEnd}
|
||||
|
||||
@ -21,6 +21,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean().default(false),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
|
||||
Reference in New Issue
Block a user