mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
fix: duplicate webhook calls on document complete (#1721)
Fix webhooks being sent twice due to duplicate frontend calls Updated the assistant confirmation dialog so the next signer is always visible (if dictate is enabled). Because if the form is invalid (due to no name) there is no visual queue that the form is invalid (since it's hidden) ## Notes Didn't bother to remove the weird assistants form since it currently works for now  ## Tests - Currently running locally - Tested webhooks via network tab and via webhook.site
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@ -57,8 +55,6 @@ export function AssistantConfirmationDialog({
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: ConfirmationDialogProps) {
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: zodResolver(ZNextSignerFormSchema),
|
||||
defaultValues: {
|
||||
@ -68,7 +64,7 @@ export function AssistantConfirmationDialog({
|
||||
});
|
||||
|
||||
const onOpenChange = () => {
|
||||
if (form.formState.isSubmitting) {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -77,32 +73,25 @@ export function AssistantConfirmationDialog({
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
});
|
||||
|
||||
setIsEditingNextSigner(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onConfirm({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
});
|
||||
} else {
|
||||
await onConfirm();
|
||||
const handleSubmit = () => {
|
||||
// Validate the form and submit it if dictate signer is enabled.
|
||||
if (allowDictateNextSigner) {
|
||||
void form.handleSubmit(onConfirm)();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const isNextSignerValid = !allowDictateNextSigner || (form.watch('name') && form.watch('email'));
|
||||
onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset
|
||||
disabled={form.formState.isSubmitting || isSubmitting}
|
||||
className="border-none p-0"
|
||||
>
|
||||
<form>
|
||||
<fieldset disabled={isSubmitting} className="border-none p-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
@ -118,29 +107,12 @@ export function AssistantConfirmationDialog({
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{allowDictateNextSigner && (
|
||||
<div className="space-y-4">
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<div className="my-2">
|
||||
<p className="text-muted-foreground mb-1 text-sm font-semibold">
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditingNextSigner((prev) => !prev)}
|
||||
>
|
||||
<Trans>Update Recipient</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditingNextSigner && (
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<div className="flex flex-col gap-4 rounded-xl border p-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@ -182,7 +154,6 @@ export function AssistantConfirmationDialog({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -190,27 +161,17 @@ export function AssistantConfirmationDialog({
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<Button type="button" variant="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
type="button"
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
disabled={form.formState.isSubmitting || !isNextSignerValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<Trans>Submitting...</Trans>
|
||||
) : hasUninsertedFields ? (
|
||||
<Trans>Proceed</Trans>
|
||||
) : (
|
||||
<Trans>Continue</Trans>
|
||||
)}
|
||||
{hasUninsertedFields ? <Trans>Proceed</Trans> : <Trans>Continue</Trans>}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
@ -33,13 +31,6 @@ import {
|
||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
export const ZSigningFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').optional(),
|
||||
email: z.string().email('Invalid email address').optional(),
|
||||
});
|
||||
|
||||
export type TSigningFormSchema = z.infer<typeof ZSigningFormSchema>;
|
||||
|
||||
export type DocumentSigningFormProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: Recipient;
|
||||
@ -76,8 +67,11 @@ export const DocumentSigningForm = ({
|
||||
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
const {
|
||||
mutateAsync: completeDocumentWithToken,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
|
||||
defaultValues: {
|
||||
@ -85,12 +79,8 @@ export const DocumentSigningForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState } = useForm<TSigningFormSchema>({
|
||||
resolver: zodResolver(ZSigningFormSchema),
|
||||
});
|
||||
|
||||
// Keep the loading state going if successful since the redirect may take some time.
|
||||
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||
const isSubmitting = isPending || isSuccess;
|
||||
|
||||
const fieldsRequiringValidation = useMemo(
|
||||
() => fields.filter(isFieldUnsignedAndRequired),
|
||||
@ -112,34 +102,6 @@ export const DocumentSigningForm = ({
|
||||
validateFieldsInserted(fieldsRequiringValidation);
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TSigningFormSchema) => {
|
||||
try {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSigner =
|
||||
data.email && data.name
|
||||
? {
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await completeDocument(undefined, nextSigner);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'An error occurred while signing',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onAssistantFormSubmit = () => {
|
||||
if (uninsertedRecipientFields.length > 0) {
|
||||
return;
|
||||
@ -214,8 +176,6 @@ export const DocumentSigningForm = ({
|
||||
: undefined;
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
console.log('nextRecipient', nextRecipient);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -356,9 +316,8 @@ export const DocumentSigningForm = ({
|
||||
className="w-full"
|
||||
size="lg"
|
||||
loading={isAssistantSubmitting}
|
||||
disabled={isAssistantSubmitting || uninsertedRecipientFields.length > 0}
|
||||
>
|
||||
{isAssistantSubmitting ? <Trans>Submitting...</Trans> : <Trans>Continue</Trans>}
|
||||
<Trans>Continue</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -381,7 +340,7 @@ export const DocumentSigningForm = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<div>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? (
|
||||
<Trans>Please review the document before approving.</Trans>
|
||||
@ -463,7 +422,7 @@ export const DocumentSigningForm = ({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -341,9 +341,6 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer'
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible();
|
||||
|
||||
// Update next recipient
|
||||
await page.locator('button').filter({ hasText: 'Update Recipient' }).click();
|
||||
|
||||
// Use dialog context to ensure we're targeting the correct form fields
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByLabel('Name').fill('New Signer');
|
||||
|
||||
@ -148,6 +148,10 @@ test('[TEAMS]: check signature modes work for templates', async ({ page }) => {
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).first().click();
|
||||
|
||||
// Wait for finish
|
||||
await page.getByText('Document preferences updated').waitFor({ state: 'visible' });
|
||||
await page.getByTestId('toast-close').click();
|
||||
|
||||
const template = await seedTeamTemplateWithMeta(team);
|
||||
|
||||
await page.goto(`/t/${team.url}/templates/${template.id}`);
|
||||
|
||||
@ -5049,11 +5049,6 @@ msgstr "Schritt <0>{step} von {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Betreff <0>(Optional)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
@ -6121,7 +6116,6 @@ msgstr "Profil aktualisieren"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Empfänger aktualisieren"
|
||||
|
||||
|
||||
@ -5044,11 +5044,6 @@ msgstr "Step <0>{step} of {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Subject <0>(Optional)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr "Submitting..."
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Subscribe"
|
||||
@ -6118,7 +6113,6 @@ msgstr "Update profile"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Update Recipient"
|
||||
|
||||
|
||||
@ -5049,11 +5049,6 @@ msgstr "Paso <0>{step} de {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Asunto <0>(Opcional)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
@ -6121,7 +6116,6 @@ msgstr "Actualizar perfil"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Actualizar destinatario"
|
||||
|
||||
|
||||
@ -5049,11 +5049,6 @@ msgstr "Étape <0>{step} sur {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Objet <0>(Optionnel)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
@ -6121,7 +6116,6 @@ msgstr "Mettre à jour le profil"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Mettre à jour le destinataire"
|
||||
|
||||
|
||||
@ -5049,11 +5049,6 @@ msgstr "Passo <0>{step} di {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Oggetto <0>(Opzionale)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
@ -6121,7 +6116,6 @@ msgstr "Aggiorna profilo"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Aggiorna destinatario"
|
||||
|
||||
|
||||
@ -5049,11 +5049,6 @@ msgstr "Krok <0>{step} z {maxStep}</0>"
|
||||
msgid "Subject <0>(Optional)</0>"
|
||||
msgstr "Temat <0>(Opcjonalnie)</0>"
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Submitting..."
|
||||
msgstr ""
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr ""
|
||||
@ -6121,7 +6116,6 @@ msgstr "Zaktualizuj profil"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
msgid "Update Recipient"
|
||||
msgstr "Zaktualizuj odbiorcę"
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ export const updateDocumentRoute = authenticatedProcedure
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
@ -78,6 +78,7 @@ const ToastClose = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
data-testid="toast-close"
|
||||
className={cn(
|
||||
'text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-100 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 md:opacity-0 group-hover:md:opacity-100',
|
||||
className,
|
||||
|
||||
Reference in New Issue
Block a user