mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 01:45:08 +10:00
fix: render error messages for invalid templates
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { AlertTriangleIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export const DirectTemplateInvalidPageView = () => {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex h-[70vh] w-full max-w-md flex-col items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<AlertTriangleIcon className="h-10 w-10 text-destructive" />
|
||||||
|
|
||||||
|
<h1 className="mt-4 font-semibold text-3xl">
|
||||||
|
<Trans>Invalid direct link template</Trans>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-2 text-muted-foreground text-sm">
|
||||||
|
<Trans>
|
||||||
|
This direct link template cannot be used because one or more signers do not have a signature field assigned.
|
||||||
|
Please contact the sender to update the template.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -54,6 +54,7 @@ import { useCurrentTeam } from '~/providers/team';
|
|||||||
|
|
||||||
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
|
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
|
||||||
import { EnvelopeEditorFieldsPageRenderer } from './envelope-editor-fields-page-renderer';
|
import { EnvelopeEditorFieldsPageRenderer } from './envelope-editor-fields-page-renderer';
|
||||||
|
import { EnvelopeEditorInvalidDirectTemplateAlert } from './envelope-editor-invalid-direct-template-alert';
|
||||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||||
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
|
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
|
||||||
|
|
||||||
@@ -238,6 +239,8 @@ export const EnvelopeEditorFieldsPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<EnvelopeEditorInvalidDirectTemplateAlert />
|
||||||
|
|
||||||
{/* Document View */}
|
{/* Document View */}
|
||||||
<div className="mt-4 flex h-full flex-col items-center justify-center">
|
<div className="mt-4 flex h-full flex-col items-center justify-center">
|
||||||
{envelope.recipients.length === 0 && (
|
{envelope.recipients.length === 0 && (
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
|
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export type EnvelopeEditorInvalidDirectTemplateAlertProps = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warns that a direct link template cannot be used because one or more signers
|
||||||
|
* are missing a signature field.
|
||||||
|
*/
|
||||||
|
export const EnvelopeEditorInvalidDirectTemplateAlert = ({
|
||||||
|
className,
|
||||||
|
}: EnvelopeEditorInvalidDirectTemplateAlertProps) => {
|
||||||
|
const { envelope, isTemplate } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
|
const signersMissingSignatureFields = useMemo(() => {
|
||||||
|
if (!isTemplate || !envelope.directLink?.enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRecipientsWithMissingFields(envelope.recipients, envelope.fields);
|
||||||
|
}, [isTemplate, envelope.directLink, envelope.recipients, envelope.fields]);
|
||||||
|
|
||||||
|
if (signersMissingSignatureFields.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
variant="destructive"
|
||||||
|
className={cn('mx-auto w-full max-w-[800px] flex-row items-start gap-3 rounded-sm', className)}
|
||||||
|
>
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans>Invalid direct link template</Trans>
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
Recipients cannot use this direct link template because the following signers are missing a signature field
|
||||||
|
</Trans>
|
||||||
|
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{signersMissingSignatureFields.map((recipient, i) => (
|
||||||
|
<li key={i}>{recipient.email || recipient.name || `Recipient ${i + 1}`}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@ import { match } from 'ts-pattern';
|
|||||||
import { EnvelopeGenericPageRenderer } from '~/components/general/envelope-editor/envelope-generic-page-renderer';
|
import { EnvelopeGenericPageRenderer } from '~/components/general/envelope-editor/envelope-generic-page-renderer';
|
||||||
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
|
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
|
||||||
|
|
||||||
|
import { EnvelopeEditorInvalidDirectTemplateAlert } from './envelope-editor-invalid-direct-template-alert';
|
||||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||||
|
|
||||||
export const EnvelopeEditorPreviewPage = () => {
|
export const EnvelopeEditorPreviewPage = () => {
|
||||||
@@ -228,6 +229,8 @@ export const EnvelopeEditorPreviewPage = () => {
|
|||||||
{/* Horizontal envelope item selector */}
|
{/* Horizontal envelope item selector */}
|
||||||
<EnvelopeRendererFileSelector className="px-0" fields={editorFields.localFields} />
|
<EnvelopeRendererFileSelector className="px-0" fields={editorFields.localFields} />
|
||||||
|
|
||||||
|
<EnvelopeEditorInvalidDirectTemplateAlert className="mb-4" />
|
||||||
|
|
||||||
<Alert variant="warning" className="mx-auto max-w-[800px]">
|
<Alert variant="warning" className="mx-auto max-w-[800px]">
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans>Preview Mode</Trans>
|
<Trans>Preview Mode</Trans>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/
|
|||||||
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
||||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
|
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Plural } from '@lingui/react/macro';
|
import { Plural } from '@lingui/react/macro';
|
||||||
import { UsersIcon } from 'lucide-react';
|
import { UsersIcon } from 'lucide-react';
|
||||||
@@ -14,6 +15,7 @@ import { redirect } from 'react-router';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
||||||
|
import { DirectTemplateInvalidPageView } from '~/components/general/direct-template/direct-template-invalid-page';
|
||||||
import { DirectTemplatePageView } from '~/components/general/direct-template/direct-template-page';
|
import { DirectTemplatePageView } from '~/components/general/direct-template/direct-template-page';
|
||||||
import { DirectTemplateAuthPageView } from '~/components/general/direct-template/direct-template-signing-auth-page';
|
import { DirectTemplateAuthPageView } from '~/components/general/direct-template/direct-template-signing-auth-page';
|
||||||
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
||||||
@@ -70,8 +72,18 @@ const handleV1Loader = async ({ params, request }: Route.LoaderArgs) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsWithMissingFields = getRecipientsWithMissingFields(template.recipients, template.fields);
|
||||||
|
|
||||||
|
if (recipientsWithMissingFields.length > 0) {
|
||||||
|
return {
|
||||||
|
isAccessAuthValid: true,
|
||||||
|
isTemplateMissingSignatures: true,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAccessAuthValid: true,
|
isAccessAuthValid: true,
|
||||||
|
isTemplateMissingSignatures: false,
|
||||||
template: {
|
template: {
|
||||||
...template,
|
...template,
|
||||||
folder: null,
|
folder: null,
|
||||||
@@ -96,6 +108,7 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
|
|||||||
.then((envelopeForSigning) => {
|
.then((envelopeForSigning) => {
|
||||||
return {
|
return {
|
||||||
isDocumentAccessValid: true,
|
isDocumentAccessValid: true,
|
||||||
|
isTemplateMissingSignatures: false,
|
||||||
envelopeForSigning,
|
envelopeForSigning,
|
||||||
} as const;
|
} as const;
|
||||||
})
|
})
|
||||||
@@ -108,6 +121,13 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.code === AppErrorCode.MISSING_SIGNATURE_FIELD) {
|
||||||
|
return {
|
||||||
|
isDocumentAccessValid: true,
|
||||||
|
isTemplateMissingSignatures: true,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Response('Not Found', { status: 404 });
|
throw new Response('Not Found', { status: 404 });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -181,6 +201,10 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
|||||||
return <DirectTemplateAuthPageView />;
|
return <DirectTemplateAuthPageView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.isTemplateMissingSignatures) {
|
||||||
|
return <DirectTemplateInvalidPageView />;
|
||||||
|
}
|
||||||
|
|
||||||
const { template, directTemplateRecipient } = data;
|
const { template, directTemplateRecipient } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -235,6 +259,10 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
|||||||
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />;
|
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.isTemplateMissingSignatures) {
|
||||||
|
return <DirectTemplateInvalidPageView />;
|
||||||
|
}
|
||||||
|
|
||||||
const { envelope, recipient } = data.envelopeForSigning;
|
const { envelope, recipient } = data.envelopeForSigning;
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ export const getDirectTemplateErrorMessage = (code: string): ToastMessageDescrip
|
|||||||
return match(code)
|
return match(code)
|
||||||
.with('RECIPIENT_LIMIT_EXCEEDED', () => RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
.with('RECIPIENT_LIMIT_EXCEEDED', () => RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||||
|
.with(AppErrorCode.MISSING_SIGNATURE_FIELD, () => ({
|
||||||
|
title: msg`Missing signature fields`,
|
||||||
|
description: msg`This direct link template cannot be used because one or more signers do not have a signature field assigned.`,
|
||||||
|
}))
|
||||||
.otherwise(() => ({
|
.otherwise(() => ({
|
||||||
title: msg`Something went wrong`,
|
title: msg`Something went wrong`,
|
||||||
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
||||||
@@ -77,6 +81,10 @@ export const getTemplateUseErrorMessage = (code: string): ToastMessageDescriptor
|
|||||||
title: msg`Error`,
|
title: msg`Error`,
|
||||||
description: msg`The document was created but could not be sent to recipients.`,
|
description: msg`The document was created but could not be sent to recipients.`,
|
||||||
}))
|
}))
|
||||||
|
.with(AppErrorCode.MISSING_SIGNATURE_FIELD, () => ({
|
||||||
|
title: msg`Missing signature fields`,
|
||||||
|
description: msg`The document could not be sent because some signers do not have a signature field. Please edit the template and add a signature field for each signer.`,
|
||||||
|
}))
|
||||||
.with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => ({
|
.with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => ({
|
||||||
title: msg`Error`,
|
title: msg`Error`,
|
||||||
description: msg`The document could not be created because of missing or invalid information. Please review the template's recipients and fields.`,
|
description: msg`The document could not be created because of missing or invalid information. Please review the template's recipients and fields.`,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
import { expect, type Page, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { clickEnvelopeEditorStep } from '../fixtures/envelope-editor';
|
||||||
|
|
||||||
|
const INVALID_DIRECT_TEMPLATE_ALERT_TITLE = 'Invalid direct link template';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place a field on the PDF canvas in the envelope editor.
|
||||||
|
*/
|
||||||
|
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
|
||||||
|
await root.getByRole('button', { name: fieldName, exact: true }).click();
|
||||||
|
|
||||||
|
const canvas = root.locator('.konva-container canvas').first();
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
await canvas.click({ position });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed a V2 direct template and open it in the native template editor.
|
||||||
|
*
|
||||||
|
* Only the native template editor is covered here: direct links only exist
|
||||||
|
* for templates and are not part of the embedded editor surfaces.
|
||||||
|
*/
|
||||||
|
const openDirectTemplateEditor = async (page: Page, options: { createDirectRecipientSignatureField: boolean }) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const template = await seedDirectTemplate({
|
||||||
|
title: `E2E Direct Template Validation ${Date.now()}`,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
internalVersion: 2,
|
||||||
|
createDirectRecipientSignatureField: options.createDirectRecipientSignatureField,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, team, template };
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('template editor', () => {
|
||||||
|
test('shows invalid direct template warning when a signer has no signature field', async ({ page }) => {
|
||||||
|
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||||
|
|
||||||
|
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||||
|
await expect(page.getByText('are missing a signature field')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not show the warning when all signers have signature fields', async ({ page }) => {
|
||||||
|
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: true });
|
||||||
|
|
||||||
|
// Wait for the editor to render before asserting the banner is absent.
|
||||||
|
await expect(page.getByTestId('envelope-editor-step-upload')).toBeVisible();
|
||||||
|
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('warning disappears after placing a signature field', async ({ page }) => {
|
||||||
|
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||||
|
|
||||||
|
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||||
|
|
||||||
|
// Place a signature field for the direct recipient (auto-selected single recipient).
|
||||||
|
await clickEnvelopeEditorStep(page, 'addFields');
|
||||||
|
await expect(page.locator('.konva-container canvas').first()).toBeVisible();
|
||||||
|
await placeFieldOnPdf(page, 'Signature', { x: 120, y: 140 });
|
||||||
|
|
||||||
|
// The banner clears once the field is autosaved and the envelope state updates.
|
||||||
|
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible({ timeout: 15_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||||
|
import { signSignaturePad } from '../fixtures/signature';
|
||||||
|
|
||||||
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
@@ -73,8 +74,19 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
|||||||
await expect(page.locator('body')).toContainText('public-direct-template-title');
|
await expect(page.locator('body')).toContainText('public-direct-template-title');
|
||||||
await expect(page.locator('body')).toContainText('public-direct-template-description');
|
await expect(page.locator('body')).toContainText('public-direct-template-description');
|
||||||
|
|
||||||
|
const directSignatureField = directTemplate.fields[0];
|
||||||
|
|
||||||
|
if (!directSignatureField) {
|
||||||
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
|
}
|
||||||
|
|
||||||
await page.getByRole('link', { name: 'Sign' }).click();
|
await page.getByRole('link', { name: 'Sign' }).click();
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await signSignaturePad(page);
|
||||||
|
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||||
|
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,18 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
|
|||||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
await expect(page.getByLabel('Email')).toBeDisabled();
|
await expect(page.getByLabel('Email')).toBeDisabled();
|
||||||
|
|
||||||
|
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||||
|
|
||||||
|
if (!directSignatureField) {
|
||||||
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await signSignaturePad(page);
|
||||||
|
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||||
|
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
@@ -235,6 +246,37 @@ test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page })
|
|||||||
await page.goto(directTemplatePath);
|
await page.goto(directTemplatePath);
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
|
||||||
|
|
||||||
|
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||||
|
|
||||||
|
if (!directSignatureField) {
|
||||||
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the PDF and the Konva canvas overlay to be ready.
|
||||||
|
await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
|
||||||
|
const canvas = page.locator('.konva-container canvas').first();
|
||||||
|
await expect(canvas).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
// Sign the direct template recipient's signature field via the canvas-based V2 UI.
|
||||||
|
await signSignaturePad(page);
|
||||||
|
|
||||||
|
const canvasBox = await canvas.boundingBox();
|
||||||
|
|
||||||
|
if (!canvasBox) {
|
||||||
|
throw new Error('Canvas bounding box not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const x =
|
||||||
|
(Number(directSignatureField.positionX) / 100) * canvasBox.width +
|
||||||
|
((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
|
||||||
|
const y =
|
||||||
|
(Number(directSignatureField.positionY) / 100) * canvasBox.height +
|
||||||
|
((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;
|
||||||
|
|
||||||
|
await canvas.click({ position: { x, y } });
|
||||||
|
await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await expect(page.getByLabel('Your Email')).not.toBeVisible();
|
await expect(page.getByLabel('Your Email')).not.toBeVisible();
|
||||||
|
|
||||||
@@ -266,6 +308,16 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
|
|||||||
|
|
||||||
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
|
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
|
||||||
|
|
||||||
|
const directSignatureField = template.fields[0];
|
||||||
|
|
||||||
|
if (!directSignatureField) {
|
||||||
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
await signSignaturePad(page);
|
||||||
|
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||||
|
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
await page.waitForURL(/\/sign/);
|
await page.waitForURL(/\/sign/);
|
||||||
@@ -299,19 +351,13 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const directTemplateRecipient = template.recipients[0];
|
// The seeded direct template already includes a signature field for the direct recipient.
|
||||||
|
const directSignatureField = template.fields[0];
|
||||||
|
|
||||||
if (!directTemplateRecipient) {
|
if (!directSignatureField) {
|
||||||
throw new Error('Expected direct template recipient to exist');
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
|
||||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
|
||||||
envelopeId: template.id,
|
|
||||||
recipientId: directTemplateRecipient.id,
|
|
||||||
positionY: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalName = 'Signer 2';
|
const originalName = 'Signer 2';
|
||||||
const originalSecondSignerEmail = seedTestEmail();
|
const originalSecondSignerEmail = seedTestEmail();
|
||||||
|
|
||||||
@@ -413,19 +459,13 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const directTemplateRecipient = template.recipients[0];
|
// The seeded direct template already includes a signature field for the direct recipient.
|
||||||
|
const directSignatureField = template.fields[0];
|
||||||
|
|
||||||
if (!directTemplateRecipient) {
|
if (!directSignatureField) {
|
||||||
throw new Error('Expected direct template recipient to exist');
|
throw new Error('Expected seeded direct template signature field to exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
|
||||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
|
||||||
envelopeId: template.id,
|
|
||||||
recipientId: directTemplateRecipient.id,
|
|
||||||
positionY: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const originalName = 'Signer 2';
|
const originalName = 'Signer 2';
|
||||||
const originalSecondSignerEmail = seedTestEmail();
|
const originalSecondSignerEmail = seedTestEmail();
|
||||||
|
|
||||||
@@ -521,3 +561,48 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
|||||||
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
|
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
|
||||||
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
|
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[DIRECT_TEMPLATES]: V1 direct template without signature fields shows invalid template page', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const template = await seedDirectTemplate({
|
||||||
|
title: 'V1 invalid direct template',
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
createDirectRecipientSignatureField: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||||
|
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||||
|
|
||||||
|
// The signing flow must not render.
|
||||||
|
await expect(page.getByRole('heading', { name: 'General' })).not.toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Continue' })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[DIRECT_TEMPLATES]: V2 direct template without signature fields shows invalid template page', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const template = await seedDirectTemplate({
|
||||||
|
title: 'V2 invalid direct template',
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
internalVersion: 2,
|
||||||
|
createDirectRecipientSignatureField: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||||
|
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||||
|
|
||||||
|
// The signing flow (PDF canvas) must not render.
|
||||||
|
await expect(page.locator('.konva-container canvas')).toHaveCount(0);
|
||||||
|
await expect(page.getByRole('button', { name: 'Complete' })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
||||||
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||||
|
|
||||||
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||||
|
|
||||||
|
const seedSignatureFieldForRecipient = async (options: { envelopeId: string; recipientId: number }) => {
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
|
||||||
|
where: { envelopeId: options.envelopeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.field.create({
|
||||||
|
data: {
|
||||||
|
envelopeId: options.envelopeId,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
recipientId: options.recipientId,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 5,
|
||||||
|
positionY: 10,
|
||||||
|
width: 20,
|
||||||
|
height: 5,
|
||||||
|
customText: '',
|
||||||
|
inserted: false,
|
||||||
|
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test('[TEMPLATE_USE]: shows missing signature fields error when sending a template without signature fields', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
// seedTemplate creates one SIGNER recipient and no fields.
|
||||||
|
await seedTemplate({
|
||||||
|
title: 'Template missing signature fields',
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||||
|
|
||||||
|
// Enable distribution so the document is sent on creation.
|
||||||
|
await page.locator('#distributeDocument').click();
|
||||||
|
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||||
|
|
||||||
|
await expectToastTextToBeVisible(page, 'Missing signature fields');
|
||||||
|
await expectToastTextToBeVisible(
|
||||||
|
page,
|
||||||
|
'The document could not be sent because some signers do not have a signature field',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[TEMPLATE_USE]: creates and sends a document when signers have signature fields', async ({ page }) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const template = await seedTemplate({
|
||||||
|
title: 'Template with signature fields',
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await seedSignatureFieldForRecipient({
|
||||||
|
envelopeId: template.id,
|
||||||
|
recipientId: template.recipients[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/t/${team.url}/templates`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('#distributeDocument').click();
|
||||||
|
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
|
||||||
|
|
||||||
|
const envelopeId = page.url().split('/').pop()?.split('?')[0];
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
|
where: { id: envelopeId },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(envelope.status).toBe(DocumentStatus.PENDING);
|
||||||
|
});
|
||||||
@@ -36,6 +36,13 @@ export enum AppErrorCode {
|
|||||||
*/
|
*/
|
||||||
ENVELOPE_TSP_LOCKED = 'ENVELOPE_TSP_LOCKED',
|
ENVELOPE_TSP_LOCKED = 'ENVELOPE_TSP_LOCKED',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A signer recipient does not have a signature field assigned. Thrown when
|
||||||
|
* distributing an envelope or using a direct template where at least one
|
||||||
|
* signer has no signature field.
|
||||||
|
*/
|
||||||
|
MISSING_SIGNATURE_FIELD = 'MISSING_SIGNATURE_FIELD',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSC (Cloud Signature Consortium) error codes. See the CSC QES V1 spec
|
* CSC (Cloud Signature Consortium) error codes. See the CSC QES V1 spec
|
||||||
* for the recovery taxonomy.
|
* for the recovery taxonomy.
|
||||||
@@ -84,6 +91,7 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
|||||||
[AppErrorCode.ENVELOPE_CANCELLED]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_CANCELLED]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
|
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
||||||
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||||
@@ -291,6 +299,7 @@ export class AppError extends Error {
|
|||||||
AppErrorCode.ENVELOPE_CANCELLED,
|
AppErrorCode.ENVELOPE_CANCELLED,
|
||||||
AppErrorCode.ENVELOPE_LEGACY,
|
AppErrorCode.ENVELOPE_LEGACY,
|
||||||
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
||||||
|
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
||||||
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
||||||
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
||||||
AppErrorCode.CSC_CERT_INVALID,
|
AppErrorCode.CSC_CERT_INVALID,
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
|
|||||||
.map((r) => (r.name ? `${r.name} (${r.email}, id: ${r.id})` : `${r.email} (id: ${r.id})`))
|
.map((r) => (r.name ? `${r.name} (${r.email}, id: ${r.id})` : `${r.email} (id: ${r.id})`))
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||||
message: `The following recipients are missing required fields: ${missingRecipientDescriptions}. Signers must have at least one signature field.`,
|
message: `The following recipients are missing required fields: ${missingRecipientDescriptions}. Signers must have at least one signature field.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
|
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
||||||
import { extractFieldAutoInsertValues } from '../document/send-document';
|
import { extractFieldAutoInsertValues } from '../document/send-document';
|
||||||
import { getTeamSettings } from '../team/get-team-settings';
|
import { getTeamSettings } from '../team/get-team-settings';
|
||||||
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||||
@@ -125,6 +126,17 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
||||||
|
envelope.recipients,
|
||||||
|
envelope.recipients.flatMap((envelopeRecipient) => envelopeRecipient.fields),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recipientsWithMissingFields.length > 0) {
|
||||||
|
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||||
|
message: 'One or more signers on this direct template are missing a signature field',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
||||||
|
|
||||||
const sender = settings.includeSenderDetails
|
const sender = settings.includeSenderDetails
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
extractDocumentAuthMethods,
|
extractDocumentAuthMethods,
|
||||||
} from '../../utils/document-auth';
|
} from '../../utils/document-auth';
|
||||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||||
|
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
||||||
import { sendDocument } from '../document/send-document';
|
import { sendDocument } from '../document/send-document';
|
||||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||||
import { incrementDocumentId } from '../envelope/increment-id';
|
import { incrementDocumentId } from '../envelope/increment-id';
|
||||||
@@ -172,6 +173,17 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
||||||
|
recipients,
|
||||||
|
recipients.flatMap((recipient) => recipient.fields),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recipientsWithMissingFields.length > 0) {
|
||||||
|
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||||
|
message: 'One or more signers on this direct template are missing a signature field',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||||
} from '@documenso/lib/constants/direct-templates';
|
} from '@documenso/lib/constants/direct-templates';
|
||||||
import { incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id';
|
import { incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id';
|
||||||
|
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||||
import { SignatureLevel } from '@documenso/lib/types/signature-level';
|
import { SignatureLevel } from '@documenso/lib/types/signature-level';
|
||||||
import { prefixedId } from '@documenso/lib/universal/id';
|
import { prefixedId } from '@documenso/lib/universal/id';
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
DocumentDataType,
|
DocumentDataType,
|
||||||
DocumentSource,
|
DocumentSource,
|
||||||
EnvelopeType,
|
EnvelopeType,
|
||||||
|
FieldType,
|
||||||
ReadStatus,
|
ReadStatus,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
@@ -29,6 +31,11 @@ type SeedTemplateOptions = {
|
|||||||
teamId: number;
|
teamId: number;
|
||||||
internalVersion?: 1 | 2;
|
internalVersion?: 1 | 2;
|
||||||
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
|
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
|
||||||
|
/**
|
||||||
|
* Only used by seedDirectTemplate. Creates a signature field for the direct
|
||||||
|
* recipient so the seeded direct template is valid. Defaults to true.
|
||||||
|
*/
|
||||||
|
createDirectRecipientSignatureField?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateTemplateOptions = {
|
type CreateTemplateOptions = {
|
||||||
@@ -215,6 +222,31 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { createDirectRecipientSignatureField = true } = options;
|
||||||
|
|
||||||
|
if (createDirectRecipientSignatureField) {
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
|
||||||
|
where: { envelopeId: template.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.field.create({
|
||||||
|
data: {
|
||||||
|
envelopeId: template.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
recipientId: directTemplateRecpient.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 5,
|
||||||
|
positionY: 10,
|
||||||
|
width: 20,
|
||||||
|
height: 5,
|
||||||
|
customText: '',
|
||||||
|
inserted: false,
|
||||||
|
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.envelope.findFirstOrThrow({
|
return await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: template.id,
|
id: template.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user