Files
documenso/packages/ui/primitives/document-upload-button.tsx
T
Lucas Smith bc184d445f feat: support DOCX uploads via Gotenberg (#2801)
Uploaded .docx files are converted to PDF on the server using a
Gotenberg
sidecar before entering the normal envelope pipeline. The feature is
opt-in via NEXT_PRIVATE_DOCUMENT_CONVERSION_URL; when unset, only PDF
uploads are accepted.

A per-process circuit breaker opens for 30s after a conversion failure
to shed load.

Ships a dev Dockerfile that layers Microsoft Core Fonts and additional
language fonts
onto the upstream Gotenberg image for better fidelity.

Co-authored-by: Ephraim Duncan
<55143799+ephraimduncan@users.noreply.github.com>

Co-authored-by: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com>
2026-05-13 15:06:21 +10:00

102 lines
3.4 KiB
TypeScript

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 { getAllowedUploadMimeTypes } from '@documenso/lib/constants/document-conversion';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { Upload } from 'lucide-react';
import type { DropEvent, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { Link } from 'react-router';
import { Button } from './button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';
export type DocumentUploadButtonProps = {
className?: string;
disabled?: boolean;
loading?: boolean;
disabledMessage?: MessageDescriptor;
onDrop?: (_files: File[]) => void | Promise<void>;
onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
type: EnvelopeType;
internalVersion: '1' | '2';
maxFiles?: number;
[key: string]: unknown;
};
export const DocumentUploadButton = ({
className,
loading,
onDrop,
onDropRejected,
disabled,
disabledMessage = msg`You cannot upload documents at this time.`,
type,
internalVersion,
maxFiles,
...props
}: DocumentUploadButtonProps) => {
const { _ } = useLingui();
const { organisations } = useSession();
const organisation = useCurrentOrganisation();
const isPersonalLayoutMode = isPersonalLayout(organisations);
const { getRootProps, getInputProps } = useDropzone({
accept: getAllowedUploadMimeTypes(),
multiple: internalVersion === '2',
disabled,
maxFiles,
onDrop: (acceptedFiles) => {
if (acceptedFiles.length > 0 && onDrop) {
void onDrop(acceptedFiles);
}
},
onDropRejected,
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
});
const heading = {
[EnvelopeType.DOCUMENT]: internalVersion === '1' ? msg`Document (Legacy)` : msg`Upload Document`,
[EnvelopeType.TEMPLATE]: internalVersion === '1' ? msg`Template (Legacy)` : msg`Upload Template`,
};
if (disabled && IS_BILLING_ENABLED()) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button className="bg-warning hover:bg-warning/80" asChild>
<Link to={isPersonalLayoutMode ? `/settings/billing` : `/o/${organisation.url}/settings/billing`}>
<Trans>Upgrade</Trans>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-sm">{_(disabledMessage)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<Button loading={loading} aria-disabled={disabled} {...getRootProps()} {...props}>
<div className="flex items-center gap-2">
<input data-testid="document-upload-input" {...getInputProps()} />
{!loading && <Upload className="h-4 w-4" />}
{disabled ? _(disabledMessage) : _(heading[type])}
</div>
</Button>
);
};