mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +10:00
feat: add empty emails for envelopes
This commit is contained in:
+17
-17
@@ -57,7 +57,7 @@ export type DocumentSigningCompleteDialogProps = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
directTemplatePayload?: {
|
||||
recipientPayload?: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
@@ -89,7 +89,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
recipient,
|
||||
disabled = false,
|
||||
allowDictateNextSigner = false,
|
||||
directTemplatePayload,
|
||||
recipientPayload,
|
||||
defaultNextSigner,
|
||||
buttonSize = 'lg',
|
||||
position,
|
||||
@@ -113,11 +113,11 @@ export const DocumentSigningCompleteDialog = ({
|
||||
},
|
||||
});
|
||||
|
||||
const directRecipientForm = useForm<TDirectRecipientFormSchema>({
|
||||
const recipientForm = useForm<TDirectRecipientFormSchema>({
|
||||
resolver: zodResolver(ZDirectRecipientFormSchema),
|
||||
defaultValues: {
|
||||
name: directTemplatePayload?.name ?? '',
|
||||
email: directTemplatePayload?.email ?? '',
|
||||
name: recipientPayload?.name ?? '',
|
||||
email: recipientPayload?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,16 +145,16 @@ export const DocumentSigningCompleteDialog = ({
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
try {
|
||||
let directRecipient: { name: string; email: string } | undefined;
|
||||
let recipientOverridePayload: { name: string; email: string } | undefined;
|
||||
|
||||
if (directTemplatePayload && !directTemplatePayload.email) {
|
||||
const isFormValid = await directRecipientForm.trigger();
|
||||
if (recipientPayload && !recipientPayload.email) {
|
||||
const isFormValid = await recipientForm.trigger();
|
||||
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
directRecipient = directRecipientForm.getValues();
|
||||
recipientOverridePayload = recipientForm.getValues();
|
||||
}
|
||||
|
||||
// Check if 2FA is required
|
||||
@@ -168,7 +168,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
? { name: data.name, email: data.email }
|
||||
: undefined;
|
||||
|
||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, directRecipient);
|
||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, recipientOverridePayload);
|
||||
} catch (error) {
|
||||
const err = AppError.parseError(error);
|
||||
|
||||
@@ -222,7 +222,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
<Trans>Are you sure?</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
<div className="max-w-[50ch] text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<span className="inline-flex flex-wrap">
|
||||
@@ -250,19 +250,19 @@ export const DocumentSigningCompleteDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="border-border bg-muted/50 rounded-lg border p-4 text-center">
|
||||
<p className="text-muted-foreground text-sm font-medium">{documentTitle}</p>
|
||||
<div className="rounded-lg border border-border bg-muted/50 p-4 text-center">
|
||||
<p className="text-sm font-medium text-muted-foreground">{documentTitle}</p>
|
||||
</div>
|
||||
|
||||
{!showTwoFactorForm && (
|
||||
<>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||
{directTemplatePayload && !directTemplatePayload.email && (
|
||||
<Form {...directRecipientForm}>
|
||||
{recipientPayload && !recipientPayload.email && (
|
||||
<Form {...recipientForm}>
|
||||
<div className="mb-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={directRecipientForm.control}
|
||||
control={recipientForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
@@ -284,7 +284,7 @@ export const DocumentSigningCompleteDialog = ({
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={directRecipientForm.control}
|
||||
control={recipientForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
|
||||
+14
-32
@@ -8,7 +8,6 @@ import {
|
||||
type SensorAPI,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
ZRecipientActionAuthTypesSchema,
|
||||
ZRecipientAuthOptionsSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -65,10 +65,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
id: z.number().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: msg`Invalid email`.id })
|
||||
.min(1),
|
||||
email: ZRecipientEmailSchema,
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
@@ -201,12 +198,13 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
keyName: 'nativeId',
|
||||
});
|
||||
|
||||
const emptySigners = useCallback(
|
||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
||||
[form],
|
||||
const emptySignerIndex = watchedSigners.findIndex(
|
||||
(signer) =>
|
||||
!signer.name &&
|
||||
!signer.email &&
|
||||
envelope.fields.filter((field) => field.recipientId === signer.id).length === 0,
|
||||
);
|
||||
|
||||
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
|
||||
const isUserAlreadyARecipient = watchedSigners.some(
|
||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||
);
|
||||
@@ -460,21 +458,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const formValueSigners = formValues.signers || [];
|
||||
|
||||
// Remove the last signer if it's empty.
|
||||
const nonEmptyRecipients = formValueSigners.filter((signer, i) => {
|
||||
if (i === formValueSigners.length - 1 && signer.email === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
|
||||
...formValues,
|
||||
signers: nonEmptyRecipients,
|
||||
});
|
||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||
|
||||
if (!validatedFormValues.success) {
|
||||
return;
|
||||
@@ -570,7 +554,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
<CardContent>
|
||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||
<Form {...form}>
|
||||
<div className="bg-accent/50 -mt-2 mb-2 space-y-4 rounded-md p-4">
|
||||
<div className="-mt-2 mb-2 space-y-4 rounded-md bg-accent/50 p-4">
|
||||
{organisation.organisationClaim.flags.cfr21 && (
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
@@ -618,9 +602,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
||||
}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -634,7 +616,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<span className="ml-1 cursor-help text-muted-foreground">
|
||||
<HelpCircleIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -679,7 +661,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<span className="ml-1 cursor-help text-muted-foreground">
|
||||
<HelpCircleIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -732,7 +714,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={cn('py-1', {
|
||||
'bg-widget-foreground pointer-events-none rounded-md pt-2':
|
||||
'pointer-events-none rounded-md bg-widget-foreground pt-2':
|
||||
snapshot.isDragging,
|
||||
})}
|
||||
>
|
||||
@@ -806,7 +788,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
||||
})}
|
||||
>
|
||||
{!showAdvancedSettings && index === 0 && (
|
||||
<FormLabel required>
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
@@ -41,6 +41,11 @@ export const EnvelopeRecipientSelector = ({
|
||||
}: EnvelopeRecipientSelectorProps) => {
|
||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||
|
||||
const getRecipientLabel = useCallback(
|
||||
(recipient: Recipient) => extractRecipientLabel(recipient, recipients),
|
||||
[recipients],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -49,7 +54,7 @@ export const EnvelopeRecipientSelector = ({
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
||||
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||
getRecipientColorStyles(
|
||||
Math.max(
|
||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||
@@ -59,16 +64,12 @@ export const EnvelopeRecipientSelector = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedRecipient?.email && (
|
||||
{selectedRecipient && (
|
||||
<span className="flex-1 truncate text-left">
|
||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
||||
{getRecipientLabel(selectedRecipient)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!selectedRecipient?.email && (
|
||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -154,6 +155,11 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
[fields, recipients],
|
||||
);
|
||||
|
||||
const getRecipientLabel = useCallback(
|
||||
(recipient: Recipient) => extractRecipientLabel(recipient, recipients),
|
||||
[recipients],
|
||||
);
|
||||
|
||||
return (
|
||||
<Command
|
||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||
@@ -162,21 +168,21 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
<CommandInput placeholder={placeholder} />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
<span className="inline-block px-4 text-muted-foreground">
|
||||
<Trans>No recipient matching this description was found.</Trans>
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||
</div>
|
||||
|
||||
{roleRecipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||
>
|
||||
<Trans>No recipients with this role</Trans>
|
||||
</div>
|
||||
@@ -205,18 +211,12 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
className={cn('truncate text-foreground/70', {
|
||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||
'opacity-50': isRecipientDisabled(recipient.id),
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
||||
{getRecipientLabel(recipient)}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
@@ -234,7 +234,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
<Info className="z-50 ml-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||
<Trans>
|
||||
This document has already been sent to this recipient. You can no longer
|
||||
edit this recipient.
|
||||
@@ -250,3 +250,22 @@ export const EnvelopeRecipientSelectorCommand = ({
|
||||
</Command>
|
||||
);
|
||||
};
|
||||
|
||||
const extractRecipientLabel = (recipient: Recipient, recipients: Recipient[]) => {
|
||||
if (recipient.name && recipient.email) {
|
||||
return `${recipient.name} (${recipient.email})`;
|
||||
}
|
||||
|
||||
if (recipient.name) {
|
||||
return recipient.name;
|
||||
}
|
||||
|
||||
if (recipient.email) {
|
||||
return recipient.email;
|
||||
}
|
||||
|
||||
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
|
||||
const index = recipients.indexOf(recipient);
|
||||
|
||||
return `Recipient ${index + 1}`;
|
||||
};
|
||||
|
||||
+14
-3
@@ -80,12 +80,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
const handleOnCompleteClick = async (
|
||||
nextSigner?: { name: string; email: string },
|
||||
accessAuthOptions?: TRecipientAccessAuth,
|
||||
recipientDetails?: { name: string; email: string },
|
||||
) => {
|
||||
try {
|
||||
await completeDocument({
|
||||
token: recipient.token,
|
||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||
accessAuthOptions,
|
||||
recipientOverride: recipientDetails,
|
||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||
});
|
||||
|
||||
@@ -205,9 +207,18 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const directTemplatePayload = useMemo(() => {
|
||||
const recipientPayload = useMemo(() => {
|
||||
if (!isDirectTemplate) {
|
||||
return;
|
||||
return {
|
||||
name:
|
||||
recipient.name ||
|
||||
recipient.fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||
'',
|
||||
email:
|
||||
recipient.email ||
|
||||
recipient.fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||
'',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -219,7 +230,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
return (
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isPending}
|
||||
directTemplatePayload={directTemplatePayload}
|
||||
recipientPayload={recipientPayload}
|
||||
onSignatureComplete={
|
||||
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user