Compare commits

..

3 Commits

Author SHA1 Message Date
fc2e9af6a0 fix: add preview page 2025-11-05 17:18:15 +11:00
a810d20a4f chore: update package lock 2025-11-05 16:42:42 +11:00
22011fd4ba fix: finish file stuff 2025-11-05 14:51:07 +11:00
57 changed files with 833 additions and 907 deletions

View File

@ -19,13 +19,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
type DocumentDuplicateDialogProps = { type DocumentDuplicateDialogProps = {
id: number; id: string;
token?: string;
open: boolean; open: boolean;
onOpenChange: (_open: boolean) => void; onOpenChange: (_open: boolean) => void;
}; };
export const DocumentDuplicateDialog = ({ export const DocumentDuplicateDialog = ({
id, id,
token,
open, open,
onOpenChange, onOpenChange,
}: DocumentDuplicateDialogProps) => { }: DocumentDuplicateDialogProps) => {
@ -36,42 +38,38 @@ export const DocumentDuplicateDialog = ({
const team = useCurrentTeam(); const team = useCurrentTeam();
const { data: document, isLoading } = trpcReact.document.get.useQuery( const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
trpcReact.envelope.item.getManyByToken.useQuery(
{ {
documentId: id, envelopeId: id,
access: token ? { type: 'recipient', token } : { type: 'user' },
}, },
{ {
queryHash: `document-duplicate-dialog-${id}`, enabled: open,
enabled: open === true,
}, },
); );
const documentData = document?.documentData const envelopeItems = envelopeItemsPayload?.envelopeItems || [];
? {
...document.documentData,
data: document.documentData.initialData,
}
: undefined;
const documentsPath = formatDocumentsPath(team.url); const documentsPath = formatDocumentsPath(team.url);
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } = const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
trpcReact.document.duplicate.useMutation({ trpcReact.envelope.duplicate.useMutation({
onSuccess: async ({ id }) => { onSuccess: async ({ duplicatedEnvelopeId }) => {
toast({ toast({
title: _(msg`Document Duplicated`), title: _(msg`Document Duplicated`),
description: _(msg`Your document has been successfully duplicated.`), description: _(msg`Your document has been successfully duplicated.`),
duration: 5000, duration: 5000,
}); });
await navigate(`${documentsPath}/${id}/edit`); await navigate(`${documentsPath}/${duplicatedEnvelopeId}/edit`);
onOpenChange(false); onOpenChange(false);
}, },
}); });
const onDuplicate = async () => { const onDuplicate = async () => {
try { try {
await duplicateDocument({ documentId: id }); await duplicateEnvelope({ envelopeId: id });
} catch { } catch {
toast({ toast({
title: _(msg`Something went wrong`), title: _(msg`Something went wrong`),
@ -83,14 +81,14 @@ export const DocumentDuplicateDialog = ({
}; };
return ( return (
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}> <Dialog open={open} onOpenChange={(value) => !isDuplicating && onOpenChange(value)}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
<Trans>Duplicate</Trans> <Trans>Duplicate</Trans>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
{!documentData || isLoading ? ( {isLoadingEnvelopeItems || !envelopeItems || envelopeItems.length === 0 ? (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8"> <div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
<Trans>Loading Document...</Trans> <Trans>Loading Document...</Trans>
@ -98,7 +96,12 @@ export const DocumentDuplicateDialog = ({
</div> </div>
) : ( ) : (
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll"> <div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
<PDFViewer key={document?.id} documentData={documentData} /> <PDFViewer
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={undefined}
version="original"
/>
</div> </div>
)} )}
@ -115,8 +118,8 @@ export const DocumentDuplicateDialog = ({
<Button <Button
type="button" type="button"
disabled={isDuplicateLoading || isLoading} disabled={isDuplicating}
loading={isDuplicateLoading} loading={isDuplicating}
onClick={onDuplicate} onClick={onDuplicate}
className="flex-1" className="flex-1"
> >

View File

@ -2,11 +2,10 @@ import { useState } from 'react';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client'; import { DocumentStatus, type EnvelopeItem } from '@prisma/client';
import { DownloadIcon, FileTextIcon } from 'lucide-react'; import { DownloadIcon, FileTextIcon } from 'lucide-react';
import { downloadFile } from '@documenso/lib/client-only/download-file'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
@ -20,9 +19,7 @@ import {
import { Skeleton } from '@documenso/ui/primitives/skeleton'; import { Skeleton } from '@documenso/ui/primitives/skeleton';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'title' | 'order'> & { type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'envelopeId' | 'title' | 'order'>;
documentData: DocumentData;
};
type EnvelopeDownloadDialogProps = { type EnvelopeDownloadDialogProps = {
envelopeId: string; envelopeId: string;
@ -87,19 +84,11 @@ export const EnvelopeDownloadDialog = ({
})); }));
try { try {
const downloadUrl = token await downloadPDF({
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${envelopeItemId}/download/${version}` envelopeItem,
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${envelopeItemId}/download/${version}`; token,
fileName: envelopeItem.title,
const blob = await fetch(downloadUrl).then(async (res) => await res.blob()); version,
const baseTitle = envelopeItem.title.replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
downloadFile({
filename,
data: blob,
}); });
setIsDownloadingState((prev) => ({ setIsDownloadingState((prev) => ({
@ -140,7 +129,7 @@ export const EnvelopeDownloadDialog = ({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{isLoadingEnvelopeItems ? ( {isLoadingEnvelopeItems ? (
<> <>
{Array.from({ length: 2 }).map((_, index) => ( {Array.from({ length: 1 }).map((_, index) => (
<div <div
key={index} key={index}
className="border-border bg-card flex items-center gap-2 rounded-lg border p-4" className="border-border bg-card flex items-center gap-2 rounded-lg border p-4"
@ -169,6 +158,7 @@ export const EnvelopeDownloadDialog = ({
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{/* Todo: Envelopes - Fix overflow */}
<h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4> <h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4>
<p className="text-muted-foreground mt-0.5 text-xs"> <p className="text-muted-foreground mt-0.5 text-xs">
<Trans>PDF Document</Trans> <Trans>PDF Document</Trans>

View File

@ -4,6 +4,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentData, FieldType } from '@prisma/client'; import type { DocumentData, FieldType } from '@prisma/client';
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client'; import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
import { base64 } from '@scure/base';
import { ChevronsUpDown } from 'lucide-react'; import { ChevronsUpDown } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form'; import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -12,7 +13,6 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { base64 } from '@documenso/lib/universal/base64';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers'; import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
import { useRecipientColors } from '@documenso/ui/lib/recipient-colors'; import { useRecipientColors } from '@documenso/ui/lib/recipient-colors';
@ -83,21 +83,14 @@ export const ConfigureFieldsView = ({
const normalizedDocumentData = useMemo(() => { const normalizedDocumentData = useMemo(() => {
if (documentData) { if (documentData) {
return documentData; return documentData.data;
} }
if (!configData.documentData) { if (!configData.documentData) {
return null; return null;
} }
const data = base64.encode(configData.documentData?.data); return base64.encode(configData.documentData.data);
return {
id: 'preview',
type: 'BYTES_64',
data,
initialData: data,
} satisfies DocumentData;
}, [configData.documentData]); }, [configData.documentData]);
const recipients = useMemo(() => { const recipients = useMemo(() => {
@ -541,7 +534,15 @@ export const ConfigureFieldsView = ({
<Form {...form}> <Form {...form}>
{normalizedDocumentData && ( {normalizedDocumentData && (
<div> <div>
<PDFViewer documentData={normalizedDocumentData} /> <PDFViewer
overrideData={normalizedDocumentData}
envelopeItem={{
id: '',
envelopeId: '',
}}
token={undefined}
version="signed"
/>
<ElementVisible <ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`} target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}

View File

@ -3,8 +3,8 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Recipient, Signature } from '@prisma/client'; import type { DocumentMeta, EnvelopeItem, Recipient, Signature } from '@prisma/client';
import { type DocumentData, type Field, FieldType } from '@prisma/client'; import { type Field, FieldType } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
@ -47,7 +47,7 @@ export type EmbedDirectTemplateClientPageProps = {
token: string; token: string;
envelopeId: string; envelopeId: string;
updatedAt: Date; updatedAt: Date;
documentData: DocumentData; envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
recipient: Recipient; recipient: Recipient;
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | null; metadata?: DocumentMeta | null;
@ -59,7 +59,7 @@ export const EmbedDirectTemplateClientPage = ({
token, token,
envelopeId, envelopeId,
updatedAt, updatedAt,
documentData, envelopeItems,
recipient, recipient,
fields, fields,
metadata, metadata,
@ -335,7 +335,9 @@ export const EmbedDirectTemplateClientPage = ({
{/* Viewer */} {/* Viewer */}
<div className="flex-1"> <div className="flex-1">
<PDFViewer <PDFViewer
documentData={documentData} envelopeItem={envelopeItems[0]}
token={token}
version="signed"
onDocumentLoad={() => setHasDocumentLoaded(true)} onDocumentLoad={() => setHasDocumentLoaded(true)}
/> />
</div> </div>

View File

@ -3,14 +3,8 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentMeta } from '@prisma/client'; import type { DocumentMeta, EnvelopeItem } from '@prisma/client';
import { import { type Field, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
type DocumentData,
type Field,
FieldType,
RecipientRole,
SigningStatus,
} from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
@ -50,7 +44,7 @@ export type EmbedSignDocumentClientPageProps = {
token: string; token: string;
documentId: number; documentId: number;
envelopeId: string; envelopeId: string;
documentData: DocumentData; envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
recipient: RecipientWithFields; recipient: RecipientWithFields;
fields: Field[]; fields: Field[];
completedFields: DocumentField[]; completedFields: DocumentField[];
@ -65,7 +59,7 @@ export const EmbedSignDocumentClientPage = ({
token, token,
documentId, documentId,
envelopeId, envelopeId,
documentData, envelopeItems,
recipient, recipient,
fields, fields,
completedFields, completedFields,
@ -293,7 +287,9 @@ export const EmbedSignDocumentClientPage = ({
{/* Viewer */} {/* Viewer */}
<div className="embed--DocumentViewer flex-1"> <div className="embed--DocumentViewer flex-1">
<PDFViewer <PDFViewer
documentData={documentData} envelopeItem={envelopeItems[0]}
token={token}
version="signed"
onDocumentLoad={() => setHasDocumentLoaded(true)} onDocumentLoad={() => setHasDocumentLoaded(true)}
/> />
</div> </div>

View File

@ -226,7 +226,9 @@ export const MultiSignDocumentSigningView = ({
})} })}
> >
<PDFViewer <PDFViewer
documentData={document.documentData} envelopeItem={document.envelopeItems[0]}
token={token}
version="signed"
onDocumentLoad={() => { onDocumentLoad={() => {
setHasDocumentLoaded(true); setHasDocumentLoaded(true);
onDocumentReady?.(); onDocumentReady?.();

View File

@ -153,7 +153,9 @@ export const DirectTemplatePageView = ({
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewer <PDFViewer
key={template.id} key={template.id}
documentData={template.templateDocumentData} envelopeItem={template.envelopeItems[0]}
token={directTemplateRecipient.token}
version="signed"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)} onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/> />
</CardContent> </CardContent>

View File

@ -245,7 +245,12 @@ export const DocumentSigningPageViewV1 = ({
<div className="flex-1"> <div className="flex-1">
<Card className="rounded-xl before:rounded-xl" gradient> <Card className="rounded-xl before:rounded-xl" gradient>
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewer key={documentData.id} documentData={documentData} document={document} /> <PDFViewer
key={document.envelopeItems[0].id}
envelopeItem={document.envelopeItems[0]}
token={recipient.token}
version="signed"
/>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentData, EnvelopeItem } from '@prisma/client'; import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client';
import { DownloadIcon } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { import {
@ -22,9 +23,10 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector'; import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector';
import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer'; import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer';
import { ShareDocumentDownloadButton } from '../share-document-download-button';
export type DocumentCertificateQRViewProps = { export type DocumentCertificateQRViewProps = {
documentId: number; documentId: number;
@ -34,6 +36,7 @@ export type DocumentCertificateQRViewProps = {
documentTeamUrl: string; documentTeamUrl: string;
recipientCount?: number; recipientCount?: number;
completedDate?: Date; completedDate?: Date;
token: string;
}; };
export const DocumentCertificateQRView = ({ export const DocumentCertificateQRView = ({
@ -44,6 +47,7 @@ export const DocumentCertificateQRView = ({
documentTeamUrl, documentTeamUrl,
recipientCount = 0, recipientCount = 0,
completedDate, completedDate,
token,
}: DocumentCertificateQRViewProps) => { }: DocumentCertificateQRViewProps) => {
const { data: documentViaUser } = trpc.document.get.useQuery({ const { data: documentViaUser } = trpc.document.get.useQuery({
documentId, documentId,
@ -96,11 +100,12 @@ export const DocumentCertificateQRView = ({
)} )}
{internalVersion === 2 ? ( {internalVersion === 2 ? (
<EnvelopeRenderProvider envelope={{ envelopeItems }}> <EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
<DocumentCertificateQrV2 <DocumentCertificateQrV2
title={title} title={title}
recipientCount={recipientCount} recipientCount={recipientCount}
formattedDate={formattedDate} formattedDate={formattedDate}
token={token}
/> />
</EnvelopeRenderProvider> </EnvelopeRenderProvider>
) : ( ) : (
@ -119,14 +124,27 @@ export const DocumentCertificateQRView = ({
</div> </div>
</div> </div>
<ShareDocumentDownloadButton <EnvelopeDownloadDialog
title={title} envelopeId={envelopeItems[0].envelopeId}
documentData={envelopeItems[0].documentData} envelopeStatus={DocumentStatus.COMPLETED}
envelopeItems={envelopeItems}
token={token}
trigger={
<Button type="button" variant="outline" className="flex-1">
<DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans>
</Button>
}
/> />
</div> </div>
<div className="mt-12 w-full"> <div className="mt-12 w-full">
<PDFViewer key={envelopeItems[0].id} documentData={envelopeItems[0].documentData} /> <PDFViewer
key={envelopeItems[0].id}
envelopeItem={envelopeItems[0]}
token={token}
version="signed"
/>
</div> </div>
</> </>
)} )}
@ -138,14 +156,16 @@ type DocumentCertificateQrV2Props = {
title: string; title: string;
recipientCount: number; recipientCount: number;
formattedDate: string; formattedDate: string;
token: string;
}; };
const DocumentCertificateQrV2 = ({ const DocumentCertificateQrV2 = ({
title, title,
recipientCount, recipientCount,
formattedDate, formattedDate,
token,
}: DocumentCertificateQrV2Props) => { }: DocumentCertificateQrV2Props) => {
const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender();
return ( return (
<div className="flex min-h-screen flex-col items-start"> <div className="flex min-h-screen flex-col items-start">
@ -163,12 +183,18 @@ const DocumentCertificateQrV2 = ({
</div> </div>
</div> </div>
{currentEnvelopeItem && ( <EnvelopeDownloadDialog
<ShareDocumentDownloadButton envelopeId={envelopeItems[0].envelopeId}
title={title} envelopeStatus={DocumentStatus.COMPLETED}
documentData={currentEnvelopeItem.documentData} envelopeItems={envelopeItems}
token={token}
trigger={
<Button type="button" variant="outline" className="flex-1">
<DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans>
</Button>
}
/> />
)}
</div> </div>
<div className="mt-12 w-full"> <div className="mt-12 w-full">

View File

@ -441,9 +441,10 @@ export const DocumentEditForm = ({
> >
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewer <PDFViewer
key={document.documentData.id} key={document.envelopeItems[0].id}
documentData={document.documentData} envelopeItem={document.envelopeItems[0]}
document={document} token={undefined}
version="signed"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)} onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/> />
</CardContent> </CardContent>

View File

@ -1,18 +1,14 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react'; import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TEnvelope } from '@documenso/lib/types/envelope'; import type { TEnvelope } from '@documenso/lib/types/envelope';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
@ -23,9 +19,6 @@ export type DocumentPageViewButtonProps = {
export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => { export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => {
const { user } = useSession(); const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
const recipient = envelope.recipients.find((recipient) => recipient.email === user.email); const recipient = envelope.recipients.find((recipient) => recipient.email === user.email);
const isRecipient = !!recipient; const isRecipient = !!recipient;
@ -37,25 +30,6 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
const documentsPath = formatDocumentsPath(envelope.team.url); const documentsPath = formatDocumentsPath(envelope.team.url);
const formatPath = `${documentsPath}/${envelope.id}/edit`; const formatPath = `${documentsPath}/${envelope.id}/edit`;
const onDownloadClick = async () => {
try {
// Todo; Envelopes - Support multiple items
const envelopeItem = envelope.envelopeItems[0];
if (!envelopeItem.documentData) {
throw new Error('No document available');
}
await downloadPDF({ documentData: envelopeItem.documentData, fileName: envelopeItem.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
return match({ return match({
isRecipient, isRecipient,
isPending, isPending,
@ -95,7 +69,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
</Link> </Link>
</Button> </Button>
)) ))
.with({ isComplete: true, internalVersion: 2 }, () => ( .with({ isComplete: true }, () => (
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
envelopeId={envelope.id} envelopeId={envelope.id}
envelopeStatus={envelope.status} envelopeStatus={envelope.status}
@ -109,11 +83,5 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
} }
/> />
)) ))
.with({ isComplete: true }, () => (
<Button className="w-full" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
))
.otherwise(() => null); .otherwise(() => null);
}; };

View File

@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DocumentStatus } from '@prisma/client'; import { DocumentStatus } from '@prisma/client';
@ -16,13 +15,11 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Link, useNavigate } from 'react-router'; import { Link, useNavigate } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TEnvelope } from '@documenso/lib/types/envelope'; import type { TEnvelope } from '@documenso/lib/types/envelope';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { import {
DropdownMenu, DropdownMenu,
@ -67,64 +64,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
const documentsPath = formatDocumentsPath(team.url); const documentsPath = formatDocumentsPath(team.url);
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.get.query(
{
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
{
context: {
teamId: team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: envelope.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
const onDownloadOriginalClick = async () => {
try {
const documentWithData = await trpcClient.document.get.query(
{
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
},
{
context: {
teamId: team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: envelope.title, version: 'original' });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED'); const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return ( return (
@ -147,7 +86,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{envelope.internalVersion === 2 ? (
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
envelopeId={envelope.id} envelopeId={envelope.id}
envelopeStatus={envelope.status} envelopeStatus={envelope.status}
@ -162,21 +100,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
</DropdownMenuItem> </DropdownMenuItem>
} }
/> />
) : (
<>
{isComplete && (
<DropdownMenuItem onClick={onDownloadClick}>
<Download className="mr-2 h-4 w-4" />
<Trans>Download</Trans>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onDownloadOriginalClick}>
<Download className="mr-2 h-4 w-4" />
<Trans>Download Original</Trans>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link to={`${documentsPath}/${envelope.id}/logs`}> <Link to={`${documentsPath}/${envelope.id}/logs`}>
@ -250,7 +173,8 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
{isDuplicateDialogOpen && ( {isDuplicateDialogOpen && (
<DocumentDuplicateDialog <DocumentDuplicateDialog
id={mapSecondaryIdToDocumentId(envelope.secondaryId)} id={envelope.id}
token={recipient?.token}
open={isDuplicateDialogOpen} open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen} onOpenChange={setDuplicateDialogOpen}
/> />

View File

@ -1,10 +1,20 @@
import { lazy, useEffect, useState } from 'react'; import { lazy, useEffect, useMemo, useState } from 'react';
import { faker } from '@faker-js/faker/locale/en';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { ConstructionIcon, FileTextIcon } from 'lucide-react'; import { FieldType } from '@prisma/client';
import { FileTextIcon } from 'lucide-react';
import { match } from 'ts-pattern';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import {
EnvelopeRenderProvider,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
@ -15,15 +25,169 @@ import { EnvelopeRendererFileSelector } from './envelope-file-selector';
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer')); const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
// Todo: Envelopes - Dynamically import faker
export const EnvelopeEditorPreviewPage = () => { export const EnvelopeEditorPreviewPage = () => {
const { envelope, editorFields } = useCurrentEnvelopeEditor(); const { envelope, editorFields } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>( const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
'recipient', 'recipient',
); );
const fieldsWithPlaceholders = useMemo(() => {
return fields.map((field) => {
const fieldMeta = ZFieldAndMetaSchema.parse(field);
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
if (!recipient) {
throw new Error('Recipient not found');
}
faker.seed(recipient.id);
const recipientName = recipient.name || faker.person.fullName();
const recipientEmail = recipient.email || faker.internet.email();
faker.seed(recipient.id + field.id);
return {
...field,
inserted: true,
...match(fieldMeta)
.with({ type: FieldType.TEXT }, ({ fieldMeta }) => {
let text = fieldMeta?.text || faker.lorem.words(5);
if (fieldMeta?.characterLimit) {
text = text.slice(0, fieldMeta?.characterLimit);
}
return {
customText: text,
};
})
.with({ type: FieldType.NUMBER }, ({ fieldMeta }) => {
let number = fieldMeta?.value ?? '';
if (number === '') {
number = faker.number
.int({
min: fieldMeta?.minValue ?? 0,
max: fieldMeta?.maxValue ?? 1000,
})
.toString();
}
return {
customText: number,
};
})
.with({ type: FieldType.DATE }, () => {
const date = extractFieldInsertionValues({
fieldValue: {
type: FieldType.DATE,
value: true,
},
field,
documentMeta: envelope.documentMeta,
});
return {
customText: date.customText,
};
})
.with({ type: FieldType.EMAIL }, () => {
return {
customText: recipientEmail,
};
})
.with({ type: FieldType.NAME }, () => {
return {
customText: recipientName,
};
})
.with({ type: FieldType.INITIALS }, () => {
return {
customText: extractInitials(recipientName),
};
})
.with({ type: FieldType.RADIO }, ({ fieldMeta }) => {
const values = fieldMeta?.values ?? [];
if (values.length === 0) {
return '';
}
let customText = '';
const preselectedValue = values.findIndex((value) => value.checked);
if (preselectedValue !== -1) {
customText = preselectedValue.toString();
} else {
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
customText = randomIndex.toString();
}
return {
customText,
};
})
.with({ type: FieldType.CHECKBOX }, ({ fieldMeta }) => {
let checkedValues: number[] = [];
const values = fieldMeta?.values ?? [];
values.forEach((value, index) => {
if (value.checked) {
checkedValues.push(index);
}
});
if (checkedValues.length === 0 && values.length > 0) {
const numberOfValues = fieldMeta?.validationLength || 1;
checkedValues = Array.from({ length: numberOfValues }, (_, index) => index);
}
return {
customText: toCheckboxCustomText(checkedValues),
};
})
.with({ type: FieldType.DROPDOWN }, ({ fieldMeta }) => {
const values = fieldMeta?.values ?? [];
let customText = fieldMeta?.defaultValue || '';
if (!customText && values.length > 0) {
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
customText = values[randomIndex].value;
}
return {
customText,
};
})
.with({ type: FieldType.SIGNATURE }, () => {
return {
customText: '',
signature: {
signatureImageAsBase64: '',
typedSignature: recipientName,
},
};
})
.with({ type: FieldType.FREE_SIGNATURE }, () => {
return {
customText: '',
};
})
.exhaustive(),
};
});
}, [fields, envelope, envelope.recipients, envelope.documentMeta]);
/** /**
* Set the selected recipient to the first recipient in the envelope. * Set the selected recipient to the first recipient in the envelope.
*/ */
@ -31,7 +195,17 @@ export const EnvelopeEditorPreviewPage = () => {
editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null); editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null);
}, []); }, []);
// Override the parent renderer provider so we can inject custom fields.
return ( return (
<EnvelopeRenderProvider
envelope={envelope}
token={undefined}
fields={fieldsWithPlaceholders}
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
overrideSettings={{
mode: 'export',
}}
>
<div className="relative flex h-full"> <div className="relative flex h-full">
<div className="flex w-full flex-col overflow-y-auto"> <div className="flex w-full flex-col overflow-y-auto">
{/* Horizontal envelope item selector */} {/* Horizontal envelope item selector */}
@ -48,23 +222,11 @@ export const EnvelopeEditorPreviewPage = () => {
</AlertDescription> </AlertDescription>
</Alert> </Alert>
{/* Coming soon section */}
<div className="border-border bg-card hover:bg-accent/10 flex w-full max-w-[800px] items-center gap-4 rounded-lg border p-4 transition-colors">
<div className="flex w-full flex-col items-center justify-center gap-2 py-32">
<ConstructionIcon className="text-muted-foreground h-10 w-10" />
<h3 className="text-foreground text-sm font-semibold">
<Trans>Coming soon</Trans>
</h3>
<p className="text-muted-foreground text-sm">
<Trans>This feature is coming soon</Trans>
</p>
</div>
</div>
{/* Todo: Envelopes - Remove div after preview mode is implemented */}
<div className="hidden">
{currentEnvelopeItem !== null ? ( {currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} /> <PDFViewerKonvaLazy
renderer="editor"
customPageRenderer={EnvelopeGenericPageRenderer}
/>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-32"> <div className="flex flex-col items-center justify-center py-32">
<FileTextIcon className="text-muted-foreground h-10 w-10" /> <FileTextIcon className="text-muted-foreground h-10 w-10" />
@ -78,7 +240,6 @@ export const EnvelopeEditorPreviewPage = () => {
)} )}
</div> </div>
</div> </div>
</div>
{/* Right Section - Form Fields Panel */} {/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && false && ( {currentEnvelopeItem && false && (
@ -94,7 +255,9 @@ export const EnvelopeEditorPreviewPage = () => {
<Trans>Preview Mode</Trans> <Trans>Preview Mode</Trans>
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
<Trans>Preview what the signed document will look like with placeholder data</Trans> <Trans>
Preview what the signed document will look like with placeholder data
</Trans>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -168,5 +331,6 @@ export const EnvelopeEditorPreviewPage = () => {
</div> </div>
)} )}
</div> </div>
</EnvelopeRenderProvider>
); );
}; };

View File

@ -174,7 +174,7 @@ export const EnvelopeEditorSettingsDialog = ({
const { t, i18n } = useLingui(); const { t, i18n } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { envelope } = useCurrentEnvelopeEditor(); const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor();
const team = useCurrentTeam(); const team = useCurrentTeam();
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
@ -186,14 +186,12 @@ export const EnvelopeEditorSettingsDialog = ({
documentAuth: envelope.authOptions, documentAuth: envelope.authOptions,
}); });
const form = useForm<TAddSettingsFormSchema>({ const createDefaultValues = () => {
resolver: zodResolver(ZAddSettingsFormSchema), return {
defaultValues: { externalId: envelope.externalId || '',
externalId: envelope.externalId || '', // Todo: String or undefined?
visibility: envelope.visibility || '', visibility: envelope.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || [], globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
globalActionAuth: documentAuthOption?.globalActionAuth || [], globalActionAuth: documentAuthOption?.globalActionAuth || [],
meta: { meta: {
subject: envelope.documentMeta.subject ?? '', subject: envelope.documentMeta.subject ?? '',
message: envelope.documentMeta.message ?? '', message: envelope.documentMeta.message ?? '',
@ -210,10 +208,13 @@ export const EnvelopeEditorSettingsDialog = ({
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings), emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta), signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
}, },
}, };
}); };
const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation(); const form = useForm<TAddSettingsFormSchema>({
resolver: zodResolver(ZAddSettingsFormSchema),
defaultValues: createDefaultValues(),
});
const envelopeHasBeenSent = const envelopeHasBeenSent =
envelope.type === EnvelopeType.DOCUMENT && envelope.type === EnvelopeType.DOCUMENT &&
@ -239,8 +240,7 @@ export const EnvelopeEditorSettingsDialog = ({
.safeParse(data.globalAccessAuth); .safeParse(data.globalAccessAuth);
try { try {
await updateEnvelope({ await updateEnvelopeAsync({
envelopeId: envelope.id,
data: { data: {
externalId: data.externalId || null, externalId: data.externalId || null,
visibility: data.visibility, visibility: data.visibility,
@ -295,7 +295,7 @@ export const EnvelopeEditorSettingsDialog = ({
]); ]);
useEffect(() => { useEffect(() => {
form.reset(); form.reset(createDefaultValues());
setActiveTab('general'); setActiveTab('general');
}, [open, form]); }, [open, form]);

View File

@ -12,7 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
export default function EnvelopeGenericPageRenderer() { export default function EnvelopeGenericPageRenderer() {
const { i18n } = useLingui(); const { i18n } = useLingui();
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } = const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError, overrideSettings } =
useCurrentEnvelopeRender(); useCurrentEnvelopeRender();
const { const {
@ -50,12 +50,11 @@ export default function EnvelopeGenericPageRenderer() {
field: { field: {
renderId: field.id.toString(), renderId: field.id.toString(),
...field, ...field,
customText: '',
width: Number(field.width), width: Number(field.width),
height: Number(field.height), height: Number(field.height),
positionX: Number(field.positionX), positionX: Number(field.positionX),
positionY: Number(field.positionY), positionY: Number(field.positionY),
inserted: false, customText: field.inserted ? field.customText : '',
fieldMeta: field.fieldMeta, fieldMeta: field.fieldMeta,
}, },
translations: getClientSideFieldTranslations(i18n), translations: getClientSideFieldTranslations(i18n),
@ -63,7 +62,7 @@ export default function EnvelopeGenericPageRenderer() {
pageHeight: unscaledViewport.height, pageHeight: unscaledViewport.height,
color: getRecipientColorKey(field.recipientId), color: getRecipientColorKey(field.recipientId),
editable: false, editable: false,
mode: 'sign', mode: overrideSettings?.mode ?? 'sign',
}); });
}; };

View File

@ -1,49 +0,0 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Download } from 'lucide-react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import type { DocumentData } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type ShareDocumentDownloadButtonProps = {
title: string;
documentData: DocumentData;
};
export const ShareDocumentDownloadButton = ({
title,
documentData,
}: ShareDocumentDownloadButtonProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isDownloading, setIsDownloading] = useState(false);
const onDownloadClick = async () => {
try {
setIsDownloading(true);
await downloadPDF({ documentData, fileName: title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
} finally {
setIsDownloading(false);
}
};
return (
<Button loading={isDownloading} onClick={onDownloadClick}>
{!isDownloading && <Download className="mr-2 h-4 w-4" />}
<Trans>Download</Trans>
</Button>
);
};

View File

@ -313,8 +313,10 @@ export const TemplateEditForm = ({
> >
<CardContent className="p-2"> <CardContent className="p-2">
<PDFViewer <PDFViewer
key={templateDocumentData.id} key={template.envelopeItems[0].id}
documentData={templateDocumentData} envelopeItem={template.envelopeItems[0]}
token={undefined}
version="signed"
onDocumentLoad={() => setIsDocumentPdfLoaded(true)} onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
/> />
</CardContent> </CardContent>

View File

@ -1,19 +1,14 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react'; import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team'; import { useCurrentTeam } from '~/providers/team';
@ -25,8 +20,6 @@ export type DocumentsTableActionButtonProps = {
export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => { export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => {
const { user } = useSession(); const { user } = useSession();
const { toast } = useToast();
const { _ } = useLingui();
const team = useCurrentTeam(); const team = useCurrentTeam();
@ -44,39 +37,6 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
const documentsPath = formatDocumentsPath(team.url); const documentsPath = formatDocumentsPath(team.url);
const formatPath = `${documentsPath}/${row.envelopeId}/edit`; const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
const onDownloadClick = async () => {
try {
const document = !recipient
? await trpcClient.document.get.query(
{
documentId: row.id,
},
{
context: {
teamId: team?.id?.toString(),
},
},
)
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;
if (!documentData) {
throw Error('No document available');
}
await downloadPDF({ documentData, fileName: row.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers // TODO: Consider if want to keep this logic for hiding viewing for CC'ers
if (recipient?.role === RecipientRole.CC && isComplete === false) { if (recipient?.role === RecipientRole.CC && isComplete === false) {
return null; return null;
@ -134,7 +94,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
<Trans>View</Trans> <Trans>View</Trans>
</Button> </Button>
)) ))
.with({ isComplete: true, internalVersion: 2 }, () => ( .with({ isComplete: true }, () => (
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
envelopeId={row.envelopeId} envelopeId={row.envelopeId}
envelopeStatus={row.status} envelopeStatus={row.status}
@ -147,11 +107,5 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
} }
/> />
)) ))
.with({ isComplete: true }, () => (
<Button className="w-32" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
))
.otherwise(() => <div></div>); .otherwise(() => <div></div>);
}; };

View File

@ -10,7 +10,6 @@ import {
Download, Download,
Edit, Edit,
EyeIcon, EyeIcon,
FileDown,
FolderInput, FolderInput,
Loader, Loader,
MoreHorizontal, MoreHorizontal,
@ -20,12 +19,10 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { import {
DropdownMenu, DropdownMenu,
@ -34,7 +31,6 @@ import {
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@documenso/ui/primitives/dropdown-menu'; } from '@documenso/ui/primitives/dropdown-menu';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog'; import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog'; import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
@ -56,7 +52,6 @@ export const DocumentsTableActionDropdown = ({
const { user } = useSession(); const { user } = useSession();
const team = useCurrentTeam(); const team = useCurrentTeam();
const { toast } = useToast();
const { _ } = useLingui(); const { _ } = useLingui();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -76,58 +71,6 @@ export const DocumentsTableActionDropdown = ({
const documentsPath = formatDocumentsPath(team.url); const documentsPath = formatDocumentsPath(team.url);
const formatPath = `${documentsPath}/${row.envelopeId}/edit`; const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
const onDownloadClick = async () => {
try {
const document = !recipient
? await trpcClient.document.get.query({
documentId: row.id,
})
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: row.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
const onDownloadOriginalClick = async () => {
try {
const document = !recipient
? await trpcClient.document.get.query({
documentId: row.id,
})
: await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;
if (!documentData) {
return;
}
await downloadPDF({ documentData, fileName: row.title, version: 'original' });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED'); const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
return ( return (
@ -178,7 +121,6 @@ export const DocumentsTableActionDropdown = ({
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
{row.internalVersion === 2 ? (
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
envelopeId={row.envelopeId} envelopeId={row.envelopeId}
envelopeStatus={row.status} envelopeStatus={row.status}
@ -192,19 +134,6 @@ export const DocumentsTableActionDropdown = ({
</DropdownMenuItem> </DropdownMenuItem>
} }
/> />
) : (
<>
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
<Download className="mr-2 h-4 w-4" />
<Trans>Download</Trans>
</DropdownMenuItem>
<DropdownMenuItem onClick={onDownloadOriginalClick}>
<FileDown className="mr-2 h-4 w-4" />
<Trans>Download Original</Trans>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}> <DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
<Copy className="mr-2 h-4 w-4" /> <Copy className="mr-2 h-4 w-4" />
@ -273,7 +202,8 @@ export const DocumentsTableActionDropdown = ({
/> />
<DocumentDuplicateDialog <DocumentDuplicateDialog
id={row.id} id={row.envelopeId}
token={recipient?.token}
open={isDuplicateDialogOpen} open={isDuplicateDialogOpen}
onOpenChange={setDuplicateDialogOpen} onOpenChange={setDuplicateDialogOpen}
/> />

View File

@ -10,11 +10,9 @@ import { DateTime } from 'luxon';
import { Link, useSearchParams } from 'react-router'; import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types'; import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -28,6 +26,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentStatus } from '~/components/general/document/document-status'; import { DocumentStatus } from '~/components/general/document/document-status';
import { useOptionalCurrentTeam } from '~/providers/team'; import { useOptionalCurrentTeam } from '~/providers/team';
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip'; import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
export type DocumentsTableProps = { export type DocumentsTableProps = {
@ -199,28 +198,6 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
return null; return null;
} }
const onDownloadClick = async () => {
try {
const document = await trpcClient.document.getDocumentByToken.query({
token: recipient.token,
});
const documentData = document?.documentData;
if (!documentData) {
throw Error('No document available');
}
await downloadPDF({ documentData, fileName: row.title });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers // TODO: Consider if want to keep this logic for hiding viewing for CC'ers
if (recipient?.role === RecipientRole.CC && isComplete === false) { if (recipient?.role === RecipientRole.CC && isComplete === false) {
return null; return null;
@ -230,6 +207,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
isPending, isPending,
isComplete, isComplete,
isSigned, isSigned,
internalVersion: row.internalVersion,
}) })
.with({ isPending: true, isSigned: false }, () => ( .with({ isPending: true, isSigned: false }, () => (
<Button className="w-32" asChild> <Button className="w-32" asChild>
@ -263,10 +241,17 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
</Button> </Button>
)) ))
.with({ isComplete: true }, () => ( .with({ isComplete: true }, () => (
<Button className="w-32" onClick={onDownloadClick}> <EnvelopeDownloadDialog
envelopeId={row.envelopeId}
envelopeStatus={row.status}
token={recipient?.token}
trigger={
<Button className="w-32">
<DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" /> <DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans> <Trans>Download</Trans>
</Button> </Button>
}
/>
)) ))
.otherwise(() => <div></div>); .otherwise(() => <div></div>);
}; };

View File

@ -147,6 +147,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7"> <div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
<EnvelopeRenderProvider <EnvelopeRenderProvider
envelope={envelope} envelope={envelope}
token={undefined}
fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields} fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)} recipientIds={envelope.recipients.map((recipient) => recipient.id)}
> >
@ -181,9 +182,10 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
)} )}
<PDFViewer <PDFViewer
document={envelope} envelopeItem={envelope.envelopeItems[0]}
token={undefined}
key={envelope.envelopeItems[0].id} key={envelope.envelopeItems[0].id}
documentData={envelope.envelopeItems[0].documentData} version="signed"
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -101,6 +101,7 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
<EnvelopeEditorProvider initialEnvelope={envelope}> <EnvelopeEditorProvider initialEnvelope={envelope}>
<EnvelopeRenderProvider <EnvelopeRenderProvider
envelope={envelope} envelope={envelope}
token={undefined}
fields={envelope.fields} fields={envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)} recipientIds={envelope.recipients.map((recipient) => recipient.id)}
> >

View File

@ -170,6 +170,7 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7"> <div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
<EnvelopeRenderProvider <EnvelopeRenderProvider
envelope={envelope} envelope={envelope}
token={undefined}
fields={envelope.fields} fields={envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)} recipientIds={envelope.recipients.map((recipient) => recipient.id)}
> >
@ -203,9 +204,10 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
/> />
<PDFViewer <PDFViewer
document={envelope} envelopeItem={envelope.envelopeItems[0]}
token={undefined}
version="signed"
key={envelope.envelopeItems[0].id} key={envelope.envelopeItems[0].id}
documentData={envelope.envelopeItems[0].documentData}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -245,7 +245,7 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
recipient={recipient} recipient={recipient}
user={user} user={user}
> >
<EnvelopeRenderProvider envelope={envelope}> <EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<DocumentSigningPageViewV2 /> <DocumentSigningPageViewV2 />
</EnvelopeRenderProvider> </EnvelopeRenderProvider>
</DocumentSigningAuthProvider> </DocumentSigningAuthProvider>

View File

@ -492,7 +492,7 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
recipient={recipient} recipient={recipient}
user={user} user={user}
> >
<EnvelopeRenderProvider envelope={envelope}> <EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
<DocumentSigningPageViewV2 /> <DocumentSigningPageViewV2 />
</EnvelopeRenderProvider> </EnvelopeRenderProvider>
</DocumentSigningAuthProvider> </DocumentSigningAuthProvider>

View File

@ -1,10 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client'; import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react'; import { CheckCircle2, Clock8, DownloadIcon } from 'lucide-react';
import { Link, useRevalidator } from 'react-router'; import { Link, useRevalidator } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -20,14 +19,13 @@ import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-emai
import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env'; import { env } from '@documenso/lib/utils/env';
import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import type { Document } from '@documenso/prisma/types/document-legacy-schema';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card'; import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
import { ClaimAccount } from '~/components/general/claim-account'; import { ClaimAccount } from '~/components/general/claim-account';
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page'; import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
@ -207,24 +205,16 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4"> <div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<DocumentShareButton documentId={document.id} token={recipient.token} /> <DocumentShareButton documentId={document.id} token={recipient.token} />
{isDocumentCompleted(document.status) ? ( {isDocumentCompleted(document.status) && (
<DocumentDownloadButton <EnvelopeDownloadDialog
className="flex-1" envelopeId={document.envelopeId}
fileName={document.title} envelopeStatus={document.status}
documentData={document.documentData} envelopeItems={document.envelopeItems}
disabled={!isDocumentCompleted(document.status)} token={recipient?.token}
/>
) : (
<DocumentDialog
documentData={document.documentData}
trigger={ trigger={
<Button <Button type="button" variant="outline" className="flex-1">
className="text-[11px]" <DownloadIcon className="mr-2 h-5 w-5" />
title={_(msg`Signatures will appear once the document has been completed`)} <Trans>Download</Trans>
variant="outline"
>
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
<Trans>View Original Document</Trans>
</Button> </Button>
} }
/> />

View File

@ -60,6 +60,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
return { return {
document, document,
token: slug,
}; };
} }
@ -74,7 +75,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
}; };
export default function SharePage() { export default function SharePage() {
const { document } = useLoaderData<typeof loader>(); const { document, token } = useLoaderData<typeof loader>();
if (document) { if (document) {
return ( return (
@ -86,6 +87,7 @@ export default function SharePage() {
envelopeItems={document.envelopeItems} envelopeItems={document.envelopeItems}
recipientCount={document.recipientCount} recipientCount={document.recipientCount}
completedDate={document.completedAt ?? undefined} completedDate={document.completedAt ?? undefined}
token={token}
/> />
); );
} }

View File

@ -124,7 +124,7 @@ export default function EmbedDirectTemplatePage() {
token={token} token={token}
envelopeId={template.envelopeId} envelopeId={template.envelopeId}
updatedAt={template.updatedAt} updatedAt={template.updatedAt}
documentData={template.templateDocumentData} envelopeItems={template.envelopeItems}
recipient={recipient} recipient={recipient}
fields={fields} fields={fields}
metadata={template.templateMeta} metadata={template.templateMeta}

View File

@ -165,7 +165,7 @@ export default function EmbedSignDocumentPage() {
token={token} token={token}
documentId={document.id} documentId={document.id}
envelopeId={document.envelopeId} envelopeId={document.envelopeId}
documentData={document.documentData} envelopeItems={document.envelopeItems}
recipient={recipient} recipient={recipient}
fields={fields} fields={fields}
completedFields={completedFields} completedFields={completedFields}

View File

@ -25,6 +25,7 @@
"@documenso/trpc": "*", "@documenso/trpc": "*",
"@documenso/ui": "*", "@documenso/ui": "*",
"@epic-web/remember": "^1.1.0", "@epic-web/remember": "^1.1.0",
"@faker-js/faker": "^10.1.0",
"@hono/node-server": "^1.13.7", "@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",

View File

@ -1,4 +1,5 @@
import { sValidator } from '@hono/standard-validator'; import { sValidator } from '@hono/standard-validator';
import type { Prisma } from '@prisma/client';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
@ -206,8 +207,7 @@ export const filesRoute = new Hono<HonoEnv>()
async (c) => { async (c) => {
const { token, envelopeItemId } = c.req.valid('param'); const { token, envelopeItemId } = c.req.valid('param');
const envelopeItem = await prisma.envelopeItem.findFirst({ let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
where: {
id: envelopeItemId, id: envelopeItemId,
envelope: { envelope: {
recipients: { recipients: {
@ -216,7 +216,19 @@ export const filesRoute = new Hono<HonoEnv>()
}, },
}, },
}, },
};
if (token.startsWith('qr_')) {
envelopeWhereQuery = {
id: envelopeItemId,
envelope: {
qrToken: token,
}, },
};
}
const envelopeItem = await prisma.envelopeItem.findUnique({
where: envelopeWhereQuery,
include: { include: {
envelope: true, envelope: true,
documentData: true, documentData: true,
@ -247,8 +259,7 @@ export const filesRoute = new Hono<HonoEnv>()
async (c) => { async (c) => {
const { token, envelopeItemId, version } = c.req.valid('param'); const { token, envelopeItemId, version } = c.req.valid('param');
const envelopeItem = await prisma.envelopeItem.findFirst({ let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
where: {
id: envelopeItemId, id: envelopeItemId,
envelope: { envelope: {
recipients: { recipients: {
@ -257,7 +268,19 @@ export const filesRoute = new Hono<HonoEnv>()
}, },
}, },
}, },
};
if (token.startsWith('qr_')) {
envelopeWhereQuery = {
id: envelopeItemId,
envelope: {
qrToken: token,
}, },
};
}
const envelopeItem = await prisma.envelopeItem.findUnique({
where: envelopeWhereQuery,
include: { include: {
envelope: true, envelope: true,
documentData: true, documentData: true,

180
package-lock.json generated
View File

@ -45,6 +45,7 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^6.18.0", "prisma": "^6.18.0",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"superjson": "^2.2.5", "superjson": "^2.2.5",
@ -112,6 +113,7 @@
"@documenso/trpc": "*", "@documenso/trpc": "*",
"@documenso/ui": "*", "@documenso/ui": "*",
"@epic-web/remember": "^1.1.0", "@epic-web/remember": "^1.1.0",
"@faker-js/faker": "^10.1.0",
"@hono/node-server": "^1.13.7", "@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
@ -3511,6 +3513,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@faker-js/faker": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
@ -7599,6 +7617,18 @@
"empathic": "2.0.0" "empathic": "2.0.0"
} }
}, },
"node_modules/@prisma/debug": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"license": "Apache-2.0"
},
"node_modules/@prisma/dmmf": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": { "node_modules/@prisma/engines": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
@ -7618,12 +7648,6 @@
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/engines/node_modules/@prisma/debug": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
@ -7635,10 +7659,10 @@
"@prisma/get-platform": "6.18.0" "@prisma/get-platform": "6.18.0"
} }
}, },
"node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": { "node_modules/@prisma/generator": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", "integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/generator-helper": { "node_modules/@prisma/generator-helper": {
@ -7719,12 +7743,6 @@
"@prisma/debug": "6.18.0" "@prisma/debug": "6.18.0"
} }
}, },
"node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"license": "Apache-2.0"
},
"node_modules/@prisma/internals": { "node_modules/@prisma/internals": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz",
@ -28646,6 +28664,54 @@
"@prisma/client": "latest" "@prisma/client": "latest"
} }
}, },
"node_modules/prisma-json-types-generator": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.6.2.tgz",
"integrity": "sha512-WX/oENQ0S74r/Wgd2uuHT5i3KbnwLFCP2Fq5ISzrXkus/htOC4uCaQPYuGP2m/wSeKZZCw1RxptTlD+ib7Ht/A==",
"license": "MIT",
"dependencies": {
"@prisma/generator-helper": "^6.16.1",
"semver": "^7.7.2",
"tslib": "^2.8.1"
},
"bin": {
"prisma-json-types-generator": "index.js"
},
"engines": {
"node": ">=14.0"
},
"funding": {
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
},
"peerDependencies": {
"@prisma/client": "^6.14",
"prisma": "^6.14",
"typescript": "^5.9.2"
}
},
"node_modules/prisma-json-types-generator/node_modules/@prisma/generator-helper": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
"integrity": "sha512-kmlCDRRewLwBkHpkAjzyuNHD5ISlDLzUTcTsZbwmjDilQVt/S72xvvCAa+hxY16APTkqbtDYn3p7zL/XFO+C0A==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.18.0",
"@prisma/dmmf": "6.18.0",
"@prisma/generator": "6.18.0"
}
},
"node_modules/prisma-json-types-generator/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prisma-kysely": { "node_modules/prisma-kysely": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz", "resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz",
@ -36456,24 +36522,6 @@
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
} }
}, },
"node_modules/zod-prisma-types/node_modules/@prisma/debug": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"license": "Apache-2.0"
},
"node_modules/zod-prisma-types/node_modules/@prisma/dmmf": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
"license": "Apache-2.0"
},
"node_modules/zod-prisma-types/node_modules/@prisma/generator": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
"license": "Apache-2.0"
},
"node_modules/zod-prisma-types/node_modules/@prisma/generator-helper": { "node_modules/zod-prisma-types/node_modules/@prisma/generator-helper": {
"version": "6.18.0", "version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
@ -37130,72 +37178,6 @@
"typescript": "5.6.2" "typescript": "5.6.2"
} }
}, },
"packages/prisma/node_modules/@prisma/debug": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
"license": "Apache-2.0"
},
"packages/prisma/node_modules/@prisma/dmmf": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
"license": "Apache-2.0"
},
"packages/prisma/node_modules/@prisma/generator": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
"license": "Apache-2.0"
},
"packages/prisma/node_modules/@prisma/generator-helper": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
"integrity": "sha512-kmlCDRRewLwBkHpkAjzyuNHD5ISlDLzUTcTsZbwmjDilQVt/S72xvvCAa+hxY16APTkqbtDYn3p7zL/XFO+C0A==",
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.18.0",
"@prisma/dmmf": "6.18.0",
"@prisma/generator": "6.18.0"
}
},
"packages/prisma/node_modules/prisma-json-types-generator": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.6.2.tgz",
"integrity": "sha512-WX/oENQ0S74r/Wgd2uuHT5i3KbnwLFCP2Fq5ISzrXkus/htOC4uCaQPYuGP2m/wSeKZZCw1RxptTlD+ib7Ht/A==",
"license": "MIT",
"dependencies": {
"@prisma/generator-helper": "^6.16.1",
"semver": "^7.7.2",
"tslib": "^2.8.1"
},
"bin": {
"prisma-json-types-generator": "index.js"
},
"engines": {
"node": ">=14.0"
},
"funding": {
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
},
"peerDependencies": {
"@prisma/client": "^6.14",
"prisma": "^6.14",
"typescript": "^5.9.2"
}
},
"packages/prisma/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"packages/prisma/node_modules/ts-pattern": { "packages/prisma/node_modules/ts-pattern": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz", "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz",

View File

@ -56,6 +56,7 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^6.18.0", "prisma": "^6.18.0",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3", "turbo": "^1.9.3",

View File

@ -559,5 +559,10 @@ test.describe('API V2 Envelopes', () => {
); );
expect(finalEnvelope.title).toBe('Envelope Full Field Test'); expect(finalEnvelope.title).toBe('Envelope Full Field Test');
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT); expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
console.log({
createdEnvelopeId: finalEnvelope.id,
userEmail: userA.email,
});
}); });
}); });

View File

@ -25,8 +25,7 @@ import { DocumentStatus } from '@prisma/client';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js'; import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed'; import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users'; import { seedUser } from '@documenso/prisma/seed/users';
@ -95,7 +94,13 @@ test('field placement visual regression', async ({ page }, testInfo) => {
await Promise.all( await Promise.all(
completedDocument.envelopeItems.map(async (item) => { completedDocument.envelopeItems.map(async (item) => {
const pdfData = await getFile(item.documentData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: item,
token,
version: 'signed',
});
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const loadedImages = storedImages const loadedImages = storedImages
.filter((image) => image.includes(item.title)) .filter((image) => image.includes(item.title))
@ -103,7 +108,7 @@ test('field placement visual regression', async ({ page }, testInfo) => {
await compareSignedPdfWithImages({ await compareSignedPdfWithImages({
id: item.title.replaceAll(' ', '-').toLowerCase(), id: item.title.replaceAll(' ', '-').toLowerCase(),
pdfData, pdfData: new Uint8Array(pdfData),
images: loadedImages, images: loadedImages,
testInfo, testInfo,
}); });
@ -174,9 +179,15 @@ test.skip('download envelope images', async ({ page }) => {
await Promise.all( await Promise.all(
completedDocument.envelopeItems.map(async (item) => { completedDocument.envelopeItems.map(async (item) => {
const pdfData = await getFile(item.documentData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: item,
token,
version: 'signed',
});
const pdfImages = await renderPdfToImage(pdfData); const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const pdfImages = await renderPdfToImage(new Uint8Array(pdfData));
for (const [index, { image }] of pdfImages.entries()) { for (const [index, { image }] of pdfImages.entries()) {
fs.writeFileSync( fs.writeFileSync(

View File

@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client'; import { DocumentStatus, FieldType } from '@prisma/client';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents'; import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedTeam } from '@documenso/prisma/seed/teams';
@ -25,20 +25,25 @@ test.describe('Signing Certificate Tests', () => {
teamId: team.id, teamId: team.id,
}); });
const documentData = await prisma.documentData const recipient = recipients[0];
const documentData = await prisma.envelopeItem
.findFirstOrThrow({ .findFirstOrThrow({
where: { where: {
envelopeItem: {
envelopeId: document.id, envelopeId: document.id,
}, },
},
}) })
.then(async (data) => getFile(data)); .then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: data,
token: recipient.token,
version: 'signed',
});
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
});
const originalPdf = await PDFDocument.load(documentData); const originalPdf = await PDFDocument.load(documentData);
const recipient = recipients[0];
// Sign the document // Sign the document
await page.goto(`/sign/${recipient.token}`); await page.goto(`/sign/${recipient.token}`);
@ -78,9 +83,17 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const firstDocumentData = completedDocument.envelopeItems[0].documentData; const firstDocumentData = completedDocument.envelopeItems[0];
const completedDocumentData = await getFile(firstDocumentData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: firstDocumentData,
token: recipient.token,
version: 'signed',
});
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const completedDocumentData = new Uint8Array(pdfData);
// Load the PDF and check number of pages // Load the PDF and check number of pages
const pdfDoc = await PDFDocument.load(completedDocumentData); const pdfDoc = await PDFDocument.load(completedDocumentData);
@ -117,20 +130,25 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const documentData = await prisma.documentData const recipient = recipients[0];
const documentData = await prisma.envelopeItem
.findFirstOrThrow({ .findFirstOrThrow({
where: { where: {
envelopeItem: {
envelopeId: document.id, envelopeId: document.id,
}, },
},
}) })
.then(async (data) => getFile(data)); .then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: data,
token: recipient.token,
version: 'signed',
});
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
});
const originalPdf = await PDFDocument.load(documentData); const originalPdf = await PDFDocument.load(documentData);
const recipient = recipients[0];
// Sign the document // Sign the document
await page.goto(`/sign/${recipient.token}`); await page.goto(`/sign/${recipient.token}`);
@ -168,9 +186,17 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const firstDocumentData = completedDocument.envelopeItems[0].documentData; const firstDocumentData = completedDocument.envelopeItems[0];
const completedDocumentData = await getFile(firstDocumentData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: firstDocumentData,
token: recipient.token,
version: 'signed',
});
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
const completedDocumentData = new Uint8Array(pdfData);
// Load the PDF and check number of pages // Load the PDF and check number of pages
const completedPdf = await PDFDocument.load(completedDocumentData); const completedPdf = await PDFDocument.load(completedDocumentData);
@ -207,19 +233,24 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const documentData = await prisma.documentData const recipient = recipients[0];
const documentData = await prisma.envelopeItem
.findFirstOrThrow({ .findFirstOrThrow({
where: { where: {
envelopeItem: {
envelopeId: document.id, envelopeId: document.id,
}, },
},
}) })
.then(async (data) => getFile(data)); .then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: data,
token: recipient.token,
version: 'signed',
});
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
});
const originalPdf = await PDFDocument.load(documentData); const originalPdf = await PDFDocument.load(new Uint8Array(documentData));
const recipient = recipients[0];
// Sign the document // Sign the document
await page.goto(`/sign/${recipient.token}`); await page.goto(`/sign/${recipient.token}`);
@ -258,7 +289,15 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
const completedDocumentData = await getFile(completedDocument.envelopeItems[0].documentData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: completedDocument.envelopeItems[0],
token: recipient.token,
version: 'signed',
});
const completedDocumentData = await fetch(documentUrl).then(
async (res) => await res.arrayBuffer(),
);
// Load the PDF and check number of pages // Load the PDF and check number of pages
const completedPdf = await PDFDocument.load(completedDocumentData); const completedPdf = await PDFDocument.load(completedDocumentData);

View File

@ -1,12 +1,14 @@
import type { DocumentData } from '@prisma/client'; import type { EnvelopeItem } from '@prisma/client';
import { getFile } from '../universal/upload/get-file'; import { getEnvelopeDownloadUrl } from '../utils/envelope-download';
import { downloadFile } from './download-file'; import { downloadFile } from './download-file';
type DocumentVersion = 'original' | 'signed'; type DocumentVersion = 'original' | 'signed';
type DownloadPDFProps = { type DownloadPDFProps = {
documentData: DocumentData; envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
fileName?: string; fileName?: string;
/** /**
* Specifies which version of the document to download. * Specifies which version of the document to download.
@ -17,18 +19,18 @@ type DownloadPDFProps = {
}; };
export const downloadPDF = async ({ export const downloadPDF = async ({
documentData, envelopeItem,
token,
fileName, fileName,
version = 'signed', version = 'signed',
}: DownloadPDFProps) => { }: DownloadPDFProps) => {
const bytes = await getFile({ const downloadUrl = getEnvelopeDownloadUrl({
type: documentData.type, envelopeItem: envelopeItem,
data: version === 'signed' ? documentData.data : documentData.initialData, token,
version,
}); });
const blob = new Blob([bytes], { const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
type: 'application/pdf',
});
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, ''); const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf'; const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';

View File

@ -46,6 +46,7 @@ type EnvelopeEditorProviderValue = {
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void; setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void; updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void; setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>; setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
@ -66,8 +67,6 @@ type EnvelopeEditorProviderValue = {
}; };
syncEnvelope: () => Promise<void>; syncEnvelope: () => Promise<void>;
// refetchEnvelope: () => Promise<void>;
// updateEnvelope: (envelope: TEnvelope) => Promise<void>;
}; };
interface EnvelopeEditorProviderProps { interface EnvelopeEditorProviderProps {
@ -236,6 +235,13 @@ export const EnvelopeEditorProvider = ({
setEnvelopeDebounced(envelopeUpdates); setEnvelopeDebounced(envelopeUpdates);
}; };
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
await envelopeUpdateMutationQuery.mutateAsync({
envelopeId: envelope.id,
...envelopeUpdates,
});
};
const getRecipientColorKey = useCallback( const getRecipientColorKey = useCallback(
(recipientId: number) => { (recipientId: number) => {
const recipientIndex = envelope.recipients.findIndex( const recipientIndex = envelope.recipients.findIndex(
@ -323,6 +329,7 @@ export const EnvelopeEditorProvider = ({
setLocalEnvelope, setLocalEnvelope,
getRecipientColorKey, getRecipientColorKey,
updateEnvelope, updateEnvelope,
updateEnvelopeAsync,
setRecipientsDebounced, setRecipientsDebounced,
setRecipientsAsync, setRecipientsAsync,
editorFields, editorFields,

View File

@ -1,13 +1,11 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import React from 'react'; import React from 'react';
import type { DocumentData } from '@prisma/client';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors'; import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import type { TEnvelope } from '../../types/envelope'; import type { TEnvelope } from '../../types/envelope';
import { getFile } from '../../universal/upload/get-file'; import { getEnvelopeDownloadUrl } from '../../utils/envelope-download';
type FileData = type FileData =
| { | {
@ -18,6 +16,10 @@ type FileData =
status: 'loaded'; status: 'loaded';
}; };
type EnvelopeRenderOverrideSettings = {
mode: 'edit' | 'sign' | 'export';
};
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number]; type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderProviderValue = { type EnvelopeRenderProviderValue = {
@ -30,10 +32,12 @@ type EnvelopeRenderProviderValue = {
renderError: boolean; renderError: boolean;
setRenderError: (renderError: boolean) => void; setRenderError: (renderError: boolean) => void;
overrideSettings?: EnvelopeRenderOverrideSettings;
}; };
interface EnvelopeRenderProviderProps { interface EnvelopeRenderProviderProps {
children: React.ReactNode; children: React.ReactNode;
envelope: Pick<TEnvelope, 'envelopeItems'>; envelope: Pick<TEnvelope, 'envelopeItems'>;
/** /**
@ -49,6 +53,18 @@ interface EnvelopeRenderProviderProps {
* Only required for generic page renderers. * Only required for generic page renderers.
*/ */
recipientIds?: number[]; recipientIds?: number[];
/**
* The token to access the envelope.
*
* If not provided, it will be assumed that the current user can access the document.
*/
token: string | undefined;
/**
* Custom override settings for generic page renderers.
*/
overrideSettings?: EnvelopeRenderOverrideSettings;
} }
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null); const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
@ -70,7 +86,9 @@ export const EnvelopeRenderProvider = ({
children, children,
envelope, envelope,
fields, fields,
token,
recipientIds = [], recipientIds = [],
overrideSettings,
}: EnvelopeRenderProviderProps) => { }: EnvelopeRenderProviderProps) => {
// Indexed by documentDataId. // Indexed by documentDataId.
const [files, setFiles] = useState<Record<string, FileData>>({}); const [files, setFiles] = useState<Record<string, FileData>>({});
@ -84,27 +102,35 @@ export const EnvelopeRenderProvider = ({
[envelope.envelopeItems], [envelope.envelopeItems],
); );
const loadEnvelopeItemPdfFile = async (documentData: DocumentData) => { const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => {
if (files[documentData.id]?.status === 'loading') { if (files[envelopeItem.documentDataId]?.status === 'loading') {
return; return;
} }
if (!files[documentData.id]) { if (!files[envelopeItem.documentDataId]) {
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[documentData.id]: { [envelopeItem.documentDataId]: {
status: 'loading', status: 'loading',
}, },
})); }));
} }
try { try {
const file = await getFile(documentData); const downloadUrl = getEnvelopeDownloadUrl({
envelopeItem: envelopeItem,
token,
version: 'signed',
});
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
const file = await blob.arrayBuffer();
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[documentData.id]: { [envelopeItem.documentDataId]: {
file, file: new Uint8Array(file),
status: 'loaded', status: 'loaded',
}, },
})); }));
@ -113,7 +139,7 @@ export const EnvelopeRenderProvider = ({
setFiles((prev) => ({ setFiles((prev) => ({
...prev, ...prev,
[documentData.id]: { [envelopeItem.documentDataId]: {
status: 'error', status: 'error',
}, },
})); }));
@ -145,7 +171,7 @@ export const EnvelopeRenderProvider = ({
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]); const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]);
for (const item of missingFiles) { for (const item of missingFiles) {
void loadEnvelopeItemPdfFile(item.documentData); void loadEnvelopeItemPdfFile(item);
} }
}, [envelope.envelopeItems]); }, [envelope.envelopeItems]);
@ -171,6 +197,7 @@ export const EnvelopeRenderProvider = ({
getRecipientColorKey, getRecipientColorKey,
renderError, renderError,
setRenderError, setRenderError,
overrideSettings,
}} }}
> >
{children} {children}

View File

@ -78,6 +78,14 @@ export const adminFindDocuments = async ({
url: true, url: true,
}, },
}, },
envelopeItems: {
select: {
id: true,
envelopeId: true,
title: true,
order: true,
},
},
}, },
}), }),
prisma.envelope.count({ prisma.envelope.count({

View File

@ -248,6 +248,14 @@ export const findDocuments = async ({
url: true, url: true,
}, },
}, },
envelopeItems: {
select: {
id: true,
envelopeId: true,
title: true,
order: true,
},
},
}, },
}), }),
prisma.envelope.count({ prisma.envelope.count({

View File

@ -91,7 +91,7 @@ export const getDocumentAndSenderByToken = async ({
}, },
}, },
envelopeItems: { envelopeItems: {
select: { include: {
documentData: true, documentData: true,
}, },
}, },

View File

@ -63,5 +63,8 @@ export const getDocumentWithDetailsById = async ({
documentId: legacyDocumentId, documentId: legacyDocumentId,
password: null, password: null,
}, },
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
...envelopeItem,
})),
}; };
}; };

View File

@ -2,7 +2,6 @@ import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } fro
import { z } from 'zod'; import { z } from 'zod';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema'; import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema'; import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
@ -72,20 +71,12 @@ export const ZEnvelopeForSigningResponse = z.object({
.array(), .array(),
envelopeItems: EnvelopeItemSchema.pick({ envelopeItems: EnvelopeItemSchema.pick({
envelopeId: true,
id: true, id: true,
title: true, title: true,
documentDataId: true, documentDataId: true,
order: true, order: true,
}) }).array(),
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
team: TeamSchema.pick({ team: TeamSchema.pick({
id: true, id: true,
@ -199,11 +190,7 @@ export const getEnvelopeForRecipientSigning = async ({
signingOrder: 'asc', signingOrder: 'asc',
}, },
}, },
envelopeItems: { envelopeItems: true,
include: {
documentData: true,
},
},
team: { team: {
select: { select: {
id: true, id: true,

View File

@ -87,5 +87,9 @@ export const getTemplateByDirectLinkToken = async ({
}, },
recipients: recipientsWithMappedFields, recipients: recipientsWithMappedFields,
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields), fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
envelopeItems: envelope.envelopeItems.map((item) => ({
id: item.id,
envelopeId: item.envelopeId,
})),
}; };
}; };

View File

@ -29,6 +29,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
envelopeItems: { envelopeItems: {
select: { select: {
id: true, id: true,
envelopeId: true,
documentData: true, documentData: true,
}, },
}, },
@ -94,5 +95,9 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
} }
: null, : null,
id: mapSecondaryIdToTemplateId(envelope.secondaryId), id: mapSecondaryIdToTemplateId(envelope.secondaryId),
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
id: envelopeItem.id,
envelopeId: envelopeItem.envelopeId,
})),
}; };
}; };

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
@ -74,6 +75,10 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
password: z.string().nullable().default(null), password: z.string().nullable().default(null),
documentId: z.number().default(-1).optional(), documentId: z.number().default(-1).optional(),
}), }),
envelopeItems: EnvelopeItemSchema.pick({
id: true,
envelopeId: true,
}).array(),
folder: FolderSchema.pick({ folder: FolderSchema.pick({
id: true, id: true,

View File

@ -1,6 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema'; import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema'; import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
@ -66,20 +65,12 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
templateId: true, templateId: true,
}).array(), }).array(),
envelopeItems: EnvelopeItemSchema.pick({ envelopeItems: EnvelopeItemSchema.pick({
envelopeId: true,
id: true, id: true,
title: true, title: true,
documentDataId: true, documentDataId: true,
order: true, order: true,
}) }).array(),
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
directLink: TemplateDirectLinkSchema.pick({ directLink: TemplateDirectLinkSchema.pick({
directTemplateRecipientId: true, directTemplateRecipientId: true,
enabled: true, enabled: true,

View File

@ -2,6 +2,7 @@ import { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
@ -87,6 +88,10 @@ export const ZTemplateSchema = TemplateSchema.pick({
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}).nullable(), }).nullable(),
envelopeItems: EnvelopeItemSchema.pick({
id: true,
envelopeId: true,
}).array(),
}); });
export type TTemplate = z.infer<typeof ZTemplateSchema>; export type TTemplate = z.infer<typeof ZTemplateSchema>;

View File

@ -0,0 +1,19 @@
import type { EnvelopeItem } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export type EnvelopeDownloadUrlOptions = {
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
version: 'original' | 'signed';
};
export const getEnvelopeDownloadUrl = (options: EnvelopeDownloadUrlOptions) => {
const { envelopeItem, token, version } = options;
const { id, envelopeId } = envelopeItem;
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
};

View File

@ -92,6 +92,14 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
url: true, url: true,
}, },
}, },
envelopeItems: {
select: {
id: true,
envelopeId: true,
title: true,
order: true,
},
},
}, },
}), }),
prisma.envelope.count({ prisma.envelope.count({

View File

@ -4,6 +4,7 @@ import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient'; import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
import FieldSchema from '@documenso/prisma/generated/zod/modelSchema/FieldSchema'; import FieldSchema from '@documenso/prisma/generated/zod/modelSchema/FieldSchema';
import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema'; import SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
@ -40,6 +41,10 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
signature: SignatureSchema.nullable(), signature: SignatureSchema.nullable(),
}), }),
), ),
envelopeItems: EnvelopeItemSchema.pick({
id: true,
envelopeId: true,
}).array(),
}); });
export type TGetMultiSignDocumentRequestSchema = z.infer<typeof ZGetMultiSignDocumentRequestSchema>; export type TGetMultiSignDocumentRequestSchema = z.infer<typeof ZGetMultiSignDocumentRequestSchema>;

View File

@ -1,6 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema'; import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({ export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
@ -19,16 +18,8 @@ export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
export const ZGetEnvelopeItemsByTokenResponseSchema = z.object({ export const ZGetEnvelopeItemsByTokenResponseSchema = z.object({
envelopeItems: EnvelopeItemSchema.pick({ envelopeItems: EnvelopeItemSchema.pick({
id: true, id: true,
envelopeId: true,
title: true, title: true,
order: true, order: true,
}) }).array(),
.extend({
documentData: DocumentDataSchema.pick({
type: true,
id: true,
data: true,
initialData: true,
}),
})
.array(),
}); });

View File

@ -1,61 +0,0 @@
import { useState } from 'react';
import type { DocumentData } from '@prisma/client';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Dialog, DialogOverlay, DialogPortal, DialogTrigger } from '../../primitives/dialog';
import PDFViewer from '../../primitives/pdf-viewer';
export type DocumentDialogProps = {
trigger?: React.ReactNode;
documentData: DocumentData;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
/**
* A dialog which renders the provided document.
*/
export default function DocumentDialog({ trigger, documentData, ...props }: DocumentDialogProps) {
const [documentLoaded, setDocumentLoaded] = useState(false);
const onDocumentLoad = () => {
setDocumentLoaded(true);
};
return (
<Dialog {...props}>
<DialogPortal>
<DialogOverlay className="bg-black/80" />
{trigger && (
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger}
</DialogTrigger>
)}
<DialogPrimitive.Content
className={cn(
'animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 pointer-events-none fixed z-50 h-screen w-screen overflow-y-auto px-2 py-14 opacity-0 transition-opacity lg:py-32',
{
'opacity-100': documentLoaded,
},
)}
onClick={() => props.onOpenChange?.(false)}
>
<PDFViewer
className="mx-auto w-full max-w-3xl xl:max-w-5xl"
documentData={documentData}
onClick={(e) => e.stopPropagation()}
onDocumentLoad={onDocumentLoad}
/>
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none">
<X className="h-6 w-6 text-white" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
</Dialog>
);
}

View File

@ -1,69 +0,0 @@
import type { HTMLAttributes } from 'react';
import { useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentData } from '@prisma/client';
import { Download } from 'lucide-react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Button } from '../../primitives/button';
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
disabled?: boolean;
fileName?: string;
documentData?: DocumentData;
};
export const DocumentDownloadButton = ({
className,
fileName,
documentData,
disabled,
...props
}: DownloadButtonProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const onDownloadClick = async () => {
try {
setIsLoading(true);
if (!documentData) {
setIsLoading(false);
return;
}
await downloadPDF({ documentData, fileName }).then(() => {
setIsLoading(false);
});
} catch (err) {
setIsLoading(false);
toast({
title: _('Something went wrong'),
description: _('An error occurred while downloading your document.'),
variant: 'destructive',
});
}
};
return (
<Button
type="button"
variant="outline"
className={className}
disabled={disabled || !documentData}
onClick={onDownloadClick}
loading={isLoading}
{...props}
>
{!isLoading && <Download className="mr-2 h-5 w-5" />}
<Trans>Download</Trans>
</Button>
);
};

View File

@ -58,7 +58,7 @@ const Combobox = ({
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" side="bottom" align="start"> <PopoverContent className="z-[1001] p-0" side="bottom" align="start">
<Command> <Command>
<CommandInput placeholder={value || placeholderValue} /> <CommandInput placeholder={value || placeholderValue} />

View File

@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentData } from '@prisma/client'; import type { EnvelopeItem } from '@prisma/client';
import { base64 } from '@scure/base';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { type PDFDocumentProxy } from 'pdfjs-dist'; import { type PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
@ -11,7 +12,7 @@ import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css'; import 'react-pdf/dist/esm/Page/TextLayer.css';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { useToast } from './use-toast'; import { useToast } from './use-toast';
@ -48,17 +49,23 @@ const PDFLoader = () => (
export type PDFViewerProps = { export type PDFViewerProps = {
className?: string; className?: string;
documentData: Pick<DocumentData, 'type' | 'data'>; envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
version: 'original' | 'signed';
onDocumentLoad?: (_doc: LoadedPDFDocument) => void; onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick; onPageClick?: OnPDFViewerPageClick;
overrideData?: string;
[key: string]: unknown; [key: string]: unknown;
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>; } & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
export const PDFViewer = ({ export const PDFViewer = ({
className, className,
documentData, envelopeItem,
token,
version,
onDocumentLoad, onDocumentLoad,
onPageClick, onPageClick,
overrideData,
...props ...props
}: PDFViewerProps) => { }: PDFViewerProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
@ -67,17 +74,14 @@ export const PDFViewer = ({
const $el = useRef<HTMLDivElement>(null); const $el = useRef<HTMLDivElement>(null);
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false); const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(null); const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(
overrideData ? base64.decode(overrideData) : null,
);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [numPages, setNumPages] = useState(0); const [numPages, setNumPages] = useState(0);
const [pdfError, setPdfError] = useState(false); const [pdfError, setPdfError] = useState(false);
const memoizedData = useMemo(
() => ({ type: documentData.type, data: documentData.data }),
[documentData.data, documentData.type],
);
const isLoading = isDocumentBytesLoading || !documentBytes; const isLoading = isDocumentBytesLoading || !documentBytes;
const onDocumentLoaded = (doc: LoadedPDFDocument) => { const onDocumentLoaded = (doc: LoadedPDFDocument) => {
@ -142,13 +146,26 @@ export const PDFViewer = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (overrideData) {
const bytes = base64.decode(overrideData);
setDocumentBytes(bytes);
return;
}
const fetchDocumentBytes = async () => { const fetchDocumentBytes = async () => {
try { try {
setIsDocumentBytesLoading(true); setIsDocumentBytesLoading(true);
const bytes = await getFile(memoizedData); const documentUrl = getEnvelopeDownloadUrl({
envelopeItem: envelopeItem,
token,
version,
});
setDocumentBytes(bytes); const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
setDocumentBytes(new Uint8Array(bytes));
setIsDocumentBytesLoading(false); setIsDocumentBytesLoading(false);
} catch (err) { } catch (err) {
@ -163,7 +180,7 @@ export const PDFViewer = ({
}; };
void fetchDocumentBytes(); void fetchDocumentBytes();
}, [memoizedData, toast]); }, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
return ( return (
<div ref={$el} className={cn('overflow-hidden', className)} {...props}> <div ref={$el} className={cn('overflow-hidden', className)} {...props}>