mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
fix: refactor use template
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user