From 08b693ff95a7ddb62db649e9474696f318f0cc1a Mon Sep 17 00:00:00 2001 From: Mythie Date: Mon, 8 Apr 2024 17:01:11 +0700 Subject: [PATCH 1/5] feat: add prefilling pdf form fields via api --- packages/api/v1/implementation.ts | 29 ++++++++++ packages/api/v1/schema.ts | 2 + .../server-only/document/create-document.ts | 3 ++ .../server-only/document/send-document.tsx | 36 +++++++++++++ .../pdf/insert-form-values-in-pdf.ts | 54 +++++++++++++++++++ .../template/create-document-from-template.ts | 1 + .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + 8 files changed, 128 insertions(+) create mode 100644 packages/lib/server-only/pdf/insert-form-values-in-pdf.ts create mode 100644 packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 675c3b532..d9bc1a6d7 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -13,6 +13,7 @@ import { createField } from '@documenso/lib/server-only/field/create-field'; import { deleteField } from '@documenso/lib/server-only/field/delete-field'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; import { updateField } from '@documenso/lib/server-only/field/update-field'; +import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf'; import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient'; import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; @@ -20,6 +21,8 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { getFile } from '@documenso/lib/universal/upload/get-file'; +import { putFile } from '@documenso/lib/universal/upload/put-file'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client'; @@ -156,6 +159,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { title: body.title, userId: user.id, teamId: team?.id, + formValues: body.formValues, documentDataId: documentData.id, requestMetadata: extractNextApiRequestMetadata(args.req), }); @@ -217,12 +221,37 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { recipients: body.recipients, }); + let documentDataId = document.documentDataId; + + if (body.formValues) { + const pdf = await getFile(document.documentData); + + const prefilled = await insertFormValuesInPdf({ + pdf: Buffer.from(pdf), + formValues: body.formValues, + }); + + const newDocumentData = await putFile({ + name: fileName, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(prefilled), + }); + + documentDataId = newDocumentData.id; + } + await updateDocument({ documentId: document.id, userId: user.id, teamId: team?.id, data: { title: fileName, + formValues: body.formValues, + documentData: { + connect: { + id: documentDataId, + }, + }, }, }); diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index fbe3ba5c1..01f6e2d58 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -73,6 +73,7 @@ export const ZCreateDocumentMutationSchema = z.object({ redirectUrl: z.string(), }) .partial(), + formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), }); export type TCreateDocumentMutationSchema = z.infer; @@ -112,6 +113,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ }) .partial() .optional(), + formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(), }); export type TCreateDocumentFromTemplateMutationSchema = z.infer< diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index ce1f16670..1d145a60d 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -14,6 +14,7 @@ export type CreateDocumentOptions = { userId: number; teamId?: number; documentDataId: string; + formValues?: Record; requestMetadata?: RequestMetadata; }; @@ -22,6 +23,7 @@ export const createDocument = async ({ title, documentDataId, teamId, + formValues, requestMetadata, }: CreateDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -51,6 +53,7 @@ export const createDocument = async ({ documentDataId, userId, teamId, + formValues, }, }); diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 7c928f9a9..acbcc499f 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -17,6 +17,9 @@ import { RECIPIENT_ROLES_DESCRIPTION, RECIPIENT_ROLE_TO_EMAIL_TYPE, } from '../../constants/recipient-roles'; +import { getFile } from '../../universal/upload/get-file'; +import { putFile } from '../../universal/upload/put-file'; +import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type SendDocumentOptions = { @@ -65,6 +68,7 @@ export const sendDocument = async ({ include: { Recipient: true, documentMeta: true, + documentData: true, }, }); @@ -82,6 +86,38 @@ export const sendDocument = async ({ throw new Error('Can not send completed document'); } + const { documentData } = document; + + if (!documentData.data) { + throw new Error('Document data not found'); + } + + if (document.formValues) { + const file = await getFile(documentData); + + const prefilled = await insertFormValuesInPdf({ + pdf: Buffer.from(file), + formValues: document.formValues as Record, + }); + + const newDocumentData = await putFile({ + name: document.title, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(prefilled), + }); + + const result = await prisma.document.update({ + where: { + id: document.id, + }, + data: { + documentDataId: newDocumentData.id, + }, + }); + + Object.assign(document, result); + } + await Promise.all( document.Recipient.map(async (recipient) => { if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) { diff --git a/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts b/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts new file mode 100644 index 000000000..a3c311895 --- /dev/null +++ b/packages/lib/server-only/pdf/insert-form-values-in-pdf.ts @@ -0,0 +1,54 @@ +import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib'; + +export type InsertFormValuesInPdfOptions = { + pdf: Buffer; + formValues: Record; +}; + +export const insertFormValuesInPdf = async ({ pdf, formValues }: InsertFormValuesInPdfOptions) => { + const doc = await PDFDocument.load(pdf); + + const form = doc.getForm(); + + if (!form) { + return pdf; + } + + for (const [key, value] of Object.entries(formValues)) { + try { + const field = form.getField(key); + + if (!field) { + continue; + } + + if (typeof value === 'boolean' && field instanceof PDFCheckBox) { + if (value) { + field.check(); + } else { + field.uncheck(); + } + } + + if (field instanceof PDFTextField) { + field.setText(value.toString()); + } + + if (field instanceof PDFDropdown) { + field.select(value.toString()); + } + + if (field instanceof PDFRadioGroup) { + field.select(value.toString()); + } + } catch (err) { + if (err instanceof Error) { + console.error(`Error setting value for field ${key}: ${err.message}`); + } else { + console.error(`Error setting value for field ${key}`); + } + } + } + + return await doc.save().then((buf) => Buffer.from(buf)); +}; 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 55519a30e..8ae5fecaf 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({ id: 'asc', }, }, + documentData: true, }, }); diff --git a/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql b/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql new file mode 100644 index 000000000..fbf67b637 --- /dev/null +++ b/packages/prisma/migrations/20240408083413_add_form_values_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "formValues" JSONB; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 868b8d8e1..35d429779 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -257,6 +257,7 @@ model Document { userId Int User User @relation(fields: [userId], references: [id], onDelete: Cascade) authOptions Json? + formValues Json? title String status DocumentStatus @default(DRAFT) Recipient Recipient[] From 627265f0169272d553e98096da38d9f60ba4beb6 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:28:50 +0300 Subject: [PATCH 2/5] fix: return updated doc (#1089) ## Description Fetch the updated version of the document after sealing it and return it. Previously, the `document.documentData.data` wasn't up to date. Now it is. ## Related Issue Fixes #1088. ## Testing Performed * Added console.logs in the code to make sure it returns the proper data * Set up a webhook and tested that the webhook receives the updated data ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. --- packages/lib/server-only/document/seal-document.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 58480a7bd..ec5f93539 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -153,9 +153,19 @@ export const sealDocument = async ({ await sendCompletedEmail({ documentId, requestMetadata }); } + const updatedDocument = await prisma.document.findFirstOrThrow({ + where: { + id: document.id, + }, + include: { + documentData: true, + Recipient: true, + }, + }); + await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_COMPLETED, - data: document, + data: updatedDocument, userId: document.userId, teamId: document.teamId ?? undefined, }); From 1400c335a5291a272ea82b1d7c113632c029d6da Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 9 Apr 2024 11:31:53 +0700 Subject: [PATCH 3/5] fix: improve document loading ui consistency (#1082) ## Description General UI updates ## Changes Made - Add consistent spacing between document edit/view/log pages - Add document status to document audit log page - Update document loading page to reserve space for the document status below the title - Update the document audit log page to show full dates in the correct locale --- .../[id]/edit/document-edit-page-view.tsx | 2 +- .../(dashboard)/documents/[id]/loading.tsx | 9 ++++- .../[id]/logs/document-logs-page-view.tsx | 39 +++++++++++++++---- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx index cab17c841..8a78ca9aa 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit/document-edit-page-view.tsx @@ -100,7 +100,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie @@ -13,7 +15,12 @@ export default function Loading() {

Loading Document...

-
+ +
+ +
+ +
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx index 019ced57e..33d6cb8fe 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/logs/document-logs-page-view.tsx @@ -2,16 +2,21 @@ import Link from 'next/link'; import { redirect } from 'next/navigation'; import { ChevronLeft, DownloadIcon } from 'lucide-react'; +import { DateTime } from 'luxon'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getLocale } from '@documenso/lib/server-only/headers/get-locale'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import type { Recipient, Team } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { Card } from '@documenso/ui/primitives/card'; -import { FRIENDLY_STATUS_MAP } from '~/components/formatter/document-status'; +import { + DocumentStatus as DocumentStatusComponent, + FRIENDLY_STATUS_MAP, +} from '~/components/formatter/document-status'; import { DocumentLogsDataTable } from './document-logs-data-table'; @@ -23,6 +28,8 @@ export type DocumentLogsPageViewProps = { }; export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => { + const locale = getLocale(); + const { id } = params; const documentId = Number(id); @@ -67,15 +74,21 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie }, { description: 'Created by', - value: document.User.name ?? document.User.email, + value: document.User.name + ? `${document.User.name} (${document.User.email})` + : document.User.email, }, { description: 'Date created', - value: document.createdAt.toISOString(), + value: DateTime.fromJSDate(document.createdAt) + .setLocale(locale) + .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), }, { description: 'Last updated', - value: document.updatedAt.toISOString(), + value: DateTime.fromJSDate(document.updatedAt) + .setLocale(locale) + .toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS), }, { description: 'Time zone', @@ -90,7 +103,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie text = `${recipient.name} (${recipient.email})`; } - return `${text} - ${recipient.role}`; + return `[${recipient.role}] ${text}`; }; return ( @@ -104,9 +117,19 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
-

- {document.title} -

+
+

+ {document.title} +

+ +
+ +
+