mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: add prefilling pdf form fields via api (#1086)
## Description Adds the ability to prefill native PDF form fields via the API using either normal documents or templates. Since we won't always know when a document is uploaded and has forms we opt to do this on creation for templates and on sending the document to recipients in all cases. This means that for a created document it can look a little funky since the form fields are missing the data until the document is sent. This should be improved in a later change but since we've scoped this to an API only workflow for now we are less concerned with the visual issues. ## Related Issue N/A ## Changes Made - Added the `formValues` field the document model - Added a new method for finding and filling form fields based on a `key | value` pair - Updated the API input shapes to take the new field. ## Testing Performed - Have created and tested a document using the API both for creation and usage with a template. - Have verified that the fields display as expected either during creation or sending depending on the document type.
This commit is contained in:
@ -14,6 +14,7 @@ export type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentDataId: string;
|
||||
formValues?: Record<string, string | number | boolean>;
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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<string, string | number | boolean>,
|
||||
});
|
||||
|
||||
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) {
|
||||
|
||||
54
packages/lib/server-only/pdf/insert-form-values-in-pdf.ts
Normal file
54
packages/lib/server-only/pdf/insert-form-values-in-pdf.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib';
|
||||
|
||||
export type InsertFormValuesInPdfOptions = {
|
||||
pdf: Buffer;
|
||||
formValues: Record<string, string | boolean | number>;
|
||||
};
|
||||
|
||||
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));
|
||||
};
|
||||
@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
documentData: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user