From 4d286e01d131cc50c4aa944c287666240ae887a4 Mon Sep 17 00:00:00 2001 From: Mythie Date: Tue, 20 Feb 2024 17:20:03 +0000 Subject: [PATCH 1/7] feat: allow recipients when creating document from template --- .../template/create-document-from-template.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index c520d4ce1..6828bd23f 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,14 +1,21 @@ import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; -import type { TCreateDocumentFromTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; +import type { RecipientRole } from '@documenso/prisma/client'; -export type CreateDocumentFromTemplateOptions = TCreateDocumentFromTemplateMutationSchema & { +export type CreateDocumentFromTemplateOptions = { + templateId: number; userId: number; + recipients?: { + name?: string; + email: string; + role?: RecipientRole; + }[]; }; export const createDocumentFromTemplate = async ({ templateId, userId, + recipients, }: CreateDocumentFromTemplateOptions) => { const template = await prisma.template.findUnique({ where: { @@ -63,7 +70,11 @@ export const createDocumentFromTemplate = async ({ }, include: { - Recipient: true, + Recipient: { + orderBy: { + id: 'asc', + }, + }, }, }); @@ -88,5 +99,34 @@ export const createDocumentFromTemplate = async ({ }), }); + 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, + name: recipient.name, + role: recipient.role, + token: nanoid(), + }, + }); + }), + ); + } + return document; }; From 6ee896048ea9e676f457005dceeaecdc3c4ef262 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 20 Feb 2024 19:11:12 +0000 Subject: [PATCH 2/7] feat: dialog to enter custom recipients for creating document from template --- .../templates/data-table-templates.tsx | 66 +---- .../templates/use-template-dialog.tsx | 242 ++++++++++++++++++ .../server-only/template/find-templates.ts | 1 + .../trpc/server/template-router/router.ts | 1 + .../trpc/server/template-router/schema.ts | 11 + .../primitives/document-flow/add-signers.tsx | 2 +- 6 files changed, 270 insertions(+), 53 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx index 309695c88..e878d8df2 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -1,30 +1,31 @@ 'use client'; -import { useState, useTransition } from 'react'; +import { useTransition } from 'react'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { AlertTriangle, Loader, Plus } from 'lucide-react'; +import { AlertTriangle, Loader } from 'lucide-react'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import type { Template } from '@documenso/prisma/client'; -import { trpc } from '@documenso/trpc/react'; +import type { Recipient, Template } from '@documenso/prisma/client'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; -import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; -import { useToast } from '@documenso/ui/primitives/use-toast'; import { LocaleDate } from '~/components/formatter/locale-date'; import { TemplateType } from '~/components/formatter/template-type'; import { DataTableActionDropdown } from './data-table-action-dropdown'; import { DataTableTitle } from './data-table-title'; +import { UseTemplateDialog } from './use-template-dialog'; + +type TemplateWithRecipient = Template & { + Recipient: Recipient[]; +}; type TemplatesDataTableProps = { - templates: Template[]; + templates: TemplateWithRecipient[]; perPage: number; page: number; totalPages: number; @@ -47,14 +48,6 @@ export const TemplatesDataTable = ({ const { remaining } = useLimits(); - const router = useRouter(); - - const { toast } = useToast(); - const [loadingStates, setLoadingStates] = useState<{ [key: string]: boolean }>({}); - - const { mutateAsync: createDocumentFromTemplate } = - trpc.template.createDocumentFromTemplate.useMutation(); - const onPaginationChange = (page: number, perPage: number) => { startTransition(() => { updateSearchParams({ @@ -64,28 +57,6 @@ export const TemplatesDataTable = ({ }); }; - const onUseButtonClick = async (templateId: number) => { - try { - const { id } = await createDocumentFromTemplate({ - templateId, - }); - - toast({ - title: 'Document created', - description: 'Your document has been created from the template successfully.', - duration: 5000, - }); - - router.push(`${documentRootPath}/${id}`); - } catch (err) { - toast({ - title: 'Error', - description: 'An error occurred while creating document from template.', - variant: 'destructive', - }); - } - }; - return (
{remaining.documents === 0 && ( @@ -121,22 +92,13 @@ export const TemplatesDataTable = ({ header: 'Actions', accessorKey: 'actions', cell: ({ row }) => { - const isRowLoading = loadingStates[row.original.id]; - return (
- + ; + +export type UseTemplateDialogProps = { + templateId: number; + recipients: Recipient[]; + documentRootPath: string; +}; + +export function UseTemplateDialog({ + recipients, + documentRootPath, + templateId, +}: UseTemplateDialogProps) { + const router = useRouter(); + const { toast } = useToast(); + + const { + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(ZAddRecipientsForNewDocumentSchema), + defaultValues: { + recipients: + recipients.length > 0 + ? recipients.map((recipient) => ({ + nativeId: recipient.id, + formId: String(recipient.id), + name: recipient.name, + email: recipient.email, + role: recipient.role, + })) + : [ + { + name: '', + email: '', + role: RecipientRole.SIGNER, + }, + ], + }, + }); + + const { mutateAsync: createDocumentFromTemplate, isLoading: isCreatingDocumentFromTemplate } = + trpc.template.createDocumentFromTemplate.useMutation(); + + const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => { + try { + const { id } = await createDocumentFromTemplate({ + templateId, + recipients: data.recipients, + }); + + toast({ + title: 'Document created', + description: 'Your document has been created from the template successfully.', + duration: 5000, + }); + + router.push(`${documentRootPath}/${id}`); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while creating document from template.', + variant: 'destructive', + }); + } + }; + + const onCreateDocumentFromTemplate = handleSubmit(onSubmit); + + const { fields: formRecipients } = useFieldArray({ + control, + name: 'recipients', + }); + + return ( + + + + + + + Document Recipients + Add the recipients to create the template with. + +
+ {formRecipients.map((recipient, index) => ( +
+
+ + + ( + + )} + /> +
+ +
+ + + ( + + )} + /> +
+ +
+ ( + + )} + /> +
+ +
+ + +
+
+ ))} +
+ + + + + + + + +
+
+ ); +} diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts index d453d28a0..69b43f9b9 100644 --- a/packages/lib/server-only/template/find-templates.ts +++ b/packages/lib/server-only/template/find-templates.ts @@ -38,6 +38,7 @@ export const findTemplates = async ({ include: { templateDocumentData: true, Field: true, + Recipient: true, }, skip: Math.max(page - 1, 0) * perPage, orderBy: { diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 7417e7d00..3f5346554 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -52,6 +52,7 @@ export const templateRouter = router({ return await createDocumentFromTemplate({ templateId, userId: ctx.user.id, + recipients: input.recipients, }); } catch (err) { throw new TRPCError({ diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 3d87d4b4f..561bad109 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +import { RecipientRole } from '@documenso/prisma/client'; + export const ZCreateTemplateMutationSchema = z.object({ title: z.string().min(1).trim(), teamId: z.number().optional(), @@ -8,6 +10,15 @@ export const ZCreateTemplateMutationSchema = z.object({ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ templateId: z.number(), + recipients: z + .array( + z.object({ + email: z.string().email(), + name: z.string(), + role: z.nativeEnum(RecipientRole), + }), + ) + .optional(), }); export const ZDuplicateTemplateMutationSchema = z.object({ diff --git a/packages/ui/primitives/document-flow/add-signers.tsx b/packages/ui/primitives/document-flow/add-signers.tsx index b1341c6ca..06e0a2af2 100644 --- a/packages/ui/primitives/document-flow/add-signers.tsx +++ b/packages/ui/primitives/document-flow/add-signers.tsx @@ -32,7 +32,7 @@ import { import { ShowFieldItem } from './show-field-item'; import type { DocumentFlowStep } from './types'; -const ROLE_ICONS: Record = { +export const ROLE_ICONS: Record = { SIGNER: , APPROVER: , CC: , From 77193b93c424d4d185788a72f8186e3f46ea9936 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 24 Feb 2024 10:36:27 +0000 Subject: [PATCH 3/7] fix: resolve build error and update dialog width --- .../web/src/app/(dashboard)/templates/use-template-dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx index f1cbb3e4c..c7da8bf49 100644 --- a/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/use-template-dialog.tsx @@ -19,10 +19,10 @@ import { DialogTitle, DialogTrigger, } from '@documenso/ui/primitives/dialog'; -import { ROLE_ICONS } from '@documenso/ui/primitives/document-flow/add-signers'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; 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'; @@ -119,7 +119,7 @@ export function UseTemplateDialog({ Use Template - + Document Recipients Add the recipients to create the template with. From a5a0fd9187d9410f2db1b791e703ef5278b854a7 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 24 Feb 2024 10:55:48 +0000 Subject: [PATCH 4/7] fix: update e2e test --- packages/app-tests/e2e/templates/manage-templates.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index 53edc705d..40ff87beb 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -187,6 +187,7 @@ test('[TEMPLATES]: use template', async ({ page }) => { // Use personal template. await page.getByRole('button', { name: 'Use Template' }).click(); + await page.getByRole('button', { name: 'Create Document' }).click(); await page.waitForURL(/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL('/documents'); @@ -196,6 +197,7 @@ test('[TEMPLATES]: use template', async ({ page }) => { // Use team template. await page.getByRole('button', { name: 'Use Template' }).click(); + await page.getByRole('button', { name: 'Create Document' }).click(); await page.waitForURL(/\/t\/.+\/documents/); await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); await page.waitForURL(`/t/${team.url}/documents`); From 14c77d7c926a4ac9a54524393c9c64dd61b06beb Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 24 Feb 2024 11:19:07 +0000 Subject: [PATCH 5/7] fix: update e2e test --- packages/prisma/seed/templates.ts | 35 +++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts index 7f1b2f8e9..3feb82289 100644 --- a/packages/prisma/seed/templates.ts +++ b/packages/prisma/seed/templates.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { prisma } from '..'; -import { DocumentDataType } from '../client'; +import { DocumentDataType, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '../client'; const examplePdf = fs .readFileSync(path.join(__dirname, '../../../assets/example.pdf')) @@ -28,9 +28,36 @@ export const seedTemplate = async (options: SeedTemplateOptions) => { return await prisma.template.create({ data: { title, - templateDocumentDataId: documentData.id, - userId: userId, - teamId, + templateDocumentData: { + connect: { + id: documentData.id, + }, + }, + User: { + connect: { + id: userId, + }, + }, + Recipient: { + create: { + email: 'recipient.1@documenso.com', + name: 'Recipient 1', + token: Math.random().toString().slice(2, 7), + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + readStatus: ReadStatus.NOT_OPENED, + role: RecipientRole.SIGNER, + }, + }, + ...(teamId + ? { + team: { + connect: { + id: teamId, + }, + }, + } + : {}), }, }); }; From 4ca154d88a8cf20f71b0561d3f5ebabd85fb4ff2 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 24 Feb 2024 11:54:13 +0000 Subject: [PATCH 6/7] fix: update e2e test? --- packages/app-tests/e2e/templates/manage-templates.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index 40ff87beb..f89583097 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -107,6 +107,8 @@ test('[TEMPLATES]: delete template', async ({ page }) => { await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'Delete' }).click(); await expect(page.getByText('Template deleted').first()).toBeVisible(); + + await page.waitForTimeout(1000); } await unseedTeam(team.url); @@ -194,6 +196,7 @@ test('[TEMPLATES]: use template', async ({ page }) => { await expect(page.getByRole('main')).toContainText('Showing 1 result'); await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`); + await page.waitForTimeout(1000); // Use team template. await page.getByRole('button', { name: 'Use Template' }).click(); From 5bd0dde4a5caee5583b982feb53e3dbb83b833ef Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 26 Feb 2024 10:50:46 +1100 Subject: [PATCH 7/7] fix: e2e tests --- .../src/app/(dashboard)/documents/[id]/document-page-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx index e12a745a2..e1b2e0d20 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx @@ -95,7 +95,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) return (
- + Documents