Compare commits

...

14 Commits

Author SHA1 Message Date
ephraimduncan f3f5903760 chore: merge main, resolve biome formatting conflicts
Merge origin/main into feat/document-file-conversion. Conflicts were
format-only (Tailwind class ordering, single-line vs multi-line) plus
two semantic merges:

- files.helpers.ts: combine main's pending-PDF download path with the
  branch's original-source-file (DOCX/PNG/JPEG) download path
- download-pdf.ts: combine main's versionToFilenameSuffix helper with
  the branch's server-provided Content-Disposition filename support
2026-05-12 11:28:47 +00:00
ephraimduncan f153a7c437 fix: broken merge in team-account-folders E2E test
The merge of main into feat/document-file-conversion silently produced a
bad hybrid for 'can create a template inside a template folder': main
(#2303) replaced the click+setInputFiles flow with fileChooser.setFiles,
while this branch only touched .nth(0) within that same block. Git kept
both, leaving an obsolete click on 'Upload Template Document'. Apply
main's final form.
2026-04-20 10:48:49 +00:00
ephraimduncan f9cb8d84ed Merge branch 'main' into feat/document-file-conversion
Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller

Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
2026-04-20 10:11:35 +00:00
ephraimduncan cc67454513 fix: merge conflicts 2026-01-15 12:42:36 +00:00
Ephraim Duncan 665a0d0ea0 Merge branch 'main' into feat/document-file-conversion 2026-01-06 20:48:27 +00:00
ephraimduncan 02ff8df09b chore: remove tests - temp 2026-01-01 23:04:26 +00:00
ephraimduncan d4bee4aed1 fix: tests 2026-01-01 22:46:35 +00:00
ephraimduncan f4cfa71379 fix: tests 2026-01-01 10:44:55 +00:00
ephraimduncan b1763e422c fix: tests 2026-01-01 10:13:34 +00:00
ephraimduncan 64e0695811 fix: translations 2025-12-30 20:10:08 +00:00
ephraimduncan 8a4205d808 fix: build errors 2025-12-30 19:57:01 +00:00
ephraimduncan 74db3d7a1c feat: add document file conversion 2025-12-30 19:24:23 +00:00
ephraimduncan 3976531045 chore: fix merge conflicts 2025-12-29 20:02:51 +00:00
Ephraim Atta-Duncan 7a499270be feat: document file conversion 2025-12-15 11:58:42 +00:00
44 changed files with 1512 additions and 3721 deletions
@@ -4,6 +4,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';
@@ -115,6 +116,12 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
)
.with('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({
@@ -158,9 +165,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
});
};
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,
@@ -183,7 +188,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
</h2>
<p className="mt-4 text-md 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() && (
@@ -114,6 +114,18 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const errorMessage = match(error.code)
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
.with(
'UNSUPPORTED_FILE_TYPE',
() => t`This file type is not supported. Please upload a PDF, DOCX, JPEG, or PNG file.`,
)
.with(
'CONVERSION_SERVICE_UNAVAILABLE',
() => t`File conversion is temporarily unavailable. Please upload a PDF file instead.`,
)
.with(
'CONVERSION_FAILED',
() => t`Failed to convert the file to PDF. Please try again or upload a PDF file instead.`,
)
.with(
AppErrorCode.LIMIT_EXCEEDED,
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
+28 -6
View File
@@ -1,3 +1,4 @@
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
@@ -24,6 +25,8 @@ type DocumentDataInput = {
type: DocumentDataType;
data: string;
initialData: string;
originalData?: string | null;
originalMimeType?: string | null;
};
type EnvelopeForPendingDownload = {
@@ -84,7 +87,19 @@ const handleStaticFileRequest = async ({
isDownload,
context: c,
}: StaticFileRequestOptions) => {
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');
@@ -105,7 +120,7 @@ const handleStaticFileRequest = 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) {
@@ -117,10 +132,17 @@ const handleStaticFileRequest = 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));
+1 -1
View File
@@ -46,7 +46,7 @@ export const filesRoute = new Hono<HonoEnv>()
return c.json({ error: 'File too large' }, 400);
}
const result = await putNormalizedPdfFileServerSide(file);
const result = await putNormalizedPdfFileServerSide({ file });
return c.json(result);
} catch (error) {
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+9
View File
@@ -48,6 +48,15 @@ services:
entrypoint: sh
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
gotenberg:
image: gotenberg/gotenberg:8
container_name: gotenberg
ports:
- 3001:3000
command:
- 'gotenberg'
- '--api-timeout=30s'
volumes:
minio:
redis:
+5 -3
View File
@@ -814,9 +814,11 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
});
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
file: {
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
},
});
await prisma.envelopeItem.update({
+37 -4
View File
@@ -13,7 +13,7 @@ type DownloadPDFProps = {
/**
* Specifies which version of the document to download.
* 'signed': Downloads the signed version (default).
* 'original': Downloads the original version.
* 'original': Downloads the original version (may be DOCX, PNG, JPEG if converted).
* 'pending': Downloads the original document with currently-inserted fields burned in.
* Only valid while the envelope is in PENDING status. Not supported via
* recipient token.
@@ -21,6 +21,29 @@ type DownloadPDFProps = {
version?: DocumentVersion;
};
const getFilenameFromContentDisposition = (header: string | null): string | null => {
if (!header) {
return null;
}
const filenameStarMatch = header.match(/filename\*=(?:UTF-8''|utf-8'')([^;]+)/i);
if (filenameStarMatch) {
return decodeURIComponent(filenameStarMatch[1]);
}
const filenameMatch = header.match(/filename="([^"]+)"/);
if (filenameMatch) {
return filenameMatch[1];
}
const filenameNoQuotesMatch = header.match(/filename=([^;\s]+)/);
if (filenameNoQuotesMatch) {
return filenameNoQuotesMatch[1];
}
return null;
};
const versionToFilenameSuffix = (version: DocumentVersion): string => {
switch (version) {
case 'signed':
@@ -40,12 +63,22 @@ export const downloadPDF = async ({ envelopeItem, token, fileName, version = 'si
version,
});
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
const response = await fetch(downloadUrl);
const blob = await response.blob();
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
const contentDisposition = response.headers.get('Content-Disposition');
const serverFilename = getFilenameFromContentDisposition(contentDisposition);
let filename: string;
if (serverFilename) {
filename = serverFilename;
} else {
const baseTitle = (fileName ?? 'document').replace(/\.[^/.]+$/, '');
filename = `${baseTitle}${versionToFilenameSuffix(version)}`;
}
downloadFile({
filename: `${baseTitle}${versionToFilenameSuffix(version)}`,
filename,
data: blob,
});
};
+23
View File
@@ -0,0 +1,23 @@
import { env } from '@documenso/lib/utils/env';
export const ALLOWED_UPLOAD_MIME_TYPES: Record<string, string[]> = {
'application/pdf': ['.pdf'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
};
export const isAllowedMimeType = (mimeType: string): boolean =>
mimeType in ALLOWED_UPLOAD_MIME_TYPES;
export const getGotenbergUrl = (): string | undefined => env('NEXT_PRIVATE_GOTENBERG_URL');
export const getGotenbergTimeout = (): number => {
const timeout = env('NEXT_PRIVATE_GOTENBERG_TIMEOUT');
return timeout ? parseInt(timeout, 10) : 30_000;
};
export const getFileExtensionForMimeType = (mimeType: string): string => {
const extensions = ALLOWED_UPLOAD_MIME_TYPES[mimeType];
return extensions?.[0] ?? '.pdf';
};
@@ -471,7 +471,7 @@ const decorateAndSignPdf = async ({
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdfBytes),
},
envelopeItem.documentData.initialData,
{ initialData: envelopeItem.documentData.initialData },
);
return {
@@ -11,14 +11,24 @@ export type CreateDocumentDataOptions = {
* If not provided, the current data will be used.
*/
initialData?: string;
originalData?: string;
originalMimeType?: string;
};
export const createDocumentData = async ({ type, data, initialData }: CreateDocumentDataOptions) => {
export const createDocumentData = async ({
type,
data,
initialData,
originalData,
originalMimeType,
}: CreateDocumentDataOptions) => {
return await prisma.documentData.create({
data: {
type,
data,
initialData: initialData || data,
originalData,
originalMimeType,
},
});
};
@@ -43,6 +43,8 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
type: true,
data: true,
initialData: true,
originalMimeType: true,
originalData: true,
},
},
},
@@ -74,6 +74,8 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
id: true,
data: true,
initialData: true,
originalMimeType: true,
originalData: true,
},
},
},
@@ -336,9 +338,11 @@ const injectFormValuesIntoDocument = async (
}
const newDocumentData = await putNormalizedPdfFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
file: {
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
},
});
await prisma.envelopeItem.update({
@@ -49,6 +49,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId, overrides }: Dupli
data: true,
initialData: true,
type: true,
originalMimeType: true,
},
},
},
@@ -0,0 +1,50 @@
import { isAllowedMimeType } from '../../constants/upload';
import { AppError } from '../../errors/app-error';
import { convertFileToPdfViaGotenberg } from '../gotenberg/gotenberg-client';
type FileInput = {
name: string;
type: string;
arrayBuffer: () => Promise<ArrayBuffer>;
};
export type ConvertToPdfResult = {
pdfBuffer: Buffer;
originalBuffer: Buffer | null;
originalMimeType: string;
};
export const convertToPdfIfNeeded = async (file: FileInput): Promise<ConvertToPdfResult> => {
const originalMimeType = file.type;
if (!isAllowedMimeType(originalMimeType)) {
throw new AppError('UNSUPPORTED_FILE_TYPE', {
message: `File type '${originalMimeType}' is not supported`,
userMessage: 'This file type is not supported. Please upload a PDF, DOCX, JPEG, or PNG file.',
statusCode: 400,
});
}
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
if (originalMimeType === 'application/pdf') {
return {
pdfBuffer: buffer,
originalBuffer: null,
originalMimeType,
};
}
const pdfBuffer = await convertFileToPdfViaGotenberg({
file: buffer,
filename: file.name,
mimeType: originalMimeType,
});
return {
pdfBuffer,
originalBuffer: buffer,
originalMimeType,
};
};
@@ -0,0 +1,81 @@
import { getGotenbergTimeout, getGotenbergUrl } from '../../constants/upload';
import { AppError } from '../../errors/app-error';
export type ConvertFileToPdfOptions = {
file: Buffer;
filename: string;
mimeType: string;
};
export const convertFileToPdfViaGotenberg = async ({
file,
filename,
mimeType,
}: ConvertFileToPdfOptions): Promise<Buffer> => {
const gotenbergUrl = getGotenbergUrl();
if (!gotenbergUrl) {
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
message: 'Gotenberg URL is not configured',
userMessage: 'File conversion service is not available. Please upload a PDF file instead.',
statusCode: 503,
});
}
const formData = new FormData();
const blob = new Blob([file], { type: mimeType });
formData.append('files', blob, filename);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), getGotenbergTimeout());
try {
const response = await fetch(`${gotenbergUrl}/forms/libreoffice/convert`, {
method: 'POST',
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
console.error(`Gotenberg conversion failed: ${response.status} - ${errorText}`);
throw new AppError('CONVERSION_FAILED', {
message: `Gotenberg returned status ${response.status}: ${errorText}`,
userMessage:
'Failed to convert the file to PDF. Please try again or upload a PDF file instead.',
statusCode: 400,
});
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof AppError) {
throw error;
}
if (error instanceof Error && error.name === 'AbortError') {
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
message: 'Gotenberg request timed out',
userMessage:
'File conversion timed out. Please try again with a smaller file or upload a PDF instead.',
statusCode: 503,
});
}
console.error('Gotenberg conversion error:', error);
throw new AppError('CONVERSION_SERVICE_UNAVAILABLE', {
message: `Failed to reach Gotenberg: ${error instanceof Error ? error.message : 'Unknown error'}`,
userMessage:
'File conversion service is temporarily unavailable. Please upload a PDF file instead.',
statusCode: 503,
});
}
};
@@ -477,9 +477,11 @@ export const createDocumentFromTemplate = async ({
}
const duplicatedFile = await putNormalizedPdfFileServerSide({
name: titleToUse,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(buffer),
file: {
name: titleToUse,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(buffer),
},
});
const newDocumentData = await prisma.documentData.create({
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -47,6 +47,7 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
id: true,
data: true,
initialData: true,
originalMimeType: true,
}).extend({
envelopeItemId: z.string(),
}),
+1
View File
@@ -41,6 +41,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
id: true,
data: true,
initialData: true,
originalMimeType: true,
}).extend({
envelopeItemId: z.string(),
}),
@@ -15,11 +15,17 @@ type File = {
arrayBuffer: () => Promise<ArrayBuffer>;
};
type PutPdfFileOptions = {
initialData?: string;
originalData?: string;
originalMimeType?: string;
};
/**
* Uploads a document file to the appropriate storage location and creates
* a document data record.
*/
export const putPdfFileServerSide = async (file: File, initialData?: string) => {
export const putPdfFileServerSide = async (file: File, options: PutPdfFileOptions = {}) => {
const isEncryptedDocumentsAllowed = false; // Was feature flag.
const arrayBuffer = await file.arrayBuffer();
@@ -40,7 +46,13 @@ export const putPdfFileServerSide = async (file: File, initialData?: string) =>
const { type, data } = await putFileServerSide(file);
const createdData = await createDocumentData({ type, data, initialData });
const createdData = await createDocumentData({
type,
data,
initialData: options.initialData,
originalData: options.originalData,
originalMimeType: options.originalMimeType,
});
return {
documentData: createdData,
@@ -48,13 +60,25 @@ export const putPdfFileServerSide = async (file: File, initialData?: string) =>
};
};
type PutNormalizedPdfOptions = {
file: File;
originalData?: string;
originalMimeType?: string;
flattenForm?: boolean;
};
/**
* Uploads a pdf file and normalizes it.
*/
export const putNormalizedPdfFileServerSide = async (file: File, options: { flattenForm?: boolean } = {}) => {
export const putNormalizedPdfFileServerSide = async ({
file,
originalData,
originalMimeType,
flattenForm = true,
}: PutNormalizedPdfOptions) => {
const buffer = Buffer.from(await file.arrayBuffer());
const normalized = await normalizePdf(buffer, options);
const normalized = await normalizePdf(buffer, { flattenForm });
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
@@ -67,6 +91,8 @@ export const putNormalizedPdfFileServerSide = async (file: File, options: { flat
return await createDocumentData({
type: documentData.type,
data: documentData.data,
originalData,
originalMimeType,
});
};
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentData" ADD COLUMN "originalMimeType" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentData" ADD COLUMN "originalData" TEXT;
+7 -5
View File
@@ -495,11 +495,13 @@ enum DocumentSigningOrder {
}
model DocumentData {
id String @id @default(cuid())
type DocumentDataType
data String
initialData String
envelopeItem EnvelopeItem?
id String @id @default(cuid())
type DocumentDataType
data String
initialData String
originalData String?
originalMimeType String?
envelopeItem EnvelopeItem?
}
enum DocumentDistributionMethod {
@@ -1,8 +1,9 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { convertToPdfIfNeeded } from '@documenso/lib/server-only/file-conversion/convert-to-pdf';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { putFileServerSide, putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { EnvelopeType } from '@prisma/client';
@@ -35,7 +36,9 @@ export const createDocumentRoute = authenticatedProcedure
attachments,
} = payload;
let pdf = Buffer.from(await file.arrayBuffer());
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
let pdf = pdfBuffer;
if (formValues) {
// eslint-disable-next-line require-atomic-updates
@@ -45,10 +48,24 @@ export const createDocumentRoute = authenticatedProcedure
});
}
let originalData: string | undefined;
if (originalBuffer) {
const stored = await putFileServerSide({
name: `original-${file.name}`,
type: originalMimeType,
arrayBuffer: async () => Promise.resolve(originalBuffer),
});
originalData = stored.data;
}
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
file: {
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(pdf),
},
originalData,
originalMimeType,
});
ctx.logger.info({
@@ -17,6 +17,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
id: true,
data: true,
initialData: true,
originalMimeType: true,
}),
documentMeta: DocumentMetaSchema.pick({
signingOrder: true,
@@ -1,10 +1,11 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { convertToPdfIfNeeded } from '@documenso/lib/server-only/file-conversion/convert-to-pdf';
import { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { putFileServerSide, putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { EnvelopeType } from '@prisma/client';
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
@@ -96,17 +97,12 @@ export const createEnvelopeRouteCaller = async ({
});
}
if (files.some((file) => !file.type.startsWith('application/pdf'))) {
throw new AppError('INVALID_DOCUMENT_FILE', {
message: 'You cannot upload non-PDF files',
statusCode: 400,
});
}
// For each file: normalize, extract & clean placeholders, then upload.
// For each file: convert to PDF if needed, normalize, extract & clean placeholders, then upload.
const envelopeItems = await Promise.all(
files.map(async (file) => {
let pdf = Buffer.from(await file.arrayBuffer());
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
let pdf = pdfBuffer;
if (formValues) {
// eslint-disable-next-line require-atomic-updates
@@ -123,11 +119,27 @@ export const createEnvelopeRouteCaller = async ({
// Todo: Embeds - Might need to add this for client-side embeds in the future.
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
const { documentData } = await putPdfFileServerSide({
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(cleanedPdf),
});
let originalData: string | undefined;
if (originalBuffer) {
const stored = await putFileServerSide({
name: `original-${file.name}`,
type: originalMimeType,
arrayBuffer: async () => Promise.resolve(originalBuffer),
});
originalData = stored.data;
}
const { documentData } = await putPdfFileServerSide(
{
name: file.name,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(cleanedPdf),
},
{
originalData,
originalMimeType: originalBuffer ? originalMimeType : undefined,
},
);
return {
title: file.name,
@@ -18,6 +18,7 @@ export const ZGetEnvelopeItemsResponseSchema = z.object({
id: true,
data: true,
initialData: true,
originalMimeType: true,
}),
})
.array(),
@@ -74,7 +74,8 @@ export const useEnvelopeRoute = authenticatedProcedure
const uploadedFiles = await Promise.all(
filesToUpload.map(async (file) => {
// We disable flattening here since `createDocumentFromTemplate` will handle it.
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file, {
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
file,
flattenForm: false,
});
@@ -269,7 +269,8 @@ export const templateRouter = router({
attachments,
} = payload;
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file, {
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide({
file,
flattenForm: false,
});
+3 -4
View File
@@ -1,5 +1,6 @@
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
@@ -54,9 +55,7 @@ export const DocumentDropzone = ({
const organisation = useCurrentOrganisation();
const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
accept: ALLOWED_UPLOAD_MIME_TYPES,
multiple: allowMultiple,
disabled,
onDrop: (acceptedFiles) => {
@@ -151,7 +150,7 @@ export const DocumentDropzone = ({
<p className="mt-6 font-medium text-foreground">{_(heading[type])}</p>
<p className="mt-1 text-center text-muted-foreground/80 text-sm">
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}
{_(disabled ? disabledMessage : msg`Drag & drop PDF, DOCX, or images here.`)}
</p>
{disabled && IS_BILLING_ENABLED() && (
@@ -1,6 +1,7 @@
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { ALLOWED_UPLOAD_MIME_TYPES } from '@documenso/lib/constants/upload';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import type { MessageDescriptor } from '@lingui/core';
@@ -51,9 +52,7 @@ export const DocumentUploadButton = ({
const isPersonalLayoutMode = isPersonalLayout(organisations);
const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
},
accept: ALLOWED_UPLOAD_MIME_TYPES,
multiple: internalVersion === '2',
disabled,
maxFiles,