mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
fix: auto insert prefilled text and number fields (#2157)
This commit is contained in:
@ -104,7 +104,7 @@ export default function EnvelopeGenericPageRenderer() {
|
||||
pageHeight: unscaledViewport.height,
|
||||
color: getRecipientColorKey(field.recipientId),
|
||||
editable: false,
|
||||
mode: overrideSettings?.mode ?? 'sign',
|
||||
mode: overrideSettings?.mode ?? 'edit',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -220,7 +220,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
type: FieldType.NUMBER,
|
||||
fieldMeta: {
|
||||
type: 'number',
|
||||
value: '123',
|
||||
value: '123456789',
|
||||
},
|
||||
page: 4,
|
||||
...calculatePosition(3, 2),
|
||||
@ -273,8 +273,8 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
type: 'radio',
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 3, checked: false, value: 'Option 3' },
|
||||
{ id: 2, checked: false, value: 'Option 2' },
|
||||
{ id: 3, checked: true, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 5,
|
||||
@ -341,7 +341,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
||||
values: [
|
||||
{ id: 1, checked: false, value: 'Option 1' },
|
||||
{ id: 2, checked: true, value: 'Option 2' },
|
||||
{ id: 2, checked: true, value: 'Option 3' },
|
||||
{ id: 3, checked: false, value: 'Option 3' },
|
||||
],
|
||||
},
|
||||
page: 6,
|
||||
|
||||
@ -21,7 +21,7 @@ import pixelMatch from 'pixelmatch';
|
||||
import { PNG } from 'pngjs';
|
||||
import type { TestInfo } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||
@ -29,8 +29,21 @@ import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||
import { RecipientRole } from '../../../prisma/generated/types';
|
||||
import { FIELD_META_TEST_FIELDS } from '../../constants/field-meta-pdf';
|
||||
import { ALIGNMENT_TEST_FIELDS } from '../../constants/field-alignment-pdf';
|
||||
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||
import { isBase64Image } from '../../../lib/constants/signatures';
|
||||
|
||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
@ -61,21 +74,173 @@ test.skip('seed alignment test document', async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test('field placement visual regression', async ({ page }, testInfo) => {
|
||||
test('field placement visual regression', async ({ page, request }, testInfo) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const envelope = await seedAlignmentTestDocument({
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
recipientName: user.name || '',
|
||||
recipientEmail: user.email,
|
||||
insertFields: true,
|
||||
status: DocumentStatus.PENDING,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const token = envelope.recipients[0].token;
|
||||
// Step 1: Create initial envelope with Prisma (with first envelope item)
|
||||
const alignmentPdf = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
|
||||
);
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf'));
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
const fieldMetaFields = FIELD_META_TEST_FIELDS.map((field) => ({
|
||||
identifier: 'field-meta',
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
}));
|
||||
|
||||
const alignmentFields = ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||
identifier: 'alignment-pdf',
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
}));
|
||||
|
||||
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Envelope Full Field Test',
|
||||
recipients: [
|
||||
{
|
||||
email: user.email,
|
||||
name: user.name || '',
|
||||
role: RecipientRole.SIGNER,
|
||||
fields: [...fieldMetaFields, ...alignmentFields],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||
|
||||
formData.append('files', new File([alignmentPdf], 'alignment-pdf', { type: 'application/pdf' }));
|
||||
formData.append('files', new File([fieldMetaPdf], 'field-meta', { type: 'application/pdf' }));
|
||||
|
||||
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
||||
expect(createEnvelopeRequest.status()).toBe(200);
|
||||
|
||||
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: {
|
||||
id: createdEnvelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
const recipientId = envelope.recipients[0].id;
|
||||
const alignmentItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
|
||||
const fieldMetaItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 2);
|
||||
|
||||
expect(recipientId).toBeDefined();
|
||||
expect(alignmentItem).toBeDefined();
|
||||
expect(fieldMetaItem).toBeDefined();
|
||||
|
||||
if (!alignmentItem || !fieldMetaItem) {
|
||||
throw new Error('Envelope items not found');
|
||||
}
|
||||
|
||||
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
envelopeId: envelope.id,
|
||||
} satisfies TDistributeEnvelopeRequest,
|
||||
});
|
||||
|
||||
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||
|
||||
const uninsertedFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.id,
|
||||
inserted: false,
|
||||
},
|
||||
include: {
|
||||
envelopeItem: {
|
||||
select: {
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
uninsertedFields.map(async (field) => {
|
||||
let foundField = ALIGNMENT_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
field.page === f.page &&
|
||||
field.envelopeItem.title === 'alignment-pdf' &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||
);
|
||||
|
||||
if (!foundField) {
|
||||
foundField = FIELD_META_TEST_FIELDS.find(
|
||||
(f) =>
|
||||
field.page === f.page &&
|
||||
field.envelopeItem.title === 'field-meta' &&
|
||||
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||
);
|
||||
}
|
||||
|
||||
if (!foundField) {
|
||||
throw new Error('Field not found');
|
||||
}
|
||||
|
||||
await prisma.field.update({
|
||||
where: {
|
||||
id: field.id,
|
||||
},
|
||||
data: {
|
||||
inserted: true,
|
||||
customText: foundField.customText,
|
||||
signature: foundField.signature
|
||||
? {
|
||||
create: {
|
||||
recipientId: envelope.recipients[0].id,
|
||||
signatureImageAsBase64: isBase64Image(foundField.signature)
|
||||
? foundField.signature
|
||||
: null,
|
||||
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const recipientToken = envelope.recipients[0].token;
|
||||
const signUrl = `/sign/${recipientToken}`;
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
@ -124,7 +289,7 @@ test('field placement visual regression', async ({ page }, testInfo) => {
|
||||
const documentUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: item,
|
||||
token,
|
||||
token: recipientToken,
|
||||
version: 'signed',
|
||||
});
|
||||
|
||||
|
||||
@ -24,7 +24,9 @@ import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldAndMetaSchema,
|
||||
ZNumberFieldMeta,
|
||||
ZRadioFieldMeta,
|
||||
ZTextFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
@ -182,9 +184,18 @@ export const sendDocument = async ({
|
||||
// Validate and autoinsert fields for V2 envelopes.
|
||||
if (envelope.internalVersion === 2) {
|
||||
for (const unknownField of envelope.fields) {
|
||||
const recipient = envelope.recipients.find((r) => r.id === unknownField.recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldToAutoInsert = extractFieldAutoInsertValues(unknownField);
|
||||
|
||||
if (fieldToAutoInsert) {
|
||||
// Only auto-insert fields if the recipient has not been sent the document yet.
|
||||
if (fieldToAutoInsert && recipient.sendStatus !== SendStatus.SENT) {
|
||||
fieldsToAutoInsert.push(fieldToAutoInsert);
|
||||
}
|
||||
}
|
||||
@ -205,6 +216,7 @@ export const sendDocument = async ({
|
||||
if (envelope.internalVersion === 2) {
|
||||
const autoInsertedFields = await Promise.all(
|
||||
fieldsToAutoInsert.map(async (field) => {
|
||||
// Warning: Only auto-insert fields if the recipient has not been sent the document yet.
|
||||
return await tx.field.update({
|
||||
where: {
|
||||
id: field.fieldId,
|
||||
@ -337,6 +349,31 @@ export const extractFieldAutoInsertValues = (
|
||||
const field = parsedField.data;
|
||||
const fieldId = unknownField.id;
|
||||
|
||||
// Auto insert text fields with prefilled values.
|
||||
if (field.type === FieldType.TEXT) {
|
||||
const { text } = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (text) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert number fields with prefilled values.
|
||||
if (field.type === FieldType.NUMBER) {
|
||||
const { value } = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
if (value) {
|
||||
return {
|
||||
fieldId,
|
||||
customText: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert radio fields with the pre-checked value.
|
||||
if (field.type === FieldType.RADIO) {
|
||||
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
@ -350,6 +387,7 @@ export const extractFieldAutoInsertValues = (
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert dropdown fields with the default value.
|
||||
if (field.type === FieldType.DROPDOWN) {
|
||||
const { defaultValue, values = [] } = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
@ -361,6 +399,7 @@ export const extractFieldAutoInsertValues = (
|
||||
}
|
||||
}
|
||||
|
||||
// Auto insert checkbox fields with the pre-checked values.
|
||||
if (field.type === FieldType.CHECKBOX) {
|
||||
const {
|
||||
values = [],
|
||||
|
||||
@ -48,14 +48,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
let textLineHeight = FIELD_DEFAULT_LINE_HEIGHT;
|
||||
let textLetterSpacing = FIELD_DEFAULT_LETTER_SPACING;
|
||||
|
||||
// Default to blank for export mode since this we want to ensure we don't show
|
||||
// any placeholder text or labels unless actually it's inserted.
|
||||
if (mode === 'export') {
|
||||
textToRender = '';
|
||||
}
|
||||
|
||||
// Use default values for text/number if provided.
|
||||
if (fieldMeta?.type === 'text' || fieldMeta?.type === 'number') {
|
||||
// Render default values for text/number if provided for editing mode.
|
||||
if (mode === 'edit' && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
@ -68,6 +62,27 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
}
|
||||
}
|
||||
|
||||
// Default to blank for export mode since we want to ensure we don't show
|
||||
// any placeholder text or labels unless actually it's inserted.
|
||||
if (mode === 'export') {
|
||||
textToRender = '';
|
||||
}
|
||||
|
||||
// Fallback render readonly fields if prefilled value exists.
|
||||
if (field?.fieldMeta?.readOnly && (fieldMeta?.type === 'text' || fieldMeta?.type === 'number')) {
|
||||
const value = fieldMeta?.type === 'text' ? fieldMeta.text : fieldMeta.value;
|
||||
|
||||
if (value) {
|
||||
textToRender = value;
|
||||
|
||||
textVerticalAlign = fieldMeta.verticalAlign || FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN;
|
||||
textAlign = fieldMeta.textAlign || FIELD_DEFAULT_GENERIC_ALIGN;
|
||||
textLetterSpacing = fieldMeta.letterSpacing || FIELD_DEFAULT_LETTER_SPACING;
|
||||
textLineHeight = fieldMeta.lineHeight || FIELD_DEFAULT_LINE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Override everything with value if it's inserted.
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user