mirror of
https://github.com/documenso/documenso.git
synced 2026-07-11 21:45:18 +10:00
feat: document file conversion
This commit is contained in:
@@ -127,6 +127,19 @@ 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.`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -40,6 +40,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:
|
||||
documenso_database:
|
||||
|
||||
@@ -384,11 +384,9 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page })
|
||||
|
||||
await page.getByText('Upload Template Document').click();
|
||||
|
||||
await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' });
|
||||
|
||||
await page
|
||||
.locator('input[type="file"]')
|
||||
.nth(0)
|
||||
.first()
|
||||
.setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf'));
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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');
|
||||
@@ -5,14 +5,20 @@ import { prisma } from '@documenso/prisma';
|
||||
export type CreateDocumentDataOptions = {
|
||||
type: DocumentDataType;
|
||||
data: string;
|
||||
originalMimeType?: string;
|
||||
};
|
||||
|
||||
export const createDocumentData = async ({ type, data }: CreateDocumentDataOptions) => {
|
||||
export const createDocumentData = async ({
|
||||
type,
|
||||
data,
|
||||
originalMimeType,
|
||||
}: CreateDocumentDataOptions) => {
|
||||
return await prisma.documentData.create({
|
||||
data: {
|
||||
type,
|
||||
data,
|
||||
initialData: data,
|
||||
originalMimeType,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok
|
||||
type: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -82,6 +82,7 @@ export const sendDocument = async ({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
|
||||
data: true,
|
||||
initialData: true,
|
||||
type: true,
|
||||
originalMimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
if (originalMimeType === 'application/pdf') {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
return {
|
||||
pdfBuffer: Buffer.from(arrayBuffer),
|
||||
originalMimeType,
|
||||
};
|
||||
}
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const pdfBuffer = await convertFileToPdfViaGotenberg({
|
||||
file: buffer,
|
||||
filename: file.name,
|
||||
mimeType: originalMimeType,
|
||||
});
|
||||
|
||||
return {
|
||||
pdfBuffer,
|
||||
originalMimeType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { getGotenbergUrl } from '../../constants/upload';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
|
||||
export type ConvertFileToPdfOptions = {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
const CONVERSION_TIMEOUT_MS = 30_000;
|
||||
|
||||
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(), CONVERSION_TIMEOUT_MS);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}).extend({
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}).extend({
|
||||
envelopeItemId: z.string(),
|
||||
}),
|
||||
|
||||
@@ -44,10 +44,18 @@ export const putPdfFileServerSide = async (file: File) => {
|
||||
return await createDocumentData({ type, data });
|
||||
};
|
||||
|
||||
type PutNormalizedPdfOptions = {
|
||||
file: File;
|
||||
originalMimeType?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a pdf file and normalizes it.
|
||||
*/
|
||||
export const putNormalizedPdfFileServerSide = async (file: File) => {
|
||||
export const putNormalizedPdfFileServerSide = async ({
|
||||
file,
|
||||
originalMimeType,
|
||||
}: PutNormalizedPdfOptions) => {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const normalized = await normalizePdf(buffer);
|
||||
@@ -63,6 +71,7 @@ export const putNormalizedPdfFileServerSide = async (file: File) => {
|
||||
return await createDocumentData({
|
||||
type: documentData.type,
|
||||
data: documentData.data,
|
||||
originalMimeType,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentData" ADD COLUMN "originalMimeType" TEXT;
|
||||
@@ -486,11 +486,12 @@ 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
|
||||
originalMimeType String?
|
||||
envelopeItem EnvelopeItem?
|
||||
}
|
||||
|
||||
enum DocumentDistributionMethod {
|
||||
|
||||
@@ -47,9 +47,11 @@ export const createDocumentRoute = authenticatedProcedure
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
ctx.logger.info({
|
||||
|
||||
@@ -18,6 +18,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}),
|
||||
documentMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
|
||||
@@ -87,7 +87,7 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
|
||||
// For each file, stream to s3 and create the document data.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({ file });
|
||||
|
||||
return {
|
||||
title: file.name,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
@@ -59,17 +60,13 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
});
|
||||
}
|
||||
|
||||
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, stream to s3 and create the document data.
|
||||
// For each file, convert to PDF if needed, then store.
|
||||
const envelopeItems = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
let pdf = Buffer.from(await file.arrayBuffer());
|
||||
// Convert to PDF if needed (DOCX, images, etc)
|
||||
const { pdfBuffer, originalMimeType } = await convertToPdfIfNeeded(file);
|
||||
|
||||
let pdf = pdfBuffer;
|
||||
|
||||
if (formValues) {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
@@ -80,9 +77,12 @@ export const createEnvelopeRoute = authenticatedProcedure
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
originalMimeType,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const ZGetEnvelopeItemsResponseSchema = z.object({
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
originalMimeType: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
|
||||
@@ -79,7 +79,7 @@ export const useEnvelopeRoute = authenticatedProcedure
|
||||
// Process uploaded files and create document data for them
|
||||
const uploadedFiles = await Promise.all(
|
||||
filesToUpload.map(async (file) => {
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||
const { id: documentDataId } = await putNormalizedPdfFileServerSide({ file });
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
|
||||
@@ -191,7 +191,7 @@ export const templateRouter = router({
|
||||
attachments,
|
||||
} = payload;
|
||||
|
||||
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide({ file });
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
|
||||
@@ -17,7 +17,7 @@ const EnvelopePdfViewer = lazy(async () => import('./pdf-viewer-konva'));
|
||||
|
||||
export const PDFViewerKonvaLazy = (props: PDFViewerProps) => {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading client component...</div>}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<EnvelopePdfViewer {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Link } from 'react-router';
|
||||
|
||||
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 {
|
||||
@@ -55,9 +56,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) => {
|
||||
@@ -87,7 +86,7 @@ export const DocumentDropzone = ({
|
||||
<Card
|
||||
role="button"
|
||||
className={cn(
|
||||
'focus-visible:ring-ring ring-offset-background group flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
'group flex flex-1 cursor-pointer flex-col items-center justify-center ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
gradient={!disabled}
|
||||
@@ -96,77 +95,77 @@ export const DocumentDropzone = ({
|
||||
{...getRootProps()}
|
||||
{...props}
|
||||
>
|
||||
<CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6 text-muted-foreground/40">
|
||||
{disabled ? (
|
||||
// Disabled State
|
||||
<div className="flex">
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="group-hover:bg-destructive/2 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneDisabledCardLeftVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
<div className="h-2 w-5/6 rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/5 border-muted-foreground/20 group-hover:border-destructive/50 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/50 group-hover:bg-destructive/5 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneDisabledCardCenterVariants}
|
||||
>
|
||||
<AlertTriangle
|
||||
strokeWidth="2px"
|
||||
className="text-muted-foreground/20 group-hover:text-destructive h-12 w-12"
|
||||
className="h-12 w-12 text-muted-foreground/20 group-hover:text-destructive"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="group-hover:bg-destructive/2 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-destructive/10 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneDisabledCardRightVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
<div className="h-2 w-5/6 rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/10 group-hover:bg-destructive/10" />
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
// Non Disabled State
|
||||
<div className="flex">
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneCardLeftVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
<div className="h-2 w-5/6 rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneCardCenterVariants}
|
||||
>
|
||||
<Plus
|
||||
strokeWidth="2px"
|
||||
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
|
||||
className="h-12 w-12 text-muted-foreground/20 group-hover:text-documenso"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
className="z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border border-muted-foreground/20 bg-white/80 px-2 py-4 backdrop-blur-sm group-hover:border-documenso/80 dark:bg-muted/80"
|
||||
variants={DocumentDropzoneCardRightVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
<div className="h-2 w-5/6 rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
<div className="h-2 w-full rounded-[2px] bg-muted-foreground/20 group-hover:bg-documenso" />
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<p className="text-foreground mt-6 font-medium">{_(heading[type])}</p>
|
||||
<p className="mt-6 font-medium text-foreground">{_(heading[type])}</p>
|
||||
|
||||
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
|
||||
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}
|
||||
<p className="mt-1 text-center text-sm text-muted-foreground/80">
|
||||
{_(disabled ? disabledMessage : msg`Drag & drop PDF, DOCX, or images here.`)}
|
||||
</p>
|
||||
|
||||
{disabled && IS_BILLING_ENABLED() && (
|
||||
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
|
||||
<Button className="mt-4 w-32 bg-warning hover:bg-warning/80" asChild>
|
||||
<Link to={`/o/${organisation.url}/settings/billing`}>
|
||||
<Trans>Upgrade</Trans>
|
||||
</Link>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Link } from 'react-router';
|
||||
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';
|
||||
|
||||
@@ -52,9 +53,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,
|
||||
@@ -79,7 +78,7 @@ export const DocumentUploadButton = ({
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button className="hover:bg-warning/80 bg-warning" asChild>
|
||||
<Button className="bg-warning hover:bg-warning/80" asChild>
|
||||
<Link
|
||||
to={
|
||||
isPersonalLayoutMode
|
||||
|
||||
Reference in New Issue
Block a user