fix: add preview page

This commit is contained in:
David Nguyen
2025-11-05 17:18:15 +11:00
parent a810d20a4f
commit fc2e9af6a0
10 changed files with 383 additions and 132 deletions

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@
"@documenso/trpc": "*", "@documenso/trpc": "*",
"@documenso/ui": "*", "@documenso/ui": "*",
"@epic-web/remember": "^1.1.0", "@epic-web/remember": "^1.1.0",
"@faker-js/faker": "^10.1.0",
"@hono/node-server": "^1.13.7", "@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
@ -104,4 +105,4 @@
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"version": "1.13.1" "version": "1.13.1"
} }

17
package-lock.json generated
View File

@ -113,6 +113,7 @@
"@documenso/trpc": "*", "@documenso/trpc": "*",
"@documenso/ui": "*", "@documenso/ui": "*",
"@epic-web/remember": "^1.1.0", "@epic-web/remember": "^1.1.0",
"@faker-js/faker": "^10.1.0",
"@hono/node-server": "^1.13.7", "@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
@ -3512,6 +3513,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@faker-js/faker": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
"npm": ">=10"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",

View File

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

View File

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

View File

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

View File

@ -16,6 +16,10 @@ type FileData =
status: 'loaded'; status: 'loaded';
}; };
type EnvelopeRenderOverrideSettings = {
mode: 'edit' | 'sign' | 'export';
};
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number]; type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
type EnvelopeRenderProviderValue = { type EnvelopeRenderProviderValue = {
@ -28,10 +32,12 @@ type EnvelopeRenderProviderValue = {
renderError: boolean; renderError: boolean;
setRenderError: (renderError: boolean) => void; setRenderError: (renderError: boolean) => void;
overrideSettings?: EnvelopeRenderOverrideSettings;
}; };
interface EnvelopeRenderProviderProps { interface EnvelopeRenderProviderProps {
children: React.ReactNode; children: React.ReactNode;
envelope: Pick<TEnvelope, 'envelopeItems'>; envelope: Pick<TEnvelope, 'envelopeItems'>;
/** /**
@ -54,6 +60,11 @@ interface EnvelopeRenderProviderProps {
* If not provided, it will be assumed that the current user can access the document. * If not provided, it will be assumed that the current user can access the document.
*/ */
token: string | undefined; token: string | undefined;
/**
* Custom override settings for generic page renderers.
*/
overrideSettings?: EnvelopeRenderOverrideSettings;
} }
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null); const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
@ -77,6 +88,7 @@ export const EnvelopeRenderProvider = ({
fields, fields,
token, token,
recipientIds = [], recipientIds = [],
overrideSettings,
}: EnvelopeRenderProviderProps) => { }: EnvelopeRenderProviderProps) => {
// Indexed by documentDataId. // Indexed by documentDataId.
const [files, setFiles] = useState<Record<string, FileData>>({}); const [files, setFiles] = useState<Record<string, FileData>>({});
@ -185,6 +197,7 @@ export const EnvelopeRenderProvider = ({
getRecipientColorKey, getRecipientColorKey,
renderError, renderError,
setRenderError, setRenderError,
overrideSettings,
}} }}
> >
{children} {children}

View File

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