From f6a24224fe05da2cfb5f88921214877f03df5504 Mon Sep 17 00:00:00 2001
From: Catalin Pit <25515812+catalinpit@users.noreply.github.com>
Date: Fri, 14 Feb 2025 16:25:15 +0200
Subject: [PATCH] feat: download options for document
---
.../[id]/document-page-view-button.tsx | 65 ++++++++++++++--
packages/lib/client-only/download-pdf.ts | 21 ++++-
.../trpc/server/document-router/router.ts | 77 ++++++++++++++++++-
.../trpc/server/document-router/schema.ts | 2 +
4 files changed, 155 insertions(+), 10 deletions(-)
diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
index a477d75c6..2593ef0b8 100644
--- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
+++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-button.tsx
@@ -14,6 +14,12 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = {
@@ -44,11 +50,19 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
const documentsPath = formatDocumentsPath(document.team?.url);
- const onDownloadClick = async () => {
+ const onDownloadClick = async ({
+ includeCertificate = true,
+ includeAuditLog = true,
+ }: {
+ includeCertificate?: boolean;
+ includeAuditLog?: boolean;
+ } = {}) => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id,
+ includeCertificate,
+ includeAuditLog,
},
{
context: {
@@ -63,7 +77,12 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
throw new Error('No document available');
}
- await downloadPDF({ documentData, fileName: documentWithData.title });
+ await downloadPDF({
+ documentData,
+ fileName: documentWithData.title,
+ includeCertificate,
+ includeAuditLog,
+ });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
@@ -112,10 +131,44 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
))
.with({ isComplete: true }, () => (
-
+
+
+
+
+
+
+ void onDownloadClick()}>
+ Complete Document
+
+
+
+ void onDownloadClick({ includeCertificate: true, includeAuditLog: false })
+ }
+ >
+ Without Audit Log
+
+
+
+ void onDownloadClick({ includeCertificate: false, includeAuditLog: true })
+ }
+ >
+ Without Certificate
+
+
+
+ void onDownloadClick({ includeCertificate: false, includeAuditLog: false })
+ }
+ >
+ Without Certificate & Audit Log
+
+
+
))
.otherwise(() => null);
};
diff --git a/packages/lib/client-only/download-pdf.ts b/packages/lib/client-only/download-pdf.ts
index 830e3428a..197262f5c 100644
--- a/packages/lib/client-only/download-pdf.ts
+++ b/packages/lib/client-only/download-pdf.ts
@@ -6,9 +6,16 @@ import { downloadFile } from './download-file';
type DownloadPDFProps = {
documentData: DocumentData;
fileName?: string;
+ includeCertificate?: boolean;
+ includeAuditLog?: boolean;
};
-export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) => {
+export const downloadPDF = async ({
+ documentData,
+ fileName,
+ includeCertificate,
+ includeAuditLog,
+}: DownloadPDFProps) => {
const bytes = await getFile(documentData);
const blob = new Blob([bytes], {
@@ -17,8 +24,18 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps)
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
+ let suffix = '_signed';
+
+ if (includeCertificate && includeAuditLog) {
+ suffix = suffix + '_with_certificate_and_audit';
+ } else if (includeCertificate) {
+ suffix = suffix + '_with_certificate';
+ } else if (includeAuditLog) {
+ suffix = suffix + '_with_audit';
+ }
+
downloadFile({
- filename: `${baseTitle}_signed.pdf`,
+ filename: `${baseTitle}${suffix}.pdf`,
data: blob,
});
};
diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts
index 2a151b7c1..ada909fce 100644
--- a/packages/trpc/server/document-router/router.ts
+++ b/packages/trpc/server/document-router/router.ts
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';
import { DateTime } from 'luxon';
+import { PDFDocument } from 'pdf-lib';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
@@ -23,6 +24,7 @@ import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
+import { getFile } from '@documenso/lib/universal/upload/get-file';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client';
@@ -65,13 +67,84 @@ export const documentRouter = router({
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
- const { documentId } = input;
+ const { documentId, includeCertificate, includeAuditLog } = input;
- return await getDocumentById({
+ const documentWithData = await getDocumentById({
userId: ctx.user.id,
teamId,
documentId,
});
+
+ if (includeCertificate && includeAuditLog) {
+ return documentWithData;
+ } else if (includeCertificate) {
+ const pdfData = await getFile(documentWithData.documentData);
+ const pdfDoc = await PDFDocument.load(pdfData);
+
+ const totalPages = pdfDoc.getPageCount();
+
+ if (!includeAuditLog) {
+ pdfDoc.removePage(totalPages - 1);
+ }
+
+ const pdfBytes = await pdfDoc.save();
+ const pdfBuffer = Buffer.from(pdfBytes).toString('base64');
+
+ return {
+ ...documentWithData,
+ documentData: {
+ ...documentWithData.documentData,
+ data: pdfBuffer,
+ initialData: documentWithData.documentData.data,
+ type: DocumentDataType.BYTES_64,
+ },
+ };
+ } else if (includeAuditLog) {
+ const pdfData = await getFile(documentWithData.documentData);
+ const pdfDoc = await PDFDocument.load(pdfData);
+
+ const totalPages = pdfDoc.getPageCount();
+
+ if (!includeCertificate) {
+ pdfDoc.removePage(totalPages - 2);
+ }
+
+ const pdfBytes = await pdfDoc.save();
+ const pdfBuffer = Buffer.from(pdfBytes).toString('base64');
+
+ return {
+ ...documentWithData,
+ documentData: {
+ ...documentWithData.documentData,
+ data: pdfBuffer,
+ initialData: documentWithData.documentData.data,
+ type: DocumentDataType.BYTES_64,
+ },
+ };
+ } else if (!includeCertificate && !includeAuditLog) {
+ const pdfData = await getFile(documentWithData.documentData);
+ const pdfDoc = await PDFDocument.load(pdfData);
+
+ const totalPages = pdfDoc.getPageCount();
+
+ pdfDoc.removePage(totalPages - 1);
+ pdfDoc.removePage(totalPages - 2);
+
+ const pdfBytes = await pdfDoc.save();
+ const pdfBuffer = Buffer.from(pdfBytes).toString('base64');
+
+ return {
+ ...documentWithData,
+ documentData: {
+ ...documentWithData.documentData,
+ data: pdfBuffer,
+ initialData: documentWithData.documentData.data,
+ type: DocumentDataType.BYTES_64,
+ },
+ };
+ } else {
+ return documentWithData;
+ }
}),
/**
diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts
index 887bc48e2..4211d3410 100644
--- a/packages/trpc/server/document-router/schema.ts
+++ b/packages/trpc/server/document-router/schema.ts
@@ -141,6 +141,8 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
export const ZGetDocumentByIdQuerySchema = z.object({
documentId: z.number(),
+ includeCertificate: z.boolean().default(true).optional(),
+ includeAuditLog: z.boolean().default(true).optional(),
});
export const ZDuplicateDocumentRequestSchema = z.object({