mirror of
https://github.com/documenso/documenso.git
synced 2026-07-12 14:05:20 +10:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f5903760 | |||
| f153a7c437 | |||
| f9cb8d84ed | |||
| cc67454513 | |||
| 665a0d0ea0 | |||
| 02ff8df09b | |||
| d4bee4aed1 | |||
| f4cfa71379 | |||
| b1763e422c | |||
| 64e0695811 | |||
| 8a4205d808 | |||
| 74db3d7a1c | |||
| 3976531045 | |||
| 7a499270be |
@@ -4,6 +4,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
|
|||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
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 { 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 { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
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.`,
|
() => 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('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.`);
|
.otherwise(() => t`An error occurred during upload.`);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -158,9 +165,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
accept: {
|
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||||
'application/pdf': ['.pdf'],
|
|
||||||
},
|
|
||||||
multiple: true,
|
multiple: true,
|
||||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||||
maxFiles: maximumEnvelopeItemCount,
|
maxFiles: maximumEnvelopeItemCount,
|
||||||
@@ -183,7 +188,7 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mt-4 text-md text-muted-foreground">
|
<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>
|
</p>
|
||||||
|
|
||||||
{isUploadDisabled && IS_BILLING_ENABLED() && (
|
{isUploadDisabled && IS_BILLING_ENABLED() && (
|
||||||
|
|||||||
@@ -114,6 +114,18 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
|||||||
|
|
||||||
const errorMessage = match(error.code)
|
const errorMessage = match(error.code)
|
||||||
.with('INVALID_DOCUMENT_FILE', () => t`You cannot upload encrypted PDFs.`)
|
.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(
|
.with(
|
||||||
AppErrorCode.LIMIT_EXCEEDED,
|
AppErrorCode.LIMIT_EXCEEDED,
|
||||||
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
() => t`You have reached your document limit for this month. Please upgrade your plan.`,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getFileExtensionForMimeType } from '@documenso/lib/constants/upload';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
|
import { generatePartialSignedPdf } from '@documenso/lib/server-only/pdf/generate-partial-signed-pdf';
|
||||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
@@ -24,6 +25,8 @@ type DocumentDataInput = {
|
|||||||
type: DocumentDataType;
|
type: DocumentDataType;
|
||||||
data: string;
|
data: string;
|
||||||
initialData: string;
|
initialData: string;
|
||||||
|
originalData?: string | null;
|
||||||
|
originalMimeType?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EnvelopeForPendingDownload = {
|
type EnvelopeForPendingDownload = {
|
||||||
@@ -84,7 +87,19 @@ const handleStaticFileRequest = async ({
|
|||||||
isDownload,
|
isDownload,
|
||||||
context: c,
|
context: c,
|
||||||
}: StaticFileRequestOptions) => {
|
}: 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');
|
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||||
|
|
||||||
@@ -105,7 +120,7 @@ const handleStaticFileRequest = async ({
|
|||||||
return c.json({ error: 'File not found' }, 404);
|
return c.json({ error: 'File not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
c.header('Content-Type', 'application/pdf');
|
c.header('Content-Type', contentType);
|
||||||
c.header('ETag', etag);
|
c.header('ETag', etag);
|
||||||
|
|
||||||
if (!isDownload) {
|
if (!isDownload) {
|
||||||
@@ -117,10 +132,17 @@ const handleStaticFileRequest = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDownload) {
|
if (isDownload) {
|
||||||
// Generate filename following the pattern from envelope-download-dialog.tsx
|
const baseTitle = title.replace(/\.[^/.]+$/, '');
|
||||||
const baseTitle = title.replace(/\.pdf$/, '');
|
|
||||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
let filename: string;
|
||||||
const filename = `${baseTitle}${suffix}`;
|
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));
|
c.header('Content-Disposition', contentDisposition(filename));
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
return c.json({ error: 'File too large' }, 400);
|
return c.json({ error: 'File too large' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await putNormalizedPdfFileServerSide(file);
|
const result = await putNormalizedPdfFileServerSide({ file });
|
||||||
|
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
} catch (error) {
|
} 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 |
@@ -48,6 +48,15 @@ services:
|
|||||||
entrypoint: sh
|
entrypoint: sh
|
||||||
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
|
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:
|
volumes:
|
||||||
minio:
|
minio:
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
@@ -814,9 +814,11 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newDocumentData = await putNormalizedPdfFileServerSide({
|
const newDocumentData = await putNormalizedPdfFileServerSide({
|
||||||
name: fileName,
|
file: {
|
||||||
type: 'application/pdf',
|
name: fileName,
|
||||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.envelopeItem.update({
|
await prisma.envelopeItem.update({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type DownloadPDFProps = {
|
|||||||
/**
|
/**
|
||||||
* Specifies which version of the document to download.
|
* Specifies which version of the document to download.
|
||||||
* 'signed': Downloads the signed version (default).
|
* '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.
|
* 'pending': Downloads the original document with currently-inserted fields burned in.
|
||||||
* Only valid while the envelope is in PENDING status. Not supported via
|
* Only valid while the envelope is in PENDING status. Not supported via
|
||||||
* recipient token.
|
* recipient token.
|
||||||
@@ -21,6 +21,29 @@ type DownloadPDFProps = {
|
|||||||
version?: DocumentVersion;
|
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 => {
|
const versionToFilenameSuffix = (version: DocumentVersion): string => {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case 'signed':
|
case 'signed':
|
||||||
@@ -40,12 +63,22 @@ export const downloadPDF = async ({ envelopeItem, token, fileName, version = 'si
|
|||||||
version,
|
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({
|
downloadFile({
|
||||||
filename: `${baseTitle}${versionToFilenameSuffix(version)}`,
|
filename,
|
||||||
data: blob,
|
data: blob,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
type: 'application/pdf',
|
||||||
arrayBuffer: async () => Promise.resolve(pdfBytes),
|
arrayBuffer: async () => Promise.resolve(pdfBytes),
|
||||||
},
|
},
|
||||||
envelopeItem.documentData.initialData,
|
{ initialData: envelopeItem.documentData.initialData },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,14 +11,24 @@ export type CreateDocumentDataOptions = {
|
|||||||
* If not provided, the current data will be used.
|
* If not provided, the current data will be used.
|
||||||
*/
|
*/
|
||||||
initialData?: string;
|
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({
|
return await prisma.documentData.create({
|
||||||
data: {
|
data: {
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
initialData: initialData || data,
|
initialData: initialData || data,
|
||||||
|
originalData,
|
||||||
|
originalMimeType,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
|||||||
type: true,
|
type: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
|
originalData: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
|
|||||||
id: true,
|
id: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
|
originalData: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -336,9 +338,11 @@ const injectFormValuesIntoDocument = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newDocumentData = await putNormalizedPdfFileServerSide({
|
const newDocumentData = await putNormalizedPdfFileServerSide({
|
||||||
name: fileName,
|
file: {
|
||||||
type: 'application/pdf',
|
name: fileName,
|
||||||
arrayBuffer: async () => Promise.resolve(prefilled),
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.envelopeItem.update({
|
await prisma.envelopeItem.update({
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId, overrides }: Dupli
|
|||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
type: 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({
|
const duplicatedFile = await putNormalizedPdfFileServerSide({
|
||||||
name: titleToUse,
|
file: {
|
||||||
type: 'application/pdf',
|
name: titleToUse,
|
||||||
arrayBuffer: async () => Promise.resolve(buffer),
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(buffer),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const newDocumentData = await prisma.documentData.create({
|
const newDocumentData = await prisma.documentData.create({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+100
-332
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
+109
-339
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+121
-351
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+100
-330
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
|||||||
id: true,
|
id: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
envelopeItemId: z.string(),
|
envelopeItemId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
|||||||
id: true,
|
id: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
envelopeItemId: z.string(),
|
envelopeItemId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,11 +15,17 @@ type File = {
|
|||||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PutPdfFileOptions = {
|
||||||
|
initialData?: string;
|
||||||
|
originalData?: string;
|
||||||
|
originalMimeType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a document file to the appropriate storage location and creates
|
* Uploads a document file to the appropriate storage location and creates
|
||||||
* a document data record.
|
* 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 isEncryptedDocumentsAllowed = false; // Was feature flag.
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
@@ -40,7 +46,13 @@ export const putPdfFileServerSide = async (file: File, initialData?: string) =>
|
|||||||
|
|
||||||
const { type, data } = await putFileServerSide(file);
|
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 {
|
return {
|
||||||
documentData: createdData,
|
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.
|
* 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 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`;
|
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({
|
return await createDocumentData({
|
||||||
type: documentData.type,
|
type: documentData.type,
|
||||||
data: documentData.data,
|
data: documentData.data,
|
||||||
|
originalData,
|
||||||
|
originalMimeType,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentData" ADD COLUMN "originalMimeType" TEXT;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DocumentData" ADD COLUMN "originalData" TEXT;
|
||||||
@@ -495,11 +495,13 @@ enum DocumentSigningOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model DocumentData {
|
model DocumentData {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
type DocumentDataType
|
type DocumentDataType
|
||||||
data String
|
data String
|
||||||
initialData String
|
initialData String
|
||||||
envelopeItem EnvelopeItem?
|
originalData String?
|
||||||
|
originalMimeType String?
|
||||||
|
envelopeItem EnvelopeItem?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DocumentDistributionMethod {
|
enum DocumentDistributionMethod {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
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 { 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 { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
|
||||||
@@ -35,7 +36,9 @@ export const createDocumentRoute = authenticatedProcedure
|
|||||||
attachments,
|
attachments,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
let pdf = Buffer.from(await file.arrayBuffer());
|
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||||
|
|
||||||
|
let pdf = pdfBuffer;
|
||||||
|
|
||||||
if (formValues) {
|
if (formValues) {
|
||||||
// eslint-disable-next-line require-atomic-updates
|
// 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({
|
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||||
name: file.name,
|
file: {
|
||||||
type: 'application/pdf',
|
name: file.name,
|
||||||
arrayBuffer: async () => Promise.resolve(pdf),
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(pdf),
|
||||||
|
},
|
||||||
|
originalData,
|
||||||
|
originalMimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
|
|||||||
id: true,
|
id: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
}),
|
}),
|
||||||
documentMeta: DocumentMetaSchema.pick({
|
documentMeta: DocumentMetaSchema.pick({
|
||||||
signingOrder: true,
|
signingOrder: true,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
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 { extractPdfPlaceholders } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||||
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
import { normalizePdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
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 { EnvelopeType } from '@prisma/client';
|
||||||
|
|
||||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
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'))) {
|
// For each file: convert to PDF if needed, normalize, extract & clean placeholders, then upload.
|
||||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
|
||||||
message: 'You cannot upload non-PDF files',
|
|
||||||
statusCode: 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each file: normalize, extract & clean placeholders, then upload.
|
|
||||||
const envelopeItems = await Promise.all(
|
const envelopeItems = await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
let pdf = Buffer.from(await file.arrayBuffer());
|
const { pdfBuffer, originalBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||||
|
|
||||||
|
let pdf = pdfBuffer;
|
||||||
|
|
||||||
if (formValues) {
|
if (formValues) {
|
||||||
// eslint-disable-next-line require-atomic-updates
|
// 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.
|
// Todo: Embeds - Might need to add this for client-side embeds in the future.
|
||||||
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
const { cleanedPdf, placeholders } = await extractPdfPlaceholders(normalized);
|
||||||
|
|
||||||
const { documentData } = await putPdfFileServerSide({
|
let originalData: string | undefined;
|
||||||
name: file.name,
|
if (originalBuffer) {
|
||||||
type: 'application/pdf',
|
const stored = await putFileServerSide({
|
||||||
arrayBuffer: async () => Promise.resolve(cleanedPdf),
|
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 {
|
return {
|
||||||
title: file.name,
|
title: file.name,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const ZGetEnvelopeItemsResponseSchema = z.object({
|
|||||||
id: true,
|
id: true,
|
||||||
data: true,
|
data: true,
|
||||||
initialData: true,
|
initialData: true,
|
||||||
|
originalMimeType: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ export const useEnvelopeRoute = authenticatedProcedure
|
|||||||
const uploadedFiles = await Promise.all(
|
const uploadedFiles = await Promise.all(
|
||||||
filesToUpload.map(async (file) => {
|
filesToUpload.map(async (file) => {
|
||||||
// We disable flattening here since `createDocumentFromTemplate` will handle it.
|
// We disable flattening here since `createDocumentFromTemplate` will handle it.
|
||||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file, {
|
const { id: documentDataId } = await putNormalizedPdfFileServerSide({
|
||||||
|
file,
|
||||||
flattenForm: false,
|
flattenForm: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,8 @@ export const templateRouter = router({
|
|||||||
attachments,
|
attachments,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file, {
|
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide({
|
||||||
|
file,
|
||||||
flattenForm: false,
|
flattenForm: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
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 { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
@@ -54,9 +55,7 @@ export const DocumentDropzone = ({
|
|||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
accept: {
|
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||||
'application/pdf': ['.pdf'],
|
|
||||||
},
|
|
||||||
multiple: allowMultiple,
|
multiple: allowMultiple,
|
||||||
disabled,
|
disabled,
|
||||||
onDrop: (acceptedFiles) => {
|
onDrop: (acceptedFiles) => {
|
||||||
@@ -151,7 +150,7 @@ export const DocumentDropzone = ({
|
|||||||
<p className="mt-6 font-medium text-foreground">{_(heading[type])}</p>
|
<p className="mt-6 font-medium text-foreground">{_(heading[type])}</p>
|
||||||
|
|
||||||
<p className="mt-1 text-center text-muted-foreground/80 text-sm">
|
<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>
|
</p>
|
||||||
|
|
||||||
{disabled && IS_BILLING_ENABLED() && (
|
{disabled && IS_BILLING_ENABLED() && (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
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 { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
|
||||||
import type { MessageDescriptor } from '@lingui/core';
|
import type { MessageDescriptor } from '@lingui/core';
|
||||||
@@ -51,9 +52,7 @@ export const DocumentUploadButton = ({
|
|||||||
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
const isPersonalLayoutMode = isPersonalLayout(organisations);
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
accept: {
|
accept: ALLOWED_UPLOAD_MIME_TYPES,
|
||||||
'application/pdf': ['.pdf'],
|
|
||||||
},
|
|
||||||
multiple: internalVersion === '2',
|
multiple: internalVersion === '2',
|
||||||
disabled,
|
disabled,
|
||||||
maxFiles,
|
maxFiles,
|
||||||
|
|||||||
Reference in New Issue
Block a user