fix: refactor use template

This commit is contained in:
David Nguyen
2024-04-22 16:05:16 +07:00
parent 4b90adde6b
commit 6cba74e128
3 changed files with 171 additions and 229 deletions

View File

@ -2,11 +2,10 @@ import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import * as z from 'zod'; import * as z from 'zod';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -19,24 +18,56 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
const ZAddRecipientsForNewDocumentSchema = z.object({ const ZAddRecipientsForNewDocumentSchema = z
recipients: z.array( .object({
z.object({ recipients: z.array(
email: z.string().email(), z.object({
name: z.string(), id: z.number(),
role: z.nativeEnum(RecipientRole), email: z.string().email(),
}), name: z.string(),
), }),
}); ),
})
// Display exactly which rows are duplicates.
.superRefine((items, ctx) => {
const uniqueEmails = new Map<string, number>();
for (const [index, recipients] of items.recipients.entries()) {
const email = recipients.email.toLowerCase();
const firstFoundIndex = uniqueEmails.get(email);
if (firstFoundIndex === undefined) {
uniqueEmails.set(email, index);
continue;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Emails must be unique',
path: ['recipients', index, 'email'],
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Emails must be unique',
path: ['recipients', firstFoundIndex, 'email'],
});
}
});
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>; type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
@ -56,33 +87,18 @@ export function UseTemplateDialog({
const team = useOptionalCurrentTeam(); const team = useOptionalCurrentTeam();
const { const form = useForm<TAddRecipientsForNewDocumentSchema>({
control,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TAddRecipientsForNewDocumentSchema>({
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
defaultValues: { defaultValues: {
recipients: recipients: recipients.map((recipient) => ({
recipients.length > 0 id: recipient.id,
? recipients.map((recipient) => ({ name: recipient.name,
nativeId: recipient.id, email: recipient.email,
formId: String(recipient.id), })),
name: recipient.name,
email: recipient.email,
role: recipient.role,
}))
: [
{
name: '',
email: '',
role: RecipientRole.SIGNER,
},
],
}, },
}); });
const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } = const { mutateAsync: createDocumentFromTemplate } =
trpc.template.createDocumentFromTemplate.useMutation(); trpc.template.createDocumentFromTemplate.useMutation();
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
@ -109,10 +125,8 @@ export function UseTemplateDialog({
} }
}; };
const onCreateDocumentFromTemplate = handleSubmit(onSubmit);
const { fields: formRecipients } = useFieldArray({ const { fields: formRecipients } = useFieldArray({
control, control: form.control,
name: 'recipients', name: 'recipients',
}); });
@ -127,120 +141,65 @@ export function UseTemplateDialog({
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Document Recipients</DialogTitle> <DialogTitle>Document Recipients</DialogTitle>
<DialogDescription>Add the recipients to create the template with.</DialogDescription> <DialogDescription>Add the recipients to create the template with</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col space-y-4">
{formRecipients.map((recipient, index) => ( <Form {...form}>
<div <form onSubmit={form.handleSubmit(onSubmit)}>
key={recipient.id} <fieldset
data-native-id={recipient.id} className="flex h-full flex-col space-y-4"
className="flex flex-wrap items-end gap-x-4" disabled={form.formState.isSubmitting}
> >
<div className="flex-1"> <div className="custom-scrollbar -m-1 max-h-[60vh] space-y-4 overflow-y-auto p-1">
<Label htmlFor={`recipient-${recipient.id}-email`}> {formRecipients.map((recipient, index) => (
Email <div className="flex w-full flex-row space-x-4" key={recipient.id}>
<span className="text-destructive ml-1 inline-block font-medium">*</span> <FormField
</Label> control={form.control}
name={`recipients.${index}.email`}
render={({ field }) => (
<FormItem className="w-full">
{index === 0 && <FormLabel required>Email</FormLabel>}
<Controller <FormControl>
control={control} <Input {...field} />
name={`recipients.${index}.email`} </FormControl>
render={({ field }) => ( <FormMessage />
<Input </FormItem>
id={`recipient-${recipient.id}-email`} )}
type="email"
className="bg-background mt-2"
disabled={isSubmitting}
{...field}
/> />
)}
/>
</div>
<div className="flex-1"> <FormField
<Label htmlFor={`recipient-${recipient.id}-name`}>Name</Label> control={form.control}
name={`recipients.${index}.name`}
render={({ field }) => (
<FormItem className="w-full">
{index === 0 && <FormLabel>Name</FormLabel>}
<Controller <FormControl>
control={control} <Input {...field} />
name={`recipients.${index}.name`} </FormControl>
render={({ field }) => ( <FormMessage />
<Input </FormItem>
id={`recipient-${recipient.id}-name`} )}
type="text"
className="bg-background mt-2"
disabled={isSubmitting}
{...field}
/> />
)} </div>
/> ))}
</div> </div>
<div className="w-[60px]"> <DialogFooter>
<Controller <DialogClose asChild>
control={control} <Button type="button" variant="secondary">
name={`recipients.${index}.role`} Close
render={({ field: { value, onChange } }) => ( </Button>
<Select value={value} onValueChange={(x) => onChange(x)}> </DialogClose>
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
<SelectContent className="" align="end"> <Button type="submit" loading={form.formState.isSubmitting}>
<SelectItem value={RecipientRole.SIGNER}> Create Document
<div className="flex items-center"> </Button>
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span> </DialogFooter>
Signer </fieldset>
</div> </form>
</SelectItem> </Form>
<SelectItem value={RecipientRole.CC}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
Receives copy
</div>
</SelectItem>
<SelectItem value={RecipientRole.APPROVER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
Approver
</div>
</SelectItem>
<SelectItem value={RecipientRole.VIEWER}>
<div className="flex items-center">
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
Viewer
</div>
</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="w-full">
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.email} />
<FormErrorMessage className="mt-2" error={errors.recipients?.[index]?.name} />
</div>
</div>
))}
</div>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
</Button>
</DialogClose>
<Button
type="button"
loading={isCreatingDocumentFromTemplate}
disabled={isCreatingDocumentFromTemplate}
onClick={onCreateDocumentFromTemplate}
>
Create Document
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -1,15 +1,14 @@
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { RecipientRole } from '@documenso/prisma/client';
export type CreateDocumentFromTemplateOptions = { export type CreateDocumentFromTemplateOptions = {
templateId: number; templateId: number;
userId: number; userId: number;
teamId?: number; teamId?: number;
recipients?: { recipients: {
id: number;
name?: string; name?: string;
email: string; email: string;
role?: RecipientRole;
}[]; }[];
}; };
@ -49,6 +48,20 @@ export const createDocumentFromTemplate = async ({
throw new Error('Template not found.'); throw new Error('Template not found.');
} }
const finalRecipients = template.Recipient.map((templateRecipient) => {
const foundRecipient = recipients.find((recipient) => recipient.id === templateRecipient.id);
if (!foundRecipient) {
throw new Error('Recipient not found.');
}
return {
name: foundRecipient.name,
email: foundRecipient.email,
role: templateRecipient.role,
};
});
const documentData = await prisma.documentData.create({ const documentData = await prisma.documentData.create({
data: { data: {
type: template.templateDocumentData.type, type: template.templateDocumentData.type,
@ -57,81 +70,55 @@ export const createDocumentFromTemplate = async ({
}, },
}); });
const document = await prisma.document.create({ return await prisma.$transaction(async (tx) => {
data: { const document = await tx.document.create({
userId, data: {
teamId: template.teamId, userId,
title: template.title, teamId: template.teamId,
documentDataId: documentData.id, title: template.title,
Recipient: { documentDataId: documentData.id,
create: template.Recipient.map((recipient) => ({ Recipient: {
email: recipient.email, create: finalRecipients.map((recipient) => ({
name: recipient.name,
role: recipient.role,
token: nanoid(),
})),
},
},
include: {
Recipient: {
orderBy: {
id: 'asc',
},
},
documentData: true,
},
});
await prisma.field.createMany({
data: template.Field.map((field) => {
const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId);
const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email);
return {
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: field.customText,
inserted: field.inserted,
documentId: document.id,
recipientId: documentRecipient?.id || null,
};
}),
});
if (recipients && recipients.length > 0) {
document.Recipient = await Promise.all(
recipients.map(async (recipient, index) => {
const existingRecipient = document.Recipient.at(index);
return await prisma.recipient.upsert({
where: {
documentId_email: {
documentId: document.id,
email: existingRecipient?.email ?? recipient.email,
},
},
update: {
name: recipient.name,
email: recipient.email,
role: recipient.role,
},
create: {
documentId: document.id,
email: recipient.email, email: recipient.email,
name: recipient.name, name: recipient.name,
role: recipient.role, role: recipient.role,
token: nanoid(), token: nanoid(),
})),
},
},
include: {
Recipient: {
orderBy: {
id: 'asc',
}, },
}); },
}), documentData: true,
); },
} });
return document; await tx.field.createMany({
data: template.Field.map((field) => {
const recipient = template.Recipient.find(
(recipient) => recipient.id === field.recipientId,
);
const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email);
return {
type: field.type,
page: field.page,
positionX: field.positionX,
positionY: field.positionY,
width: field.width,
height: field.height,
customText: field.customText,
inserted: field.inserted,
documentId: document.id,
recipientId: documentRecipient?.id || null,
};
}),
});
return document;
});
}; };

View File

@ -1,7 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { RecipientRole } from '@documenso/prisma/client';
export const ZCreateTemplateMutationSchema = z.object({ export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(), title: z.string().min(1).trim(),
teamId: z.number().optional(), teamId: z.number().optional(),
@ -11,15 +9,13 @@ export const ZCreateTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
templateId: z.number(), templateId: z.number(),
teamId: z.number().optional(), teamId: z.number().optional(),
recipients: z recipients: z.array(
.array( z.object({
z.object({ id: z.number(),
email: z.string().email(), email: z.string().email(),
name: z.string(), name: z.string().optional(),
role: z.nativeEnum(RecipientRole), }),
}), ),
)
.optional(),
}); });
export const ZDuplicateTemplateMutationSchema = z.object({ export const ZDuplicateTemplateMutationSchema = z.object({