Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo, useRef, useState } from 'react';

import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
import { motion } from 'framer-motion';
import {
ArrowLeftIcon,
Expand All @@ -10,6 +10,7 @@ import {
PanelLeftCloseIcon,
PanelLeftOpenIcon,
PaperclipIcon,
RotateCcwIcon,
} from 'lucide-react';
import { Link } from 'react-router';
import { match } from 'ts-pattern';
Expand Down Expand Up @@ -41,6 +42,7 @@ import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
import { EnvelopeSignerHeader } from '../envelope-signing/envelope-signer-header';
import { DocumentSigningMobileWidget } from './document-signing-mobile-widget';
import { DocumentSigningRejectDialog } from './document-signing-reject-dialog';
import { DocumentSigningSendBackDialog } from './document-signing-send-back-dialog';
import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';

export const DocumentSigningPageViewV2 = () => {
Expand All @@ -50,6 +52,7 @@ export const DocumentSigningPageViewV2 = () => {

const {
isDirectTemplate,
envelopeData,
envelope,
recipient,
recipientFields,
Expand Down Expand Up @@ -81,6 +84,22 @@ export const DocumentSigningPageViewV2 = () => {
return recipientFields.filter((field) => !field.inserted);
}, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]);

/**
* Recipients who have already completed their part, and can therefore be sent the
* document back to for correction.
*/
const earlierRecipients = useMemo(
() =>
envelope.recipients.filter(
(candidate) =>
candidate.id !== recipient.id &&
candidate.signingStatus === SigningStatus.SIGNED &&
candidate.role !== RecipientRole.CC &&
candidate.role !== RecipientRole.VIEWER,
),
[envelope.recipients, recipient.id],
);

return (
<div className="min-h-screen w-screen bg-gray-50 dark:bg-background">
<SignFieldEmailDialog.Root />
Expand Down Expand Up @@ -227,6 +246,21 @@ export const DocumentSigningPageViewV2 = () => {
}
/>
)}

{envelope.type === EnvelopeType.DOCUMENT && allowDocumentRejection && (
<DocumentSigningSendBackDialog
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
token={recipient.token}
senderName={envelopeData.sender.name || envelopeData.sender.email}
earlierRecipients={earlierRecipients}
trigger={
<Button variant="ghost" size="sm" className="w-full justify-start">
<RotateCcwIcon className="mr-2 h-4 w-4" />
<Trans>Send Back for Correction</Trans>
</Button>
}
/>
)}
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react';

import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { useToast } from '@documenso/ui/primitives/use-toast';

const SEND_BACK_TO_SENDER_VALUE = 'sender';

const ZSendBackFormSchema = z.object({
target: z.string().min(1, msg`Please select who to send the document back to`),
reason: z
.string()
.min(1, msg`Please provide a reason`)
.max(500, msg`Reason must be less than 500 characters`),
});

type TSendBackFormSchema = z.infer<typeof ZSendBackFormSchema>;

export interface DocumentSigningSendBackDialogProps {
documentId: number;
token: string;
senderName: string;
earlierRecipients: { id: number; name: string; email: string }[];
onSentBack?: () => void | Promise<void>;
trigger?: React.ReactNode;
}

export function DocumentSigningSendBackDialog({
documentId,
token,
senderName,
earlierRecipients,
onSentBack,
trigger,
}: DocumentSigningSendBackDialogProps) {
const { t } = useLingui();
const { toast } = useToast();

const [isOpen, setIsOpen] = useState(false);

const { mutateAsync: sendDocumentBackForCorrectionWithToken } =
trpc.recipient.sendDocumentBackForCorrectionWithToken.useMutation();

const form = useForm<TSendBackFormSchema>({
resolver: zodResolver(ZSendBackFormSchema),
defaultValues: {
target: '',
reason: '',
},
});

const onSendBack = async ({ target, reason }: TSendBackFormSchema) => {
try {
await sendDocumentBackForCorrectionWithToken({
documentId,
token,
targetRecipientId: target === SEND_BACK_TO_SENDER_VALUE ? null : Number(target),
reason,
});

toast({
title: t`Document sent back`,
description: t`The document has been sent back for correction.`,
duration: 5000,
});

setIsOpen(false);

await onSentBack?.();
} catch (err) {
toast({
title: t`Error`,
description: t`An error occurred while sending the document back. Please try again.`,
variant: 'destructive',
duration: 5000,
});
}
};

useEffect(() => {
if (!isOpen) {
form.reset();
}
}, [isOpen]);

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="outline">
<Trans>Send Back for Correction</Trans>
</Button>
)}
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans>Send Back for Correction</Trans>
</DialogTitle>

<DialogDescription>
<Trans>
Send this document back to an earlier recipient (or the sender) to fix a mistake.
Their previously collected fields will be cleared so they can redo them; everyone
else's information is left as-is.
</Trans>
</DialogDescription>
</DialogHeader>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSendBack)} className="space-y-4">
<FormField
control={form.control}
name="target"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Send back to</Trans>
</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t`Choose a recipient`} />
</SelectTrigger>
<SelectContent>
{earlierRecipients.map((recipient) => (
<SelectItem key={recipient.id} value={String(recipient.id)}>
{recipient.name || recipient.email}
</SelectItem>
))}
<SelectItem value={SEND_BACK_TO_SENDER_VALUE}>
{t`The sender (${senderName})`}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="reason"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Reason</Trans>
</FormLabel>
<FormControl>
<Textarea
{...field}
rows={4}
placeholder={t`Explain what needs to be corrected`}
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setIsOpen(false)}
disabled={form.formState.isSubmitting}
>
<Trans>Cancel</Trans>
</Button>

<Button
type="submit"
loading={form.formState.isSubmitting}
disabled={!form.formState.isValid}
>
<Trans>Send Back</Trans>
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
MailOpenIcon,
PenIcon,
PlusIcon,
RotateCcwIcon,
UserIcon,
} from 'lucide-react';
import { DateTime } from 'luxon';
Expand All @@ -28,6 +29,7 @@ import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { PopoverHover } from '@documenso/ui/primitives/popover';
import {
Tooltip,
Expand All @@ -37,6 +39,8 @@ import {
} from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';

import { DocumentSendBackForCorrectionDialog } from './document-send-back-for-correction-dialog';

export type DocumentPageViewRecipientsProps = {
envelope: TEnvelope;
documentRootPath: string;
Expand Down Expand Up @@ -155,6 +159,38 @@ export const DocumentPageViewRecipients = ({
</Badge>
)}

{envelope.status === DocumentStatus.PENDING &&
recipient.signingStatus === SigningStatus.SIGNED &&
recipient.role !== RecipientRole.CC &&
recipient.role !== RecipientRole.VIEWER && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-1 inline-flex">
<DocumentSendBackForCorrectionDialog
envelopeId={envelope.id}
recipientId={recipient.id}
recipientName={recipient.name || recipient.email}
trigger={
<Button
variant="ghost"
className="h-6 w-6 p-0"
aria-label={_(msg`Send back for correction`)}
>
<RotateCcwIcon className="h-3.5 w-3.5" />
</Button>
}
/>
</span>
</TooltipTrigger>
<TooltipContent sideOffset={2}>
<Trans>Send back for correction</Trans>
<TooltipArrow className="fill-background" />
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}

{envelope.status !== DocumentStatus.DRAFT &&
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
isRecipientExpired(recipient) && (
Expand Down
Loading
Loading