mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +10:00
Compare commits
12 Commits
fix/envelo
...
a08a77e98b
| Author | SHA1 | Date | |
|---|---|---|---|
| a08a77e98b | |||
| 13d9ca7a0e | |||
| d25565b7d0 | |||
| 91421a7d62 | |||
| a9f1e39b10 | |||
| b37748654e | |||
| b3ed80d721 | |||
| b3cb750470 | |||
| 1e52493144 | |||
| ab95e80987 | |||
| 1780a5c262 | |||
| cb9bf407f7 |
@ -152,18 +152,6 @@ export const EditorFieldTextForm = ({
|
|||||||
className="h-auto"
|
className="h-auto"
|
||||||
placeholder={t`Add text to the field`}
|
placeholder={t`Add text to the field`}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
|
||||||
const values = form.getValues();
|
|
||||||
const characterLimit = values.characterLimit || 0;
|
|
||||||
let textValue = e.target.value;
|
|
||||||
|
|
||||||
if (characterLimit > 0 && textValue.length > characterLimit) {
|
|
||||||
textValue = textValue.slice(0, characterLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.target.value = textValue;
|
|
||||||
field.onChange(e);
|
|
||||||
}}
|
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -187,18 +175,6 @@ export const EditorFieldTextForm = ({
|
|||||||
className="bg-background"
|
className="bg-background"
|
||||||
placeholder={t`Field character limit`}
|
placeholder={t`Field character limit`}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
|
||||||
field.onChange(e);
|
|
||||||
|
|
||||||
const values = form.getValues();
|
|
||||||
const characterLimit = parseInt(e.target.value, 10) || 0;
|
|
||||||
|
|
||||||
const textValue = values.text || '';
|
|
||||||
|
|
||||||
if (characterLimit > 0 && textValue.length > characterLimit) {
|
|
||||||
form.setValue('text', textValue.slice(0, characterLimit));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { prop, sortBy } from 'remeda';
|
|||||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
|
import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
|
||||||
import {
|
import {
|
||||||
isFieldUnsignedAndRequired,
|
isFieldUnsignedAndRequired,
|
||||||
isRequiredField,
|
isRequiredField,
|
||||||
@ -52,11 +51,7 @@ export type EnvelopeSigningContextValue = {
|
|||||||
setSelectedAssistantRecipientId: (_value: number | null) => void;
|
setSelectedAssistantRecipientId: (_value: number | null) => void;
|
||||||
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
|
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
|
||||||
|
|
||||||
signField: (
|
signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise<void>;
|
||||||
_fieldId: number,
|
|
||||||
_value: TSignEnvelopeFieldValue,
|
|
||||||
authOptions?: TRecipientActionAuth,
|
|
||||||
) => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
|
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
|
||||||
@ -289,11 +284,7 @@ export const EnvelopeSigningProvider = ({
|
|||||||
: null;
|
: null;
|
||||||
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
|
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
|
||||||
|
|
||||||
const signField = async (
|
const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => {
|
||||||
fieldId: number,
|
|
||||||
fieldValue: TSignEnvelopeFieldValue,
|
|
||||||
authOptions?: TRecipientActionAuth,
|
|
||||||
) => {
|
|
||||||
// Set the field locally for direct templates.
|
// Set the field locally for direct templates.
|
||||||
if (isDirectTemplate) {
|
if (isDirectTemplate) {
|
||||||
handleDirectTemplateFieldInsertion(fieldId, fieldValue);
|
handleDirectTemplateFieldInsertion(fieldId, fieldValue);
|
||||||
@ -304,7 +295,7 @@ export const EnvelopeSigningProvider = ({
|
|||||||
token: envelopeData.recipient.token,
|
token: envelopeData.recipient.token,
|
||||||
fieldId,
|
fieldId,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
authOptions,
|
authOptions: undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -103,6 +103,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
fieldUpdates.height = fieldPageHeight;
|
fieldUpdates.height = fieldPageHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todo: envelopes Use id
|
||||||
editorFields.updateFieldByFormId(fieldFormId, fieldUpdates);
|
editorFields.updateFieldByFormId(fieldFormId, fieldUpdates);
|
||||||
|
|
||||||
// Select the field if it is not already selected.
|
// Select the field if it is not already selected.
|
||||||
|
|||||||
@ -27,8 +27,7 @@ import type {
|
|||||||
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
||||||
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 } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
|
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
|
||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
@ -113,29 +112,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||||
|
|
||||||
{/* Document View */}
|
{/* Document View */}
|
||||||
<div className="mt-4 flex flex-col items-center justify-center">
|
<div className="mt-4 flex h-full justify-center p-4">
|
||||||
{envelope.recipients.length === 0 && (
|
|
||||||
<Alert
|
|
||||||
variant="neutral"
|
|
||||||
className="border-border bg-background mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<AlertTitle>
|
|
||||||
<Trans>Missing Recipients</Trans>
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans>You need at least one recipient to add fields</Trans>
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link to={`${relativePath.editorPath}`}>
|
|
||||||
<Trans>Add Recipients</Trans>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentEnvelopeItem !== null ? (
|
{currentEnvelopeItem !== null ? (
|
||||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
|
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
|
||||||
) : (
|
) : (
|
||||||
@ -153,7 +130,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Section - Form Fields Panel */}
|
{/* Right Section - Form Fields Panel */}
|
||||||
{currentEnvelopeItem && envelope.recipients.length > 0 && (
|
{currentEnvelopeItem && (
|
||||||
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
|
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
|
||||||
{/* Recipient selector section. */}
|
{/* Recipient selector section. */}
|
||||||
<section className="px-4">
|
<section className="px-4">
|
||||||
@ -161,6 +138,19 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
<Trans>Selected Recipient</Trans>
|
<Trans>Selected Recipient</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{envelope.recipients.length === 0 ? (
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription className="flex flex-col gap-2">
|
||||||
|
<Trans>You need at least one recipient to add fields</Trans>
|
||||||
|
|
||||||
|
<Link to={`${relativePath.editorPath}`} className="text-sm">
|
||||||
|
<p>
|
||||||
|
<Trans>Click here to add a recipient</Trans>
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
<RecipientSelector
|
<RecipientSelector
|
||||||
selectedRecipient={editorFields.selectedRecipient}
|
selectedRecipient={editorFields.selectedRecipient}
|
||||||
onSelectedRecipientChange={(recipient) =>
|
onSelectedRecipientChange={(recipient) =>
|
||||||
@ -170,6 +160,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
align="end"
|
align="end"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{editorFields.selectedRecipient &&
|
{editorFields.selectedRecipient &&
|
||||||
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (
|
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (
|
||||||
|
|||||||
@ -323,7 +323,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
|
|
||||||
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
|
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
|
||||||
{/* Sidebar. */}
|
{/* Sidebar. */}
|
||||||
<div className="bg-accent/20 flex w-80 flex-col border-r">
|
<div className="flex w-80 flex-col border-r bg-gray-50">
|
||||||
<DialogHeader className="p-6 pb-4">
|
<DialogHeader className="p-6 pb-4">
|
||||||
<DialogTitle>Document Settings</DialogTitle>
|
<DialogTitle>Document Settings</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@ -203,6 +203,7 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
debouncedUpdateEnvelopeItems(items);
|
debouncedUpdateEnvelopeItems(items);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Todo: Envelopes - Sync into envelopes data
|
||||||
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
|
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
|
||||||
void updateEnvelopeItems({
|
void updateEnvelopeItems({
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
|
|||||||
@ -10,17 +10,14 @@ import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-rende
|
|||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
|
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
|
||||||
import { ZFullFieldSchema } from '@documenso/lib/types/field';
|
import { ZFullFieldSchema } from '@documenso/lib/types/field';
|
||||||
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
|
||||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
|
||||||
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
|
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
|
||||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
|
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
|
||||||
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
|
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
|
||||||
@ -31,24 +28,20 @@ import { handleNumberFieldClick } from '~/utils/field-signing/number-field';
|
|||||||
import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field';
|
import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field';
|
||||||
import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
|
|
||||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||||
|
|
||||||
export default function EnvelopeSignerPageRenderer() {
|
export default function EnvelopeSignerPageRenderer() {
|
||||||
const { t, i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
const { sessionData } = useOptionalSession();
|
const { sessionData } = useOptionalSession();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
envelopeData,
|
envelopeData,
|
||||||
recipient,
|
recipient,
|
||||||
recipientFields,
|
recipientFields,
|
||||||
recipientFieldsRemaining,
|
recipientFieldsRemaining,
|
||||||
showPendingFieldTooltip,
|
showPendingFieldTooltip,
|
||||||
signField: signFieldInternal,
|
signField,
|
||||||
email,
|
email,
|
||||||
setEmail,
|
setEmail,
|
||||||
fullName,
|
fullName,
|
||||||
@ -325,6 +318,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
* SIGNATURE FIELD.
|
* SIGNATURE FIELD.
|
||||||
*/
|
*/
|
||||||
.with({ type: FieldType.SIGNATURE }, (field) => {
|
.with({ type: FieldType.SIGNATURE }, (field) => {
|
||||||
|
// Todo: Envelopes - Reauth
|
||||||
handleSignatureFieldClick({
|
handleSignatureFieldClick({
|
||||||
field,
|
field,
|
||||||
signature,
|
signature,
|
||||||
@ -335,21 +329,11 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
.then(async (payload) => {
|
.then(async (payload) => {
|
||||||
if (payload) {
|
if (payload) {
|
||||||
fieldGroup.add(loadingSpinnerGroup);
|
fieldGroup.add(loadingSpinnerGroup);
|
||||||
|
|
||||||
if (payload.value) {
|
|
||||||
void executeActionAuthProcedure({
|
|
||||||
onReauthFormSubmit: async (authOptions) => {
|
|
||||||
await signField(field.id, payload, authOptions);
|
|
||||||
|
|
||||||
loadingSpinnerGroup.destroy();
|
|
||||||
},
|
|
||||||
actionTarget: field.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSignature(payload.value);
|
|
||||||
} else {
|
|
||||||
await signField(field.id, payload);
|
await signField(field.id, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (payload?.value) {
|
||||||
|
setSignature(payload.value);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -363,26 +347,6 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
fieldGroup.on('pointerdown', handleFieldGroupClick);
|
fieldGroup.on('pointerdown', handleFieldGroupClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
const signField = async (
|
|
||||||
fieldId: number,
|
|
||||||
payload: TSignEnvelopeFieldValue,
|
|
||||||
authOptions?: TRecipientActionAuth,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await signFieldInternal(fieldId, payload, authOptions);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: t`Error`,
|
|
||||||
description: t`An error occurred while signing the field.`,
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Konva page canvas and all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@ -19,6 +19,7 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
|
"pdf2json": "^4.0.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
@ -27198,6 +27199,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pdf2json": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdf2json/-/pdf2json-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-WkezNsLK8sGpuFC7+PPP0DsXROwdoOxmXPBTtUWWkCwCi/Vi97MRC52Ly6FWIJjOKIywpm/L2oaUgSrmtU+7ZQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"pdf2json": "bin/pdf2json.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pdfjs-dist": {
|
"node_modules/pdfjs-dist": {
|
||||||
"version": "3.11.174",
|
"version": "3.11.174",
|
||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
|
||||||
|
|||||||
@ -74,6 +74,7 @@
|
|||||||
"inngest-cli": "^0.29.1",
|
"inngest-cli": "^0.29.1",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
|
"pdf2json": "^4.0.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
|
const PLACEHOLDER_PDF_PATH = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../assets/project-proposal-single-recipient.pdf',
|
||||||
|
);
|
||||||
|
test.describe('PDF Placeholders with single recipient', () => {
|
||||||
|
test('[AUTO_PLACING_FIELDS]: should automatically create recipients from PDF placeholders', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||||
|
await fileInput.waitFor({ state: 'attached' });
|
||||||
|
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||||
|
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
await expect(page.getByPlaceholder('Email')).toHaveValue('recipient.1@documenso.com');
|
||||||
|
await expect(page.getByPlaceholder('Name')).toHaveValue('Recipient 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[AUTO_PLACING_FIELDS]: should automatically place fields from PDF placeholders', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||||
|
await fileInput.waitFor({ state: 'attached' });
|
||||||
|
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||||
|
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.locator('[data-field-type="SIGNATURE"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-field-type="EMAIL"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-field-type="NAME"]')).toBeVisible();
|
||||||
|
await expect(page.locator('[data-field-type="TEXT"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[AUTO_PLACING_FIELDS]: should automatically configure fields from PDF placeholders', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/documents`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInput = page.locator('input[type="file"]').nth(1);
|
||||||
|
await fileInput.waitFor({ state: 'attached' });
|
||||||
|
await fileInput.setInputFiles(PLACEHOLDER_PDF_PATH);
|
||||||
|
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await page.getByText('Text').nth(1).click();
|
||||||
|
await page.getByRole('button', { name: 'Advanced settings' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Advanced settings' })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('div')
|
||||||
|
.filter({ hasText: /^Required field$/ })
|
||||||
|
.getByRole('switch'),
|
||||||
|
).toBeChecked();
|
||||||
|
|
||||||
|
await expect(page.getByRole('combobox')).toHaveText('Right');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -78,6 +78,7 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Todo: Envelopes
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const completedDocumentData = await getFile(firstDocumentData);
|
||||||
@ -168,6 +169,7 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Todo: Envelopes
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const completedDocumentData = await getFile(firstDocumentData);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { insertFieldsFromPlaceholdersInPDF } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
||||||
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
@ -233,7 +234,7 @@ export const createEnvelope = async ({
|
|||||||
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
? await incrementDocumentId().then((v) => v.formattedDocumentId)
|
||||||
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
: await incrementTemplateId().then((v) => v.formattedTemplateId);
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
const createdEnvelope = await prisma.$transaction(async (tx) => {
|
||||||
const envelope = await tx.envelope.create({
|
const envelope = await tx.envelope.create({
|
||||||
data: {
|
data: {
|
||||||
id: prefixedId('envelope'),
|
id: prefixedId('envelope'),
|
||||||
@ -353,8 +354,12 @@ export const createEnvelope = async ({
|
|||||||
recipients: true,
|
recipients: true,
|
||||||
fields: true,
|
fields: true,
|
||||||
folder: true,
|
folder: true,
|
||||||
envelopeItems: true,
|
|
||||||
envelopeAttachments: true,
|
envelopeAttachments: true,
|
||||||
|
envelopeItems: {
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -390,4 +395,51 @@ export const createEnvelope = async ({
|
|||||||
|
|
||||||
return createdEnvelope;
|
return createdEnvelope;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const envelopeItem of createdEnvelope.envelopeItems) {
|
||||||
|
const buffer = await getFileServerSide(envelopeItem.documentData);
|
||||||
|
|
||||||
|
// Use normalized PDF if normalizePdf was true, otherwise use original
|
||||||
|
const pdfToProcess = normalizePdf
|
||||||
|
? await makeNormalizedPdf(Buffer.from(buffer))
|
||||||
|
: Buffer.from(buffer);
|
||||||
|
|
||||||
|
await insertFieldsFromPlaceholdersInPDF(
|
||||||
|
pdfToProcess,
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
{
|
||||||
|
type: 'envelopeId',
|
||||||
|
id: createdEnvelope.id,
|
||||||
|
},
|
||||||
|
requestMetadata,
|
||||||
|
envelopeItem.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEnvelope = await prisma.envelope.findFirst({
|
||||||
|
where: {
|
||||||
|
id: createdEnvelope.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
recipients: true,
|
||||||
|
fields: true,
|
||||||
|
folder: true,
|
||||||
|
envelopeAttachments: true,
|
||||||
|
envelopeItems: {
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!finalEnvelope) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Envelope not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalEnvelope;
|
||||||
};
|
};
|
||||||
|
|||||||
517
packages/lib/server-only/pdf/auto-place-fields.ts
Normal file
517
packages/lib/server-only/pdf/auto-place-fields.ts
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
import { EnvelopeType, FieldType, RecipientRole } from '@prisma/client';
|
||||||
|
import PDFParser from 'pdf2json';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||||
|
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
|
||||||
|
import { createDocumentRecipients } from '@documenso/lib/server-only/recipient/create-document-recipients';
|
||||||
|
import { createTemplateRecipients } from '@documenso/lib/server-only/recipient/create-template-recipients';
|
||||||
|
import { type TFieldAndMeta, ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import type { EnvelopeIdOptions } from '@documenso/lib/utils/envelope';
|
||||||
|
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { getPageSize } from './get-page-size';
|
||||||
|
|
||||||
|
type TextPosition = {
|
||||||
|
text: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CharIndexMapping = {
|
||||||
|
textPositionIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PlaceholderInfo = {
|
||||||
|
placeholder: string;
|
||||||
|
recipient: string;
|
||||||
|
fieldAndMeta: TFieldAndMeta;
|
||||||
|
page: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
pageWidth: number;
|
||||||
|
pageHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FieldToCreate = TFieldAndMeta & {
|
||||||
|
envelopeItemId?: string;
|
||||||
|
recipientId: number;
|
||||||
|
pageNumber: number;
|
||||||
|
pageX: number;
|
||||||
|
pageY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RecipientPlaceholderInfo = {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
recipientIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Questions for later:
|
||||||
|
- Does it handle multi-page PDFs? ✅ YES! ✅
|
||||||
|
- Does it handle multiple recipients on the same page? ✅ YES! ✅
|
||||||
|
- Does it handle multiple recipients on multiple pages? ✅ YES! ✅
|
||||||
|
- What happens with incorrect placeholders? E.g. those containing non-accepted properties.
|
||||||
|
- The placeholder data is dynamic. How to handle this parsing? Perhaps we need to do it similar to the fieldMeta parsing. ✅
|
||||||
|
- Need to handle envelopes with multiple items. ✅
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse field type string to FieldType enum.
|
||||||
|
Normalizes the input (uppercase, trim) and validates it's a valid field type.
|
||||||
|
This ensures we handle case variations and whitespace, and provides clear error messages.
|
||||||
|
*/
|
||||||
|
const parseFieldType = (fieldTypeString: string): FieldType => {
|
||||||
|
const normalizedType = fieldTypeString.toUpperCase().trim();
|
||||||
|
|
||||||
|
return match(normalizedType)
|
||||||
|
.with('SIGNATURE', () => FieldType.SIGNATURE)
|
||||||
|
.with('FREE_SIGNATURE', () => FieldType.FREE_SIGNATURE)
|
||||||
|
.with('INITIALS', () => FieldType.INITIALS)
|
||||||
|
.with('NAME', () => FieldType.NAME)
|
||||||
|
.with('EMAIL', () => FieldType.EMAIL)
|
||||||
|
.with('DATE', () => FieldType.DATE)
|
||||||
|
.with('TEXT', () => FieldType.TEXT)
|
||||||
|
.with('NUMBER', () => FieldType.NUMBER)
|
||||||
|
.with('RADIO', () => FieldType.RADIO)
|
||||||
|
.with('CHECKBOX', () => FieldType.CHECKBOX)
|
||||||
|
.with('DROPDOWN', () => FieldType.DROPDOWN)
|
||||||
|
.otherwise(() => {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: `Invalid field type: ${fieldTypeString}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Transform raw field metadata from placeholder format to schema format.
|
||||||
|
Users should provide properly capitalized property names (e.g., readOnly, fontSize, textAlign).
|
||||||
|
Converts string values to proper types (booleans, numbers).
|
||||||
|
*/
|
||||||
|
const parseFieldMeta = (
|
||||||
|
rawFieldMeta: Record<string, string>,
|
||||||
|
fieldType: FieldType,
|
||||||
|
): Record<string, unknown> | undefined => {
|
||||||
|
if (fieldType === FieldType.SIGNATURE || fieldType === FieldType.FREE_SIGNATURE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(rawFieldMeta).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldTypeString = String(fieldType).toLowerCase();
|
||||||
|
|
||||||
|
const parsedFieldMeta: Record<string, boolean | number | string> = {
|
||||||
|
type: fieldTypeString,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
rawFieldMeta is an object with string keys and string values.
|
||||||
|
It contains string values because the PDF parser returns the values as strings.
|
||||||
|
|
||||||
|
E.g. { 'required': 'true', 'fontSize': '12', 'maxValue': '100', 'minValue': '0', 'characterLimit': '100' }
|
||||||
|
*/
|
||||||
|
const rawFieldMetaEntries = Object.entries(rawFieldMeta);
|
||||||
|
|
||||||
|
for (const [property, value] of rawFieldMetaEntries) {
|
||||||
|
if (property === 'readOnly' || property === 'required') {
|
||||||
|
parsedFieldMeta[property] = value === 'true';
|
||||||
|
} else if (
|
||||||
|
property === 'fontSize' ||
|
||||||
|
property === 'maxValue' ||
|
||||||
|
property === 'minValue' ||
|
||||||
|
property === 'characterLimit'
|
||||||
|
) {
|
||||||
|
const numValue = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isNaN(numValue)) {
|
||||||
|
parsedFieldMeta[property] = numValue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedFieldMeta[property] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedFieldMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<PlaceholderInfo[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parser = new PDFParser(null, true);
|
||||||
|
|
||||||
|
parser.on('pdfParser_dataError', (errData) => {
|
||||||
|
reject(errData);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('pdfParser_dataReady', (pdfData) => {
|
||||||
|
const placeholders: PlaceholderInfo[] = [];
|
||||||
|
|
||||||
|
pdfData.Pages.forEach((page, pageIndex) => {
|
||||||
|
/*
|
||||||
|
pdf2json returns the PDF page content as an array of characters.
|
||||||
|
We need to concatenate the characters to get the full text.
|
||||||
|
We also need to get the position of the text so we can place the placeholders in the correct position.
|
||||||
|
|
||||||
|
Page dimensions from PDF2JSON are in "page units" (relative coordinates)
|
||||||
|
*/
|
||||||
|
let pageText = '';
|
||||||
|
const textPositions: TextPosition[] = [];
|
||||||
|
const charIndexMappings: CharIndexMapping[] = [];
|
||||||
|
|
||||||
|
page.Texts.forEach((text) => {
|
||||||
|
/*
|
||||||
|
R is an array of objects containing each character, its position and styling information.
|
||||||
|
The decodedText stores the characters, without any other information.
|
||||||
|
|
||||||
|
textPositions stores each character and its position on the page.
|
||||||
|
*/
|
||||||
|
const decodedText = text.R.map((run) => decodeURIComponent(run.T)).join('');
|
||||||
|
|
||||||
|
/*
|
||||||
|
For each character in the decodedText, we store its position in the textPositions array.
|
||||||
|
This allows us to quickly find the position of a character in the textPositions array by its index.
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < decodedText.length; i++) {
|
||||||
|
charIndexMappings.push({
|
||||||
|
textPositionIndex: textPositions.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pageText += decodedText;
|
||||||
|
|
||||||
|
textPositions.push({
|
||||||
|
text: decodedText,
|
||||||
|
x: text.x,
|
||||||
|
y: text.y,
|
||||||
|
w: text.w || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderMatches = pageText.matchAll(/{{([^}]+)}}/g);
|
||||||
|
|
||||||
|
/*
|
||||||
|
A placeholder match has the following format:
|
||||||
|
|
||||||
|
[
|
||||||
|
'{{fieldType,recipient,fieldMeta}}',
|
||||||
|
'fieldType,recipient,fieldMeta',
|
||||||
|
'index: <number>',
|
||||||
|
'input: <pdf-text>'
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
for (const placeholderMatch of placeholderMatches) {
|
||||||
|
const placeholder = placeholderMatch[0];
|
||||||
|
const placeholderData = placeholderMatch[1].split(',').map((property) => property.trim());
|
||||||
|
|
||||||
|
const [fieldTypeString, recipient, ...fieldMetaData] = placeholderData;
|
||||||
|
|
||||||
|
const rawFieldMeta = Object.fromEntries(
|
||||||
|
fieldMetaData.map((property) => property.split('=')),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldType = parseFieldType(fieldTypeString);
|
||||||
|
const parsedFieldMeta = parseFieldMeta(rawFieldMeta, fieldType);
|
||||||
|
|
||||||
|
const fieldAndMeta: TFieldAndMeta = ZFieldAndMetaSchema.parse({
|
||||||
|
type: fieldType,
|
||||||
|
fieldMeta: parsedFieldMeta,
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Find the position of where the placeholder starts and ends in the text.
|
||||||
|
|
||||||
|
Then find the position of the characters in the textPositions array.
|
||||||
|
This allows us to quickly find the position of a character in the textPositions array by its index.
|
||||||
|
*/
|
||||||
|
if (placeholderMatch.index === undefined) {
|
||||||
|
console.error('Placeholder match index is undefined for placeholder', placeholder);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderEndCharIndex = placeholderMatch.index + placeholder.length;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the index of the placeholder's first and last character in the textPositions array.
|
||||||
|
Used to retrieve the character information from the textPositions array.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
startTextPosIndex - 1
|
||||||
|
endTextPosIndex - 40
|
||||||
|
*/
|
||||||
|
const startTextPosIndex = charIndexMappings[placeholderMatch.index].textPositionIndex;
|
||||||
|
const endTextPosIndex = charIndexMappings[placeholderEndCharIndex - 1].textPositionIndex;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the placeholder's first and last character information from the textPositions array.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
placeholderStart = { text: '{', x: 100, y: 100, w: 100 }
|
||||||
|
placeholderEnd = { text: '}', x: 200, y: 100, w: 100 }
|
||||||
|
*/
|
||||||
|
const placeholderStart = textPositions[startTextPosIndex];
|
||||||
|
const placeholderEnd = textPositions[endTextPosIndex];
|
||||||
|
|
||||||
|
const width = placeholderEnd.x + placeholderEnd.w * 0.1 - placeholderStart.x;
|
||||||
|
|
||||||
|
placeholders.push({
|
||||||
|
placeholder,
|
||||||
|
recipient,
|
||||||
|
fieldAndMeta,
|
||||||
|
page: pageIndex + 1,
|
||||||
|
x: placeholderStart.x,
|
||||||
|
y: placeholderStart.y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
pageWidth: page.Width,
|
||||||
|
pageHeight: page.Height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(placeholders);
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.parseBuffer(pdf);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const replacePlaceholdersInPDF = async (pdf: Buffer): Promise<Buffer> => {
|
||||||
|
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFDocument.load(new Uint8Array(pdf));
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
for (const placeholder of placeholders) {
|
||||||
|
const pageIndex = placeholder.page - 1;
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
|
||||||
|
const { width: pdfLibPageWidth, height: pdfLibPageHeight } = getPageSize(page);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert PDF2JSON coordinates to pdf-lib coordinates:
|
||||||
|
|
||||||
|
PDF2JSON uses relative "page units":
|
||||||
|
- x, y, width, height are in page units
|
||||||
|
- Page dimensions (Width, Height) are also in page units
|
||||||
|
|
||||||
|
pdf-lib uses absolute points (1 point = 1/72 inch):
|
||||||
|
- Need to convert from page units to points
|
||||||
|
- Y-axis in pdf-lib is bottom-up (origin at bottom-left)
|
||||||
|
- Y-axis in PDF2JSON is top-down (origin at top-left)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const xPoints = (placeholder.x / placeholder.pageWidth) * pdfLibPageWidth;
|
||||||
|
const yPoints = pdfLibPageHeight - (placeholder.y / placeholder.pageHeight) * pdfLibPageHeight;
|
||||||
|
const widthPoints = (placeholder.width / placeholder.pageWidth) * pdfLibPageWidth;
|
||||||
|
const heightPoints = (placeholder.height / placeholder.pageHeight) * pdfLibPageHeight;
|
||||||
|
|
||||||
|
page.drawRectangle({
|
||||||
|
x: xPoints,
|
||||||
|
y: yPoints - heightPoints, // Adjust for height since y is at baseline
|
||||||
|
width: widthPoints,
|
||||||
|
height: heightPoints,
|
||||||
|
color: rgb(1, 1, 1),
|
||||||
|
borderColor: rgb(1, 1, 1),
|
||||||
|
borderWidth: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedPdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
return Buffer.from(modifiedPdfBytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractRecipientPlaceholder = (placeholder: string): RecipientPlaceholderInfo => {
|
||||||
|
const indexMatch = placeholder.match(/^r(\d+)$/i);
|
||||||
|
|
||||||
|
if (!indexMatch) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: `Invalid recipient placeholder format: ${placeholder}. Expected format: r1, r2, r3, etc.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientIndex = Number(indexMatch[1]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
email: `recipient.${recipientIndex}@documenso.com`,
|
||||||
|
name: `Recipient ${recipientIndex}`,
|
||||||
|
recipientIndex,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertFieldsFromPlaceholdersInPDF = async (
|
||||||
|
pdf: Buffer,
|
||||||
|
userId: number,
|
||||||
|
teamId: number,
|
||||||
|
envelopeId: EnvelopeIdOptions,
|
||||||
|
requestMetadata: ApiRequestMetadata,
|
||||||
|
envelopeItemId?: string,
|
||||||
|
): Promise<Buffer> => {
|
||||||
|
const placeholders = await extractPlaceholdersFromPDF(pdf);
|
||||||
|
|
||||||
|
if (placeholders.length === 0) {
|
||||||
|
return pdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
A structure that maps the recipient index to the recipient name.
|
||||||
|
Example: 1 => 'Recipient 1'
|
||||||
|
*/
|
||||||
|
const recipientPlaceholders = new Map<number, string>();
|
||||||
|
|
||||||
|
for (const placeholder of placeholders) {
|
||||||
|
const { name, recipientIndex } = extractRecipientPlaceholder(placeholder.recipient);
|
||||||
|
|
||||||
|
recipientPlaceholders.set(recipientIndex, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a list of recipients to create.
|
||||||
|
Example: [{ email: 'recipient.1@documenso.com', name: 'Recipient 1', role: 'SIGNER', signingOrder: 1 }]
|
||||||
|
*/
|
||||||
|
const recipientsToCreate = Array.from(
|
||||||
|
recipientPlaceholders.entries(),
|
||||||
|
([recipientIndex, name]) => {
|
||||||
|
return {
|
||||||
|
email: `recipient.${recipientIndex}@documenso.com`,
|
||||||
|
name,
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
signingOrder: recipientIndex,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||||
|
id: envelopeId,
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
type: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findFirst({
|
||||||
|
where: envelopeWhereInput,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
secondaryId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelope) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Envelope not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRecipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingEmails = new Set(existingRecipients.map((r) => r.email));
|
||||||
|
const recipientsToCreateFiltered = recipientsToCreate.filter(
|
||||||
|
(recipient) => !existingEmails.has(recipient.email),
|
||||||
|
);
|
||||||
|
|
||||||
|
let createdRecipients: Pick<Recipient, 'id' | 'email'>[] = existingRecipients;
|
||||||
|
|
||||||
|
if (recipientsToCreateFiltered.length > 0) {
|
||||||
|
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||||
|
const { recipients } = await createDocumentRecipients({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
id: envelopeId,
|
||||||
|
recipients: recipientsToCreateFiltered,
|
||||||
|
requestMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
createdRecipients = [...existingRecipients, ...recipients];
|
||||||
|
} else if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||||
|
const templateId =
|
||||||
|
envelopeId.type === 'templateId'
|
||||||
|
? envelopeId.id
|
||||||
|
: mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||||
|
|
||||||
|
const { recipients } = await createTemplateRecipients({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
templateId,
|
||||||
|
recipients: recipientsToCreateFiltered,
|
||||||
|
});
|
||||||
|
|
||||||
|
createdRecipients = [...existingRecipients, ...recipients];
|
||||||
|
} else {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: `Invalid envelope type: ${envelope.type}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsToCreate: FieldToCreate[] = [];
|
||||||
|
|
||||||
|
for (const placeholder of placeholders) {
|
||||||
|
/*
|
||||||
|
Convert PDF2JSON coordinates to percentage-based coordinates (0-100)
|
||||||
|
The UI expects positionX and positionY as percentages, not absolute points
|
||||||
|
PDF2JSON uses relative coordinates: x/pageWidth and y/pageHeight give us the percentage
|
||||||
|
*/
|
||||||
|
const xPercent = (placeholder.x / placeholder.pageWidth) * 100;
|
||||||
|
const yPercent = (placeholder.y / placeholder.pageHeight) * 100;
|
||||||
|
|
||||||
|
const widthPercent = (placeholder.width / placeholder.pageWidth) * 100;
|
||||||
|
const heightPercent = (placeholder.height / placeholder.pageHeight) * 100;
|
||||||
|
|
||||||
|
const { email } = extractRecipientPlaceholder(placeholder.recipient);
|
||||||
|
const recipient = createdRecipients.find((r) => r.email === email);
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: `Could not find recipient ID for placeholder: ${placeholder.placeholder}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientId = recipient.id;
|
||||||
|
|
||||||
|
// Default height percentage if too small (use 2% as a reasonable default)
|
||||||
|
const finalHeightPercent = heightPercent > 0.01 ? heightPercent : 2;
|
||||||
|
|
||||||
|
fieldsToCreate.push({
|
||||||
|
...placeholder.fieldAndMeta,
|
||||||
|
envelopeItemId,
|
||||||
|
recipientId,
|
||||||
|
pageNumber: placeholder.page,
|
||||||
|
pageX: xPercent,
|
||||||
|
pageY: yPercent,
|
||||||
|
width: widthPercent,
|
||||||
|
height: finalHeightPercent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createEnvelopeFields({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
id: envelopeId,
|
||||||
|
fields: fieldsToCreate,
|
||||||
|
requestMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pdf;
|
||||||
|
};
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||||
|
|
||||||
|
import { replacePlaceholdersInPDF } from './auto-place-fields';
|
||||||
import { flattenAnnotations } from './flatten-annotations';
|
import { flattenAnnotations } from './flatten-annotations';
|
||||||
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ export const normalizePdf = async (pdf: Buffer) => {
|
|||||||
removeOptionalContentGroups(pdfDoc);
|
removeOptionalContentGroups(pdfDoc);
|
||||||
await flattenForm(pdfDoc);
|
await flattenForm(pdfDoc);
|
||||||
flattenAnnotations(pdfDoc);
|
flattenAnnotations(pdfDoc);
|
||||||
|
const pdfWithoutPlaceholders = await replacePlaceholdersInPDF(pdf);
|
||||||
|
|
||||||
return Buffer.from(await pdfDoc.save());
|
return pdfWithoutPlaceholders;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -62,15 +62,16 @@ export const renderCheckboxFieldElement = (
|
|||||||
const rectWidth = fieldRect.width() * groupScaleX;
|
const rectWidth = fieldRect.width() * groupScaleX;
|
||||||
const rectHeight = fieldRect.height() * groupScaleY;
|
const rectHeight = fieldRect.height() * groupScaleY;
|
||||||
|
|
||||||
|
// Todo: Envelopes - check sorting more than 10
|
||||||
|
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||||
|
|
||||||
const squares = fieldGroup
|
const squares = fieldGroup
|
||||||
.find('.checkbox-square')
|
.find('.checkbox-square')
|
||||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||||
const checkmarks = fieldGroup
|
const checkmarks = fieldGroup
|
||||||
.find('.checkbox-checkmark')
|
.find('.checkbox-checkmark')
|
||||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||||
const text = fieldGroup
|
const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
|
||||||
.find('.checkbox-text')
|
|
||||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
|
||||||
|
|
||||||
const groupedItems = squares.map((square, i) => ({
|
const groupedItems = squares.map((square, i) => ({
|
||||||
squareElement: square,
|
squareElement: square,
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
|||||||
import type { TFieldMetaSchema } from '../../types/field-meta';
|
import type { TFieldMetaSchema } from '../../types/field-meta';
|
||||||
import { renderCheckboxFieldElement } from './render-checkbox-field';
|
import { renderCheckboxFieldElement } from './render-checkbox-field';
|
||||||
import { renderDropdownFieldElement } from './render-dropdown-field';
|
import { renderDropdownFieldElement } from './render-dropdown-field';
|
||||||
import { renderGenericTextFieldElement } from './render-generic-text-field';
|
|
||||||
import { renderRadioFieldElement } from './render-radio-field';
|
import { renderRadioFieldElement } from './render-radio-field';
|
||||||
import { renderSignatureFieldElement } from './render-signature-field';
|
import { renderSignatureFieldElement } from './render-signature-field';
|
||||||
|
import { renderTextFieldElement } from './render-text-field';
|
||||||
|
|
||||||
export const MIN_FIELD_HEIGHT_PX = 12;
|
export const MIN_FIELD_HEIGHT_PX = 12;
|
||||||
export const MIN_FIELD_WIDTH_PX = 36;
|
export const MIN_FIELD_WIDTH_PX = 36;
|
||||||
@ -43,9 +43,9 @@ type RenderFieldOptions = {
|
|||||||
*
|
*
|
||||||
* @default 'edit'
|
* @default 'edit'
|
||||||
*
|
*
|
||||||
* - `edit` - The field is rendered in editor page.
|
* - `edit` - The field is rendered in edit mode.
|
||||||
* - `sign` - The field is rendered for the signing page.
|
* - `sign` - The field is rendered in sign mode. No interactive elements.
|
||||||
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
|
* - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
|
||||||
*/
|
*/
|
||||||
mode: 'edit' | 'sign' | 'export';
|
mode: 'edit' | 'sign' | 'export';
|
||||||
|
|
||||||
@ -76,21 +76,10 @@ export const renderField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return match(field.type)
|
return match(field.type)
|
||||||
.with(
|
.with(FieldType.TEXT, () => renderTextFieldElement(field, options))
|
||||||
FieldType.INITIALS,
|
|
||||||
FieldType.NAME,
|
|
||||||
FieldType.EMAIL,
|
|
||||||
FieldType.DATE,
|
|
||||||
FieldType.TEXT,
|
|
||||||
FieldType.NUMBER,
|
|
||||||
() => renderGenericTextFieldElement(field, options),
|
|
||||||
)
|
|
||||||
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
|
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
|
||||||
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
|
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
|
||||||
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
|
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
|
||||||
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
|
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
|
||||||
.with(FieldType.FREE_SIGNATURE, () => {
|
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
|
||||||
throw new Error('Free signature fields are not supported');
|
|
||||||
})
|
|
||||||
.exhaustive();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,8 +12,6 @@ import {
|
|||||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||||
import { calculateFieldPosition } from './field-renderer';
|
import { calculateFieldPosition } from './field-renderer';
|
||||||
|
|
||||||
const DEFAULT_TEXT_ALIGN = 'left';
|
|
||||||
|
|
||||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||||
|
|
||||||
@ -33,8 +31,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
|||||||
// Calculate text positioning based on alignment
|
// Calculate text positioning based on alignment
|
||||||
const textX = 0;
|
const textX = 0;
|
||||||
const textY = 0;
|
const textY = 0;
|
||||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
|
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
|
||||||
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
|
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
|
||||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||||
const textPadding = 10;
|
const textPadding = 10;
|
||||||
|
|
||||||
@ -42,33 +40,51 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
|||||||
|
|
||||||
// Handle edit mode.
|
// Handle edit mode.
|
||||||
if (mode === 'edit') {
|
if (mode === 'edit') {
|
||||||
if (textMeta?.text) {
|
|
||||||
textToRender = textMeta.text;
|
|
||||||
} else if (textMeta?.label) {
|
|
||||||
textToRender = textMeta.label;
|
|
||||||
} else {
|
|
||||||
// Show field name which is centered for the edit mode if no label/text is avaliable.
|
|
||||||
textToRender = fieldTypeName;
|
textToRender = fieldTypeName;
|
||||||
textAlign = 'center';
|
textAlign = 'center';
|
||||||
|
textVerticalAlign = 'middle';
|
||||||
|
|
||||||
|
if (textMeta?.label) {
|
||||||
|
textToRender = textMeta.label;
|
||||||
|
} else if (textMeta?.text) {
|
||||||
|
textToRender = textMeta.text;
|
||||||
|
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||||
|
|
||||||
|
// Todo: Envelopes - Handle this on signatures
|
||||||
|
if (textMeta.characterLimit) {
|
||||||
|
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle sign mode.
|
// Handle sign mode.
|
||||||
if (mode === 'sign' || mode === 'export') {
|
if (mode === 'sign' || mode === 'export') {
|
||||||
if (!field.inserted) {
|
|
||||||
if (textMeta?.text) {
|
|
||||||
textToRender = textMeta.text;
|
|
||||||
} else if (textMeta?.label) {
|
|
||||||
textToRender = textMeta.label;
|
|
||||||
} else if (mode === 'sign') {
|
|
||||||
// Only show the field name in sign mode if no text/label is avaliable.
|
|
||||||
textToRender = fieldTypeName;
|
textToRender = fieldTypeName;
|
||||||
textAlign = 'center';
|
textAlign = 'center';
|
||||||
|
textVerticalAlign = 'middle';
|
||||||
|
|
||||||
|
if (textMeta?.label) {
|
||||||
|
textToRender = textMeta.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textMeta?.text) {
|
||||||
|
textToRender = textMeta.text;
|
||||||
|
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||||
|
|
||||||
|
// Todo: Envelopes - Handle this on signatures
|
||||||
|
if (textMeta.characterLimit) {
|
||||||
|
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
textToRender = field.customText;
|
textToRender = field.customText;
|
||||||
|
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||||
|
|
||||||
|
// Todo: Envelopes - Handle this on signatures
|
||||||
|
if (textMeta?.characterLimit) {
|
||||||
|
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +106,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
|||||||
return fieldText;
|
return fieldText;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderGenericTextFieldElement = (
|
export const renderTextFieldElement = (
|
||||||
field: FieldToRender,
|
field: FieldToRender,
|
||||||
options: RenderFieldElementOptions,
|
options: RenderFieldElementOptions,
|
||||||
) => {
|
) => {
|
||||||
@ -133,49 +133,6 @@ export const signEnvelopeFieldRoute = procedure
|
|||||||
|
|
||||||
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
|
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
|
||||||
|
|
||||||
// Early return for uninserting fields.
|
|
||||||
if (!insertionValues.inserted) {
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
|
||||||
const updatedField = await tx.field.update({
|
|
||||||
where: {
|
|
||||||
id: field.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
customText: '',
|
|
||||||
inserted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.signature.deleteMany({
|
|
||||||
where: {
|
|
||||||
fieldId: field.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (recipient.role !== RecipientRole.ASSISTANT) {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
user: {
|
|
||||||
name: recipient.name,
|
|
||||||
email: recipient.email,
|
|
||||||
},
|
|
||||||
requestMetadata: metadata.requestMetadata,
|
|
||||||
data: {
|
|
||||||
field: field.type,
|
|
||||||
fieldId: field.secondaryId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
signedField: updatedField,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||||
documentAuthOptions: envelope.authOptions,
|
documentAuthOptions: envelope.authOptions,
|
||||||
recipient,
|
recipient,
|
||||||
|
|||||||
Reference in New Issue
Block a user