mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
3 Commits
717fa8f870
...
fc2e9af6a0
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2e9af6a0 | |||
| a810d20a4f | |||
| 22011fd4ba |
@ -19,13 +19,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
type DocumentDuplicateDialogProps = {
|
||||
id: number;
|
||||
id: string;
|
||||
token?: string;
|
||||
open: boolean;
|
||||
onOpenChange: (_open: boolean) => void;
|
||||
};
|
||||
|
||||
export const DocumentDuplicateDialog = ({
|
||||
id,
|
||||
token,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: DocumentDuplicateDialogProps) => {
|
||||
@ -36,42 +38,38 @@ export const DocumentDuplicateDialog = ({
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: document, isLoading } = trpcReact.document.get.useQuery(
|
||||
{
|
||||
documentId: id,
|
||||
},
|
||||
{
|
||||
queryHash: `document-duplicate-dialog-${id}`,
|
||||
enabled: open === true,
|
||||
},
|
||||
);
|
||||
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
|
||||
trpcReact.envelope.item.getManyByToken.useQuery(
|
||||
{
|
||||
envelopeId: id,
|
||||
access: token ? { type: 'recipient', token } : { type: 'user' },
|
||||
},
|
||||
{
|
||||
enabled: open,
|
||||
},
|
||||
);
|
||||
|
||||
const documentData = document?.documentData
|
||||
? {
|
||||
...document.documentData,
|
||||
data: document.documentData.initialData,
|
||||
}
|
||||
: undefined;
|
||||
const envelopeItems = envelopeItemsPayload?.envelopeItems || [];
|
||||
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
|
||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
||||
trpcReact.document.duplicate.useMutation({
|
||||
onSuccess: async ({ id }) => {
|
||||
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
|
||||
trpcReact.envelope.duplicate.useMutation({
|
||||
onSuccess: async ({ duplicatedEnvelopeId }) => {
|
||||
toast({
|
||||
title: _(msg`Document Duplicated`),
|
||||
description: _(msg`Your document has been successfully duplicated.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
await navigate(`${documentsPath}/${id}/edit`);
|
||||
await navigate(`${documentsPath}/${duplicatedEnvelopeId}/edit`);
|
||||
onOpenChange(false);
|
||||
},
|
||||
});
|
||||
|
||||
const onDuplicate = async () => {
|
||||
try {
|
||||
await duplicateDocument({ documentId: id });
|
||||
await duplicateEnvelope({ envelopeId: id });
|
||||
} catch {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@ -83,14 +81,14 @@ export const DocumentDuplicateDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||
<Dialog open={open} onOpenChange={(value) => !isDuplicating && onOpenChange(value)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Duplicate</Trans>
|
||||
</DialogTitle>
|
||||
</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">
|
||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
<Trans>Loading Document...</Trans>
|
||||
@ -98,7 +96,12 @@ export const DocumentDuplicateDialog = ({
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
|
||||
@ -115,8 +118,8 @@ export const DocumentDuplicateDialog = ({
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
disabled={isDuplicateLoading || isLoading}
|
||||
loading={isDuplicateLoading}
|
||||
disabled={isDuplicating}
|
||||
loading={isDuplicating}
|
||||
onClick={onDuplicate}
|
||||
className="flex-1"
|
||||
>
|
||||
|
||||
@ -2,11 +2,10 @@ import { useState } from 'react';
|
||||
|
||||
import { useLingui } 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 { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@ -20,9 +19,7 @@ import {
|
||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'title' | 'order'> & {
|
||||
documentData: DocumentData;
|
||||
};
|
||||
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'envelopeId' | 'title' | 'order'>;
|
||||
|
||||
type EnvelopeDownloadDialogProps = {
|
||||
envelopeId: string;
|
||||
@ -87,19 +84,11 @@ export const EnvelopeDownloadDialog = ({
|
||||
}));
|
||||
|
||||
try {
|
||||
const downloadUrl = token
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${envelopeItemId}/download/${version}`
|
||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${envelopeItemId}/download/${version}`;
|
||||
|
||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||
|
||||
const baseTitle = envelopeItem.title.replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
const filename = `${baseTitle}${suffix}`;
|
||||
|
||||
downloadFile({
|
||||
filename,
|
||||
data: blob,
|
||||
await downloadPDF({
|
||||
envelopeItem,
|
||||
token,
|
||||
fileName: envelopeItem.title,
|
||||
version,
|
||||
});
|
||||
|
||||
setIsDownloadingState((prev) => ({
|
||||
@ -140,7 +129,7 @@ export const EnvelopeDownloadDialog = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{isLoadingEnvelopeItems ? (
|
||||
<>
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
{Array.from({ length: 1 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-border bg-card flex items-center gap-2 rounded-lg border p-4"
|
||||
@ -169,6 +158,7 @@ export const EnvelopeDownloadDialog = ({
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
{/* Todo: Envelopes - Fix overflow */}
|
||||
<h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
<Trans>PDF Document</Trans>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentData, FieldType } from '@prisma/client';
|
||||
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { base64 } from '@scure/base';
|
||||
import { ChevronsUpDown } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
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 { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
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 { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { useRecipientColors } from '@documenso/ui/lib/recipient-colors';
|
||||
@ -83,21 +83,14 @@ export const ConfigureFieldsView = ({
|
||||
|
||||
const normalizedDocumentData = useMemo(() => {
|
||||
if (documentData) {
|
||||
return documentData;
|
||||
return documentData.data;
|
||||
}
|
||||
|
||||
if (!configData.documentData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = base64.encode(configData.documentData?.data);
|
||||
|
||||
return {
|
||||
id: 'preview',
|
||||
type: 'BYTES_64',
|
||||
data,
|
||||
initialData: data,
|
||||
} satisfies DocumentData;
|
||||
return base64.encode(configData.documentData.data);
|
||||
}, [configData.documentData]);
|
||||
|
||||
const recipients = useMemo(() => {
|
||||
@ -541,7 +534,15 @@ export const ConfigureFieldsView = ({
|
||||
<Form {...form}>
|
||||
{normalizedDocumentData && (
|
||||
<div>
|
||||
<PDFViewer documentData={normalizedDocumentData} />
|
||||
<PDFViewer
|
||||
overrideData={normalizedDocumentData}
|
||||
envelopeItem={{
|
||||
id: '',
|
||||
envelopeId: '',
|
||||
}}
|
||||
token={undefined}
|
||||
version="signed"
|
||||
/>
|
||||
|
||||
<ElementVisible
|
||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||
|
||||
@ -3,8 +3,8 @@ import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta, Recipient, Signature } from '@prisma/client';
|
||||
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
||||
import type { DocumentMeta, EnvelopeItem, Recipient, Signature } from '@prisma/client';
|
||||
import { type Field, FieldType } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useSearchParams } from 'react-router';
|
||||
@ -47,7 +47,7 @@ export type EmbedDirectTemplateClientPageProps = {
|
||||
token: string;
|
||||
envelopeId: string;
|
||||
updatedAt: Date;
|
||||
documentData: DocumentData;
|
||||
envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
metadata?: DocumentMeta | null;
|
||||
@ -59,7 +59,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
token,
|
||||
envelopeId,
|
||||
updatedAt,
|
||||
documentData,
|
||||
envelopeItems,
|
||||
recipient,
|
||||
fields,
|
||||
metadata,
|
||||
@ -335,7 +335,9 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
{/* Viewer */}
|
||||
<div className="flex-1">
|
||||
<PDFViewer
|
||||
documentData={documentData}
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={token}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -3,14 +3,8 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { DocumentMeta } from '@prisma/client';
|
||||
import {
|
||||
type DocumentData,
|
||||
type Field,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
import type { DocumentMeta, EnvelopeItem } from '@prisma/client';
|
||||
import { type Field, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
|
||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||
@ -50,7 +44,7 @@ export type EmbedSignDocumentClientPageProps = {
|
||||
token: string;
|
||||
documentId: number;
|
||||
envelopeId: string;
|
||||
documentData: DocumentData;
|
||||
envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
completedFields: DocumentField[];
|
||||
@ -65,7 +59,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
token,
|
||||
documentId,
|
||||
envelopeId,
|
||||
documentData,
|
||||
envelopeItems,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
@ -293,7 +287,9 @@ export const EmbedSignDocumentClientPage = ({
|
||||
{/* Viewer */}
|
||||
<div className="embed--DocumentViewer flex-1">
|
||||
<PDFViewer
|
||||
documentData={documentData}
|
||||
envelopeItem={envelopeItems[0]}
|
||||
token={token}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -226,7 +226,9 @@ export const MultiSignDocumentSigningView = ({
|
||||
})}
|
||||
>
|
||||
<PDFViewer
|
||||
documentData={document.documentData}
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={token}
|
||||
version="signed"
|
||||
onDocumentLoad={() => {
|
||||
setHasDocumentLoaded(true);
|
||||
onDocumentReady?.();
|
||||
|
||||
@ -153,7 +153,9 @@ export const DirectTemplatePageView = ({
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer
|
||||
key={template.id}
|
||||
documentData={template.templateDocumentData}
|
||||
envelopeItem={template.envelopeItems[0]}
|
||||
token={directTemplateRecipient.token}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@ -245,7 +245,12 @@ export const DocumentSigningPageViewV1 = ({
|
||||
<div className="flex-1">
|
||||
<Card className="rounded-xl before:rounded-xl" gradient>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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 {
|
||||
@ -22,9 +23,10 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
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 EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer';
|
||||
import { ShareDocumentDownloadButton } from '../share-document-download-button';
|
||||
|
||||
export type DocumentCertificateQRViewProps = {
|
||||
documentId: number;
|
||||
@ -34,6 +36,7 @@ export type DocumentCertificateQRViewProps = {
|
||||
documentTeamUrl: string;
|
||||
recipientCount?: number;
|
||||
completedDate?: Date;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const DocumentCertificateQRView = ({
|
||||
@ -44,6 +47,7 @@ export const DocumentCertificateQRView = ({
|
||||
documentTeamUrl,
|
||||
recipientCount = 0,
|
||||
completedDate,
|
||||
token,
|
||||
}: DocumentCertificateQRViewProps) => {
|
||||
const { data: documentViaUser } = trpc.document.get.useQuery({
|
||||
documentId,
|
||||
@ -96,11 +100,12 @@ export const DocumentCertificateQRView = ({
|
||||
)}
|
||||
|
||||
{internalVersion === 2 ? (
|
||||
<EnvelopeRenderProvider envelope={{ envelopeItems }}>
|
||||
<EnvelopeRenderProvider envelope={{ envelopeItems }} token={token}>
|
||||
<DocumentCertificateQrV2
|
||||
title={title}
|
||||
recipientCount={recipientCount}
|
||||
formattedDate={formattedDate}
|
||||
token={token}
|
||||
/>
|
||||
</EnvelopeRenderProvider>
|
||||
) : (
|
||||
@ -119,14 +124,27 @@ export const DocumentCertificateQRView = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ShareDocumentDownloadButton
|
||||
title={title}
|
||||
documentData={envelopeItems[0].documentData}
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelopeItems[0].envelopeId}
|
||||
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 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>
|
||||
</>
|
||||
)}
|
||||
@ -138,14 +156,16 @@ type DocumentCertificateQrV2Props = {
|
||||
title: string;
|
||||
recipientCount: number;
|
||||
formattedDate: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
const DocumentCertificateQrV2 = ({
|
||||
title,
|
||||
recipientCount,
|
||||
formattedDate,
|
||||
token,
|
||||
}: DocumentCertificateQrV2Props) => {
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-start">
|
||||
@ -163,12 +183,18 @@ const DocumentCertificateQrV2 = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentEnvelopeItem && (
|
||||
<ShareDocumentDownloadButton
|
||||
title={title}
|
||||
documentData={currentEnvelopeItem.documentData}
|
||||
/>
|
||||
)}
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelopeItems[0].envelopeId}
|
||||
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 className="mt-12 w-full">
|
||||
|
||||
@ -441,9 +441,10 @@ export const DocumentEditForm = ({
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer
|
||||
key={document.documentData.id}
|
||||
documentData={document.documentData}
|
||||
document={document}
|
||||
key={document.envelopeItems[0].id}
|
||||
envelopeItem={document.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
|
||||
@ -23,9 +19,6 @@ export type DocumentPageViewButtonProps = {
|
||||
export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => {
|
||||
const { user } = useSession();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const recipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
||||
|
||||
const isRecipient = !!recipient;
|
||||
@ -37,25 +30,6 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
const documentsPath = formatDocumentsPath(envelope.team.url);
|
||||
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({
|
||||
isRecipient,
|
||||
isPending,
|
||||
@ -95,7 +69,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
</Link>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true, internalVersion: 2 }, () => (
|
||||
.with({ isComplete: true }, () => (
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelope.id}
|
||||
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);
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
@ -16,13 +15,11 @@ import {
|
||||
} from 'lucide-react';
|
||||
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 type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||
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 {
|
||||
DropdownMenu,
|
||||
@ -67,64 +64,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
|
||||
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');
|
||||
|
||||
return (
|
||||
@ -147,36 +86,20 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{envelope.internalVersion === 2 ? (
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelope.id}
|
||||
envelopeStatus={envelope.status}
|
||||
token={recipient?.token}
|
||||
envelopeItems={envelope.envelopeItems}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isComplete && (
|
||||
<DropdownMenuItem onClick={onDownloadClick}>
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={envelope.id}
|
||||
envelopeStatus={envelope.status}
|
||||
token={recipient?.token}
|
||||
envelopeItems={envelope.envelopeItems}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={`${documentsPath}/${envelope.id}/logs`}>
|
||||
@ -250,7 +173,8 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
||||
|
||||
{isDuplicateDialogOpen && (
|
||||
<DocumentDuplicateDialog
|
||||
id={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
||||
id={envelope.id}
|
||||
token={recipient?.token}
|
||||
open={isDuplicateDialogOpen}
|
||||
onOpenChange={setDuplicateDialogOpen}
|
||||
/>
|
||||
|
||||
@ -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 { 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 { 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 PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
|
||||
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'));
|
||||
|
||||
// Todo: Envelopes - Dynamically import faker
|
||||
export const EnvelopeEditorPreviewPage = () => {
|
||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||
|
||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||
|
||||
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
|
||||
'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.
|
||||
*/
|
||||
@ -31,40 +195,38 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null);
|
||||
}, []);
|
||||
|
||||
// Override the parent renderer provider so we can inject custom fields.
|
||||
return (
|
||||
<div className="relative flex h-full">
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
<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="flex w-full flex-col overflow-y-auto">
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex flex-col items-center justify-center">
|
||||
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex flex-col items-center justify-center">
|
||||
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</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 ? (
|
||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
|
||||
<PDFViewerKonvaLazy
|
||||
renderer="editor"
|
||||
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-32">
|
||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||
@ -78,27 +240,28 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Form Fields Panel */}
|
||||
{currentEnvelopeItem && false && (
|
||||
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||
{/* Add fields section. */}
|
||||
<section className="px-4">
|
||||
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
{/* Right Section - Form Fields Panel */}
|
||||
{currentEnvelopeItem && false && (
|
||||
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||
{/* Add fields section. */}
|
||||
<section className="px-4">
|
||||
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Preivew Mode</Trans>
|
||||
</h3> */}
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert variant="neutral">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Preview what the signed document will look like with placeholder data
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* <Alert variant="neutral">
|
||||
{/* <Alert variant="neutral">
|
||||
<RadioGroup
|
||||
className="gap-y-1"
|
||||
value={selectedPreviewMode}
|
||||
@ -137,36 +300,37 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
<div>Preview what a recipient will see</div>
|
||||
|
||||
<div>Preview the signed document</div> */}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{false && (
|
||||
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
||||
{selectedPreviewMode === 'recipient' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
{false && (
|
||||
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
||||
{selectedPreviewMode === 'recipient' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Recipient selector section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
{/* Recipient selector section. */}
|
||||
<section className="px-4">
|
||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||
<Trans>Selected Recipient</Trans>
|
||||
</h3>
|
||||
|
||||
<RecipientSelector
|
||||
selectedRecipient={editorFields.selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) =>
|
||||
editorFields.setSelectedRecipient(recipient.id)
|
||||
}
|
||||
recipients={envelope.recipients}
|
||||
className="w-full"
|
||||
align="end"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</AnimateGenericFadeInOut>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RecipientSelector
|
||||
selectedRecipient={editorFields.selectedRecipient}
|
||||
onSelectedRecipientChange={(recipient) =>
|
||||
editorFields.setSelectedRecipient(recipient.id)
|
||||
}
|
||||
recipients={envelope.recipients}
|
||||
className="w-full"
|
||||
align="end"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</AnimateGenericFadeInOut>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</EnvelopeRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -174,7 +174,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
const { t, i18n } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { envelope } = useCurrentEnvelopeEditor();
|
||||
const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
const organisation = useCurrentOrganisation();
|
||||
@ -186,14 +186,12 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
documentAuth: envelope.authOptions,
|
||||
});
|
||||
|
||||
const form = useForm<TAddSettingsFormSchema>({
|
||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||
defaultValues: {
|
||||
externalId: envelope.externalId || '', // Todo: String or undefined?
|
||||
const createDefaultValues = () => {
|
||||
return {
|
||||
externalId: envelope.externalId || '',
|
||||
visibility: envelope.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||
|
||||
meta: {
|
||||
subject: envelope.documentMeta.subject ?? '',
|
||||
message: envelope.documentMeta.message ?? '',
|
||||
@ -210,10 +208,13 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
|
||||
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation();
|
||||
const form = useForm<TAddSettingsFormSchema>({
|
||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||
defaultValues: createDefaultValues(),
|
||||
});
|
||||
|
||||
const envelopeHasBeenSent =
|
||||
envelope.type === EnvelopeType.DOCUMENT &&
|
||||
@ -239,8 +240,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
.safeParse(data.globalAccessAuth);
|
||||
|
||||
try {
|
||||
await updateEnvelope({
|
||||
envelopeId: envelope.id,
|
||||
await updateEnvelopeAsync({
|
||||
data: {
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
@ -295,7 +295,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
form.reset(createDefaultValues());
|
||||
setActiveTab('general');
|
||||
}, [open, form]);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||
export default function EnvelopeGenericPageRenderer() {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } =
|
||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError, overrideSettings } =
|
||||
useCurrentEnvelopeRender();
|
||||
|
||||
const {
|
||||
@ -50,12 +50,11 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
...field,
|
||||
customText: '',
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
inserted: false,
|
||||
customText: field.inserted ? field.customText : '',
|
||||
fieldMeta: field.fieldMeta,
|
||||
},
|
||||
translations: getClientSideFieldTranslations(i18n),
|
||||
@ -63,7 +62,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: false,
|
||||
mode: 'sign',
|
||||
mode: overrideSettings?.mode ?? 'sign',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -313,8 +313,10 @@ export const TemplateEditForm = ({
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<PDFViewer
|
||||
key={templateDocumentData.id}
|
||||
documentData={templateDocumentData}
|
||||
key={template.envelopeItems[0].id}
|
||||
envelopeItem={template.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="signed"
|
||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
@ -25,8 +20,6 @@ export type DocumentsTableActionButtonProps = {
|
||||
|
||||
export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => {
|
||||
const { user } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
@ -44,39 +37,6 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
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
|
||||
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||
return null;
|
||||
@ -134,7 +94,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
<Trans>View</Trans>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true, internalVersion: 2 }, () => (
|
||||
.with({ isComplete: true }, () => (
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={row.envelopeId}
|
||||
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>);
|
||||
};
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
Download,
|
||||
Edit,
|
||||
EyeIcon,
|
||||
FileDown,
|
||||
FolderInput,
|
||||
Loader,
|
||||
MoreHorizontal,
|
||||
@ -20,12 +19,10 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
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 {
|
||||
DropdownMenu,
|
||||
@ -34,7 +31,6 @@ import {
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
||||
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||
@ -56,7 +52,6 @@ export const DocumentsTableActionDropdown = ({
|
||||
const { user } = useSession();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -76,58 +71,6 @@ export const DocumentsTableActionDropdown = ({
|
||||
const documentsPath = formatDocumentsPath(team.url);
|
||||
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');
|
||||
|
||||
return (
|
||||
@ -178,33 +121,19 @@ export const DocumentsTableActionDropdown = ({
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{row.internalVersion === 2 ? (
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={row.envelopeId}
|
||||
envelopeStatus={row.status}
|
||||
token={recipient?.token}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={row.envelopeId}
|
||||
envelopeStatus={row.status}
|
||||
token={recipient?.token}
|
||||
trigger={
|
||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
||||
<FileDown className="mr-2 h-4 w-4" />
|
||||
<Trans>Download Original</Trans>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
@ -273,7 +202,8 @@ export const DocumentsTableActionDropdown = ({
|
||||
/>
|
||||
|
||||
<DocumentDuplicateDialog
|
||||
id={row.id}
|
||||
id={row.envelopeId}
|
||||
token={recipient?.token}
|
||||
open={isDuplicateDialogOpen}
|
||||
onOpenChange={setDuplicateDialogOpen}
|
||||
/>
|
||||
|
||||
@ -10,11 +10,9 @@ import { DateTime } from 'luxon';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
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 { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
||||
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 { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
|
||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||
|
||||
export type DocumentsTableProps = {
|
||||
@ -199,28 +198,6 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
||||
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
|
||||
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||
return null;
|
||||
@ -230,6 +207,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
||||
isPending,
|
||||
isComplete,
|
||||
isSigned,
|
||||
internalVersion: row.internalVersion,
|
||||
})
|
||||
.with({ isPending: true, isSigned: false }, () => (
|
||||
<Button className="w-32" asChild>
|
||||
@ -263,10 +241,17 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true }, () => (
|
||||
<Button className="w-32" onClick={onDownloadClick}>
|
||||
<DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
<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" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))
|
||||
.otherwise(() => <div></div>);
|
||||
};
|
||||
|
||||
@ -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">
|
||||
<EnvelopeRenderProvider
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields}
|
||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
||||
>
|
||||
@ -181,9 +182,10 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
||||
)}
|
||||
|
||||
<PDFViewer
|
||||
document={envelope}
|
||||
envelopeItem={envelope.envelopeItems[0]}
|
||||
token={undefined}
|
||||
key={envelope.envelopeItems[0].id}
|
||||
documentData={envelope.envelopeItems[0].documentData}
|
||||
version="signed"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -101,6 +101,7 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
|
||||
<EnvelopeEditorProvider initialEnvelope={envelope}>
|
||||
<EnvelopeRenderProvider
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={envelope.fields}
|
||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
||||
>
|
||||
|
||||
@ -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">
|
||||
<EnvelopeRenderProvider
|
||||
envelope={envelope}
|
||||
token={undefined}
|
||||
fields={envelope.fields}
|
||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
||||
>
|
||||
@ -203,9 +204,10 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
||||
/>
|
||||
|
||||
<PDFViewer
|
||||
document={envelope}
|
||||
envelopeItem={envelope.envelopeItems[0]}
|
||||
token={undefined}
|
||||
version="signed"
|
||||
key={envelope.envelopeItems[0].id}
|
||||
documentData={envelope.envelopeItems[0].documentData}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -245,7 +245,7 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
>
|
||||
<EnvelopeRenderProvider envelope={envelope}>
|
||||
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
|
||||
<DocumentSigningPageViewV2 />
|
||||
</EnvelopeRenderProvider>
|
||||
</DocumentSigningAuthProvider>
|
||||
|
||||
@ -492,7 +492,7 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
>
|
||||
<EnvelopeRenderProvider envelope={envelope}>
|
||||
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
|
||||
<DocumentSigningPageViewV2 />
|
||||
</EnvelopeRenderProvider>
|
||||
</DocumentSigningAuthProvider>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
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 { 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 { env } from '@documenso/lib/utils/env';
|
||||
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 { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||
import { ClaimAccount } from '~/components/general/claim-account';
|
||||
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">
|
||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||
|
||||
{isDocumentCompleted(document.status) ? (
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={document.documentData}
|
||||
disabled={!isDocumentCompleted(document.status)}
|
||||
/>
|
||||
) : (
|
||||
<DocumentDialog
|
||||
documentData={document.documentData}
|
||||
{isDocumentCompleted(document.status) && (
|
||||
<EnvelopeDownloadDialog
|
||||
envelopeId={document.envelopeId}
|
||||
envelopeStatus={document.status}
|
||||
envelopeItems={document.envelopeItems}
|
||||
token={recipient?.token}
|
||||
trigger={
|
||||
<Button
|
||||
className="text-[11px]"
|
||||
title={_(msg`Signatures will appear once the document has been completed`)}
|
||||
variant="outline"
|
||||
>
|
||||
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
|
||||
<Trans>View Original Document</Trans>
|
||||
<Button type="button" variant="outline" className="flex-1">
|
||||
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -60,6 +60,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
|
||||
|
||||
return {
|
||||
document,
|
||||
token: slug,
|
||||
};
|
||||
}
|
||||
|
||||
@ -74,7 +75,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
|
||||
};
|
||||
|
||||
export default function SharePage() {
|
||||
const { document } = useLoaderData<typeof loader>();
|
||||
const { document, token } = useLoaderData<typeof loader>();
|
||||
|
||||
if (document) {
|
||||
return (
|
||||
@ -86,6 +87,7 @@ export default function SharePage() {
|
||||
envelopeItems={document.envelopeItems}
|
||||
recipientCount={document.recipientCount}
|
||||
completedDate={document.completedAt ?? undefined}
|
||||
token={token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ export default function EmbedDirectTemplatePage() {
|
||||
token={token}
|
||||
envelopeId={template.envelopeId}
|
||||
updatedAt={template.updatedAt}
|
||||
documentData={template.templateDocumentData}
|
||||
envelopeItems={template.envelopeItems}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
metadata={template.templateMeta}
|
||||
|
||||
@ -165,7 +165,7 @@ export default function EmbedSignDocumentPage() {
|
||||
token={token}
|
||||
documentId={document.id}
|
||||
envelopeId={document.envelopeId}
|
||||
documentData={document.documentData}
|
||||
envelopeItems={document.envelopeItems}
|
||||
recipient={recipient}
|
||||
fields={fields}
|
||||
completedFields={completedFields}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@epic-web/remember": "^1.1.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hono/node-server": "^1.13.7",
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
@ -104,4 +105,4 @@
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { sValidator } from '@hono/standard-validator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
@ -206,17 +207,28 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
async (c) => {
|
||||
const { token, envelopeItemId } = c.req.valid('param');
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findFirst({
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (token.startsWith('qr_')) {
|
||||
envelopeWhereQuery = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
qrToken: token,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||
where: envelopeWhereQuery,
|
||||
include: {
|
||||
envelope: true,
|
||||
documentData: true,
|
||||
@ -247,17 +259,28 @@ export const filesRoute = new Hono<HonoEnv>()
|
||||
async (c) => {
|
||||
const { token, envelopeItemId, version } = c.req.valid('param');
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findFirst({
|
||||
where: {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
recipients: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (token.startsWith('qr_')) {
|
||||
envelopeWhereQuery = {
|
||||
id: envelopeItemId,
|
||||
envelope: {
|
||||
qrToken: token,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||
where: envelopeWhereQuery,
|
||||
include: {
|
||||
envelope: true,
|
||||
documentData: true,
|
||||
|
||||
180
package-lock.json
generated
180
package-lock.json
generated
@ -45,6 +45,7 @@
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^6.18.0",
|
||||
"prisma-extension-kysely": "^3.0.0",
|
||||
"prisma-json-types-generator": "^3.6.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"superjson": "^2.2.5",
|
||||
@ -112,6 +113,7 @@
|
||||
"@documenso/trpc": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@epic-web/remember": "^1.1.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@hono/node-server": "^1.13.7",
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
@ -3511,6 +3513,22 @@
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||
@ -7599,6 +7617,18 @@
|
||||
"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": {
|
||||
"version": "6.18.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
|
||||
@ -7635,10 +7659,10 @@
|
||||
"@prisma/get-platform": "6.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
|
||||
"node_modules/@prisma/generator": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
|
||||
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/generator-helper": {
|
||||
@ -7719,12 +7743,6 @@
|
||||
"@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": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz",
|
||||
@ -28646,6 +28664,54 @@
|
||||
"@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": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz",
|
||||
@ -36456,24 +36522,6 @@
|
||||
"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": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
|
||||
@ -37130,72 +37178,6 @@
|
||||
"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": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^6.18.0",
|
||||
"prisma-extension-kysely": "^3.0.0",
|
||||
"prisma-json-types-generator": "^3.6.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"turbo": "^1.9.3",
|
||||
|
||||
@ -559,5 +559,10 @@ test.describe('API V2 Envelopes', () => {
|
||||
);
|
||||
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
||||
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
||||
|
||||
console.log({
|
||||
createdEnvelopeId: finalEnvelope.id,
|
||||
userEmail: userA.email,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,8 +25,7 @@ import { DocumentStatus } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
@ -95,7 +94,13 @@ test('field placement visual regression', async ({ page }, testInfo) => {
|
||||
|
||||
await Promise.all(
|
||||
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
|
||||
.filter((image) => image.includes(item.title))
|
||||
@ -103,7 +108,7 @@ test('field placement visual regression', async ({ page }, testInfo) => {
|
||||
|
||||
await compareSignedPdfWithImages({
|
||||
id: item.title.replaceAll(' ', '-').toLowerCase(),
|
||||
pdfData,
|
||||
pdfData: new Uint8Array(pdfData),
|
||||
images: loadedImages,
|
||||
testInfo,
|
||||
});
|
||||
@ -174,9 +179,15 @@ test.skip('download envelope images', async ({ page }) => {
|
||||
|
||||
await Promise.all(
|
||||
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()) {
|
||||
fs.writeFileSync(
|
||||
|
||||
@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||
|
||||
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 { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
@ -25,20 +25,25 @@ test.describe('Signing Certificate Tests', () => {
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const documentData = await prisma.documentData
|
||||
const recipient = recipients[0];
|
||||
|
||||
const documentData = await prisma.envelopeItem
|
||||
.findFirstOrThrow({
|
||||
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 recipient = recipients[0];
|
||||
|
||||
// Sign the document
|
||||
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
|
||||
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({
|
||||
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 recipient = recipients[0];
|
||||
|
||||
// Sign the document
|
||||
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
|
||||
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({
|
||||
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 recipient = recipients[0];
|
||||
const originalPdf = await PDFDocument.load(new Uint8Array(documentData));
|
||||
|
||||
// Sign the document
|
||||
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
|
||||
const completedPdf = await PDFDocument.load(completedDocumentData);
|
||||
|
||||
@ -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';
|
||||
|
||||
type DocumentVersion = 'original' | 'signed';
|
||||
|
||||
type DownloadPDFProps = {
|
||||
documentData: DocumentData;
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
|
||||
fileName?: string;
|
||||
/**
|
||||
* Specifies which version of the document to download.
|
||||
@ -17,18 +19,18 @@ type DownloadPDFProps = {
|
||||
};
|
||||
|
||||
export const downloadPDF = async ({
|
||||
documentData,
|
||||
envelopeItem,
|
||||
token,
|
||||
fileName,
|
||||
version = 'signed',
|
||||
}: DownloadPDFProps) => {
|
||||
const bytes = await getFile({
|
||||
type: documentData.type,
|
||||
data: version === 'signed' ? documentData.data : documentData.initialData,
|
||||
const downloadUrl = getEnvelopeDownloadUrl({
|
||||
envelopeItem: envelopeItem,
|
||||
token,
|
||||
version,
|
||||
});
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
|
||||
@ -46,6 +46,7 @@ type EnvelopeEditorProviderValue = {
|
||||
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
|
||||
|
||||
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
|
||||
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
|
||||
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
||||
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
||||
|
||||
@ -66,8 +67,6 @@ type EnvelopeEditorProviderValue = {
|
||||
};
|
||||
|
||||
syncEnvelope: () => Promise<void>;
|
||||
// refetchEnvelope: () => Promise<void>;
|
||||
// updateEnvelope: (envelope: TEnvelope) => Promise<void>;
|
||||
};
|
||||
|
||||
interface EnvelopeEditorProviderProps {
|
||||
@ -236,6 +235,13 @@ export const EnvelopeEditorProvider = ({
|
||||
setEnvelopeDebounced(envelopeUpdates);
|
||||
};
|
||||
|
||||
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
|
||||
await envelopeUpdateMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
...envelopeUpdates,
|
||||
});
|
||||
};
|
||||
|
||||
const getRecipientColorKey = useCallback(
|
||||
(recipientId: number) => {
|
||||
const recipientIndex = envelope.recipients.findIndex(
|
||||
@ -323,6 +329,7 @@ export const EnvelopeEditorProvider = ({
|
||||
setLocalEnvelope,
|
||||
getRecipientColorKey,
|
||||
updateEnvelope,
|
||||
updateEnvelopeAsync,
|
||||
setRecipientsDebounced,
|
||||
setRecipientsAsync,
|
||||
editorFields,
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
|
||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
|
||||
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { getEnvelopeDownloadUrl } from '../../utils/envelope-download';
|
||||
|
||||
type FileData =
|
||||
| {
|
||||
@ -18,6 +16,10 @@ type FileData =
|
||||
status: 'loaded';
|
||||
};
|
||||
|
||||
type EnvelopeRenderOverrideSettings = {
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
};
|
||||
|
||||
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
||||
|
||||
type EnvelopeRenderProviderValue = {
|
||||
@ -30,10 +32,12 @@ type EnvelopeRenderProviderValue = {
|
||||
|
||||
renderError: boolean;
|
||||
setRenderError: (renderError: boolean) => void;
|
||||
overrideSettings?: EnvelopeRenderOverrideSettings;
|
||||
};
|
||||
|
||||
interface EnvelopeRenderProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||
|
||||
/**
|
||||
@ -49,6 +53,18 @@ interface EnvelopeRenderProviderProps {
|
||||
* Only required for generic page renderers.
|
||||
*/
|
||||
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);
|
||||
@ -70,7 +86,9 @@ export const EnvelopeRenderProvider = ({
|
||||
children,
|
||||
envelope,
|
||||
fields,
|
||||
token,
|
||||
recipientIds = [],
|
||||
overrideSettings,
|
||||
}: EnvelopeRenderProviderProps) => {
|
||||
// Indexed by documentDataId.
|
||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||
@ -84,27 +102,35 @@ export const EnvelopeRenderProvider = ({
|
||||
[envelope.envelopeItems],
|
||||
);
|
||||
|
||||
const loadEnvelopeItemPdfFile = async (documentData: DocumentData) => {
|
||||
if (files[documentData.id]?.status === 'loading') {
|
||||
const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => {
|
||||
if (files[envelopeItem.documentDataId]?.status === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files[documentData.id]) {
|
||||
if (!files[envelopeItem.documentDataId]) {
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
[envelopeItem.documentDataId]: {
|
||||
status: 'loading',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
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) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
file,
|
||||
[envelopeItem.documentDataId]: {
|
||||
file: new Uint8Array(file),
|
||||
status: 'loaded',
|
||||
},
|
||||
}));
|
||||
@ -113,7 +139,7 @@ export const EnvelopeRenderProvider = ({
|
||||
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
[envelopeItem.documentDataId]: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
@ -145,7 +171,7 @@ export const EnvelopeRenderProvider = ({
|
||||
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]);
|
||||
|
||||
for (const item of missingFiles) {
|
||||
void loadEnvelopeItemPdfFile(item.documentData);
|
||||
void loadEnvelopeItemPdfFile(item);
|
||||
}
|
||||
}, [envelope.envelopeItems]);
|
||||
|
||||
@ -171,6 +197,7 @@ export const EnvelopeRenderProvider = ({
|
||||
getRecipientColorKey,
|
||||
renderError,
|
||||
setRenderError,
|
||||
overrideSettings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -78,6 +78,14 @@ export const adminFindDocuments = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
|
||||
@ -248,6 +248,14 @@ export const findDocuments = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
|
||||
@ -91,7 +91,7 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -63,5 +63,8 @@ export const getDocumentWithDetailsById = async ({
|
||||
documentId: legacyDocumentId,
|
||||
password: null,
|
||||
},
|
||||
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
|
||||
...envelopeItem,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } fro
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
||||
@ -72,20 +71,12 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
.array(),
|
||||
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
envelopeId: true,
|
||||
id: true,
|
||||
title: true,
|
||||
documentDataId: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
}).array(),
|
||||
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
@ -199,11 +190,7 @@ export const getEnvelopeForRecipientSigning = async ({
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -87,5 +87,9 @@ export const getTemplateByDirectLinkToken = async ({
|
||||
},
|
||||
recipients: recipientsWithMappedFields,
|
||||
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
|
||||
envelopeItems: envelope.envelopeItems.map((item) => ({
|
||||
id: item.id,
|
||||
envelopeId: item.envelopeId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
@ -94,5 +95,9 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
|
||||
}
|
||||
: null,
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
|
||||
id: envelopeItem.id,
|
||||
envelopeId: envelopeItem.envelopeId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
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 { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
@ -74,6 +75,10 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
||||
password: z.string().nullable().default(null),
|
||||
documentId: z.number().default(-1).optional(),
|
||||
}),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
}).array(),
|
||||
|
||||
folder: FolderSchema.pick({
|
||||
id: true,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
||||
@ -66,20 +65,12 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
|
||||
templateId: true,
|
||||
}).array(),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
envelopeId: true,
|
||||
id: true,
|
||||
title: true,
|
||||
documentDataId: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
}).array(),
|
||||
directLink: TemplateDirectLinkSchema.pick({
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
|
||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
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 TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
@ -87,6 +88,10 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}).nullable(),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TTemplate = z.infer<typeof ZTemplateSchema>;
|
||||
|
||||
19
packages/lib/utils/envelope-download.ts
Normal file
19
packages/lib/utils/envelope-download.ts
Normal 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}`;
|
||||
};
|
||||
@ -92,6 +92,14 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
|
||||
@ -4,6 +4,7 @@ import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
|
||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
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 SignatureSchema from '@documenso/prisma/generated/zod/modelSchema/SignatureSchema';
|
||||
|
||||
@ -40,6 +41,10 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({
|
||||
signature: SignatureSchema.nullable(),
|
||||
}),
|
||||
),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TGetMultiSignDocumentRequestSchema = z.infer<typeof ZGetMultiSignDocumentRequestSchema>;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
|
||||
export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
|
||||
@ -19,16 +18,8 @@ export const ZGetEnvelopeItemsByTokenRequestSchema = z.object({
|
||||
export const ZGetEnvelopeItemsByTokenResponseSchema = z.object({
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
}).array(),
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -58,7 +58,7 @@ const Combobox = ({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
<PopoverContent className="z-[1001] p-0" side="bottom" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={value || placeholderValue} />
|
||||
|
||||
|
||||
@ -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 { useLingui } from '@lingui/react';
|
||||
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 { type PDFDocumentProxy } from 'pdfjs-dist';
|
||||
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 { 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 { useToast } from './use-toast';
|
||||
@ -48,17 +49,23 @@ const PDFLoader = () => (
|
||||
|
||||
export type PDFViewerProps = {
|
||||
className?: string;
|
||||
documentData: Pick<DocumentData, 'type' | 'data'>;
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
version: 'original' | 'signed';
|
||||
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
||||
onPageClick?: OnPDFViewerPageClick;
|
||||
overrideData?: string;
|
||||
[key: string]: unknown;
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
||||
|
||||
export const PDFViewer = ({
|
||||
className,
|
||||
documentData,
|
||||
envelopeItem,
|
||||
token,
|
||||
version,
|
||||
onDocumentLoad,
|
||||
onPageClick,
|
||||
overrideData,
|
||||
...props
|
||||
}: PDFViewerProps) => {
|
||||
const { _ } = useLingui();
|
||||
@ -67,17 +74,14 @@ export const PDFViewer = ({
|
||||
const $el = useRef<HTMLDivElement>(null);
|
||||
|
||||
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 [numPages, setNumPages] = useState(0);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
|
||||
const memoizedData = useMemo(
|
||||
() => ({ type: documentData.type, data: documentData.data }),
|
||||
[documentData.data, documentData.type],
|
||||
);
|
||||
|
||||
const isLoading = isDocumentBytesLoading || !documentBytes;
|
||||
|
||||
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
|
||||
@ -142,13 +146,26 @@ export const PDFViewer = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (overrideData) {
|
||||
const bytes = base64.decode(overrideData);
|
||||
|
||||
setDocumentBytes(bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDocumentBytes = async () => {
|
||||
try {
|
||||
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);
|
||||
} catch (err) {
|
||||
@ -163,7 +180,7 @@ export const PDFViewer = ({
|
||||
};
|
||||
|
||||
void fetchDocumentBytes();
|
||||
}, [memoizedData, toast]);
|
||||
}, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
|
||||
|
||||
return (
|
||||
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
|
||||
|
||||
Reference in New Issue
Block a user