feat: add document file conversion

This commit is contained in:
ephraimduncan
2025-12-30 19:24:23 +00:00
parent 3976531045
commit 74db3d7a1c
16 changed files with 1308 additions and 31 deletions
@@ -20,6 +20,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
@@ -132,6 +133,18 @@ export const EnvelopeDropZoneWrapper = ({
'ENVELOPE_ITEM_LIMIT_EXCEEDED',
() => t`You have reached the limit of the number of files per envelope.`,
)
.with(
'CONVERSION_SERVICE_UNAVAILABLE',
() => t`File conversion is not available. Please upload a PDF file.`,
)
.with(
'CONVERSION_FAILED',
() => t`Failed to convert file. Please try uploading a PDF instead.`,
)
.with(
'UNSUPPORTED_FILE_TYPE',
() => t`This file type is not supported. Please upload a PDF, Word document, or image.`,
)
.otherwise(() => t`An error occurred during upload.`);
toast({
@@ -180,7 +193,9 @@ export const EnvelopeDropZoneWrapper = ({
.with(ErrorCode.FileTooLarge, () => (
<Trans>File is larger than {APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB</Trans>
))
.with(ErrorCode.FileInvalidType, () => <Trans>Only PDF files are allowed</Trans>)
.with(ErrorCode.FileInvalidType, () => (
<Trans>Supported formats: PDF, Word documents, JPEG, and PNG images</Trans>
))
.with(ErrorCode.FileTooSmall, () => <Trans>File is too small</Trans>)
.with(ErrorCode.TooManyFiles, () => (
<Trans>Only one file can be uploaded at a time</Trans>
@@ -208,9 +223,7 @@ export const EnvelopeDropZoneWrapper = ({
});
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
accept: ALLOWED_UPLOAD_MIME_TYPES,
multiple: true,
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
maxFiles: maximumEnvelopeItemCount,
@@ -237,7 +250,7 @@ export const EnvelopeDropZoneWrapper = ({
</h2>
<p className="text-md mt-4 text-muted-foreground">
<Trans>Drag and drop your PDF file here</Trans>
<Trans>Drag and drop your document here</Trans>
</p>
{isUploadDisabled && IS_BILLING_ENABLED() && (
+30 -6
View File
@@ -2,6 +2,7 @@ import { type DocumentDataType, DocumentStatus } from '@prisma/client';
import contentDisposition from 'content-disposition';
import { type Context } from 'hono';
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
import { sha256 } from '@documenso/lib/universal/crypto';
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
@@ -14,6 +15,8 @@ type HandleEnvelopeItemFileRequestOptions = {
type: DocumentDataType;
data: string;
initialData: string;
originalData?: string | null;
originalMimeType?: string | null;
};
version: 'signed' | 'original';
isDownload: boolean;
@@ -31,7 +34,21 @@ export const handleEnvelopeItemFileRequest = async ({
isDownload,
context: c,
}: HandleEnvelopeItemFileRequestOptions) => {
const documentDataToUse = version === 'signed' ? documentData.data : documentData.initialData;
const shouldServeOriginalSourceFile =
version === 'original' &&
documentData.originalData &&
documentData.originalMimeType &&
documentData.originalMimeType !== 'application/pdf';
const documentDataToUse = shouldServeOriginalSourceFile
? documentData.originalData!
: version === 'signed'
? documentData.data
: documentData.initialData;
const contentType = shouldServeOriginalSourceFile
? documentData.originalMimeType!
: 'application/pdf';
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
@@ -52,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
return c.json({ error: 'File not found' }, 404);
}
c.header('Content-Type', 'application/pdf');
c.header('Content-Type', contentType);
c.header('ETag', etag);
if (!isDownload) {
@@ -64,10 +81,17 @@ export const handleEnvelopeItemFileRequest = async ({
}
if (isDownload) {
// Generate filename following the pattern from envelope-download-dialog.tsx
const baseTitle = title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
const baseTitle = title.replace(/\.[^/.]+$/, '');
let filename: string;
if (version === 'signed') {
filename = `${baseTitle}_signed.pdf`;
} else if (shouldServeOriginalSourceFile) {
const extension = getFileExtensionForMimeType(documentData.originalMimeType!);
filename = `${baseTitle}${extension}`;
} else {
filename = `${baseTitle}.pdf`;
}
c.header('Content-Disposition', contentDisposition(filename));