feat: implement auto-save functionality for signers in document edit form (#1792)

This commit is contained in:
Catalin Pit
2025-09-02 14:01:16 +03:00
committed by GitHub
parent 19565c1821
commit bb5c2edefd
22 changed files with 2482 additions and 220 deletions

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { useCallback, useId, useMemo, useRef, useState } from 'react';
import type { DropResult, SensorAPI } from '@hello-pangea/dnd';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
@ -12,6 +12,7 @@ import { motion } from 'framer-motion';
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
@ -55,6 +56,7 @@ export type AddTemplatePlaceholderRecipientsFormProps = {
allowDictateNextSigner?: boolean;
templateDirectLink?: TemplateDirectLink | null;
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
onAutoSave: (_data: TAddTemplatePlacholderRecipientsFormSchema) => Promise<void>;
isDocumentPdfLoaded: boolean;
};
@ -67,6 +69,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
allowDictateNextSigner,
isDocumentPdfLoaded,
onSubmit,
onAutoSave,
}: AddTemplatePlaceholderRecipientsFormProps) => {
const initialId = useId();
const $sensorApi = useRef<SensorAPI | null>(null);
@ -123,15 +126,38 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
},
});
useEffect(() => {
form.reset({
signers: generateDefaultFormSigners(),
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
allowDictateNextSigner: allowDictateNextSigner ?? false,
});
const emptySigners = useCallback(
() => form.getValues('signers').filter((signer) => signer.email === ''),
[form],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [recipients]);
const { scheduleSave } = useAutoSave(onAutoSave);
const handleAutoSave = async () => {
if (emptySigners().length > 0) {
return;
}
const isFormValid = await form.trigger();
if (!isFormValid) {
return;
}
const formData = form.getValues();
scheduleSave(formData);
};
// useEffect(() => {
// form.reset({
// signers: generateDefaultFormSigners(),
// signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
// allowDictateNextSigner: allowDictateNextSigner ?? false,
// });
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [recipients]);
// Always show advanced settings if any recipient has auth options.
const alwaysShowAdvancedSettings = useMemo(() => {
@ -204,7 +230,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const onRemoveSigner = (index: number) => {
removeSigner(index);
const updatedSigners = signers.filter((_, idx) => idx !== index);
form.setValue('signers', normalizeSigningOrders(updatedSigners));
form.setValue('signers', normalizeSigningOrders(updatedSigners), {
shouldValidate: true,
shouldDirty: true,
});
void handleAutoSave();
};
const isSignerDirectRecipient = (
@ -231,7 +262,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: index + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
const lastSigner = updatedSigners[updatedSigners.length - 1];
if (lastSigner.role === RecipientRole.ASSISTANT) {
@ -244,8 +278,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
}
await form.trigger('signers');
void handleAutoSave();
},
[form, watchedSigners, toast],
[form, watchedSigners, toast, handleAutoSave],
);
const handleSigningOrderChange = useCallback(
@ -273,7 +309,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
toast({
@ -283,8 +322,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
),
});
}
void handleAutoSave();
},
[form, toast],
[form, toast, handleAutoSave],
);
const handleRoleChange = useCallback(
@ -294,7 +335,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
// Handle parallel to sequential conversion for assistants
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL, {
shouldValidate: true,
shouldDirty: true,
});
toast({
title: _(msg`Signing order is enabled.`),
description: _(msg`You cannot add assistants when signing order is disabled.`),
@ -309,7 +353,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signingOrder: idx + 1,
}));
form.setValue('signers', updatedSigners);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
toast({
@ -319,8 +366,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
),
});
}
void handleAutoSave();
},
[form, toast],
[form, toast, handleAutoSave],
);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
@ -334,10 +383,21 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
}));
form.setValue('signers', updatedSigners);
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
form.setValue('allowDictateNextSigner', false);
}, [form]);
form.setValue('signers', updatedSigners, {
shouldValidate: true,
shouldDirty: true,
});
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL, {
shouldValidate: true,
shouldDirty: true,
});
form.setValue('allowDictateNextSigner', false, {
shouldValidate: true,
shouldDirty: true,
});
void handleAutoSave();
}, [form, handleAutoSave]);
return (
<>
@ -382,8 +442,13 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
// If sequential signing is turned off, disable dictate next signer
if (!checked) {
form.setValue('allowDictateNextSigner', false);
form.setValue('allowDictateNextSigner', false, {
shouldValidate: true,
shouldDirty: true,
});
}
void handleAutoSave();
}}
disabled={isSubmitting}
/>
@ -409,7 +474,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{...field}
id="allowDictateNextSigner"
checked={value}
onCheckedChange={field.onChange}
onCheckedChange={(checked) => {
field.onChange(checked);
void handleAutoSave();
}}
disabled={isSubmitting || !isSigningOrderSequential}
/>
</FormControl>
@ -500,6 +568,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<Input
type="number"
max={signers.length}
data-testid="placeholder-recipient-signing-order-input"
className={cn(
'w-full text-center',
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
@ -558,6 +627,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signers[index].email === user?.email ||
isSignerDirectRecipient(signer)
}
onBlur={handleAutoSave}
data-testid="placeholder-recipient-email-input"
/>
</FormControl>
@ -592,6 +663,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
signers[index].email === user?.email ||
isSignerDirectRecipient(signer)
}
onBlur={handleAutoSave}
data-testid="placeholder-recipient-name-input"
/>
</FormControl>
@ -633,10 +706,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<FormControl>
<RecipientRoleSelect
{...field}
onValueChange={(value) =>
onValueChange={(value) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
handleRoleChange(index, value as RecipientRole)
}
handleRoleChange(index, value as RecipientRole);
}}
disabled={isSubmitting}
hideCCRecipients={isSignerDirectRecipient(signer)}
/>
@ -672,6 +745,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitting || signers.length === 1}
onClick={() => onRemoveSigner(index)}
data-testid="remove-placeholder-recipient-button"
>
<Trash className="h-5 w-5" />
</button>