feat: autoplace fields from placeholders (#2111)

This PR introduces automatic detection and placement of fields and
recipients based on PDF placeholders.

The placeholders have the following structure:
- `{{fieldType,recipientPosition,fieldMeta}}` 
- `{{text,r1,required=true,textAlign=right,fontSize=50}}`

When the user uploads a PDF document containing such placeholders, they
get converted automatically to Documenso fields and assigned to
recipients.
This commit is contained in:
Catalin Pit
2026-01-29 04:13:45 +02:00
committed by GitHub
parent d77f81163b
commit d18dcb4d60
22 changed files with 2045 additions and 50 deletions
@@ -22,7 +22,7 @@ export const createEnvelopeFieldsMeta: TrpcRouteMeta = {
},
};
const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
const ZCreateFieldBaseSchema = ZEnvelopeFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for'),
envelopeItemId: z
@@ -31,14 +31,51 @@ const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
}),
);
/**
* Position a field using explicit percentage-based coordinates.
*/
const ZCoordinatePositionSchema = z.object({
page: ZFieldPageNumberSchema,
positionX: ZClampedFieldPositionXSchema,
positionY: ZClampedFieldPositionYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
});
/**
* Position a field using a PDF text placeholder (e.g. "{{name}}").
*
* The placeholder text is matched in the envelope item's PDF and the field is
* placed at the bounding box of that match. Width and height can optionally be
* overridden; when omitted the dimensions of the placeholder text are used.
*/
const ZPlaceholderPositionSchema = z.object({
placeholder: z
.string()
.describe(
'Text to search for in the PDF (e.g. "{{name}}"). The field will be placed at the location of this text.',
),
width: ZClampedFieldWidthSchema.optional().describe(
'Override the width of the field. When omitted, the width of the placeholder text is used.',
),
height: ZClampedFieldHeightSchema.optional().describe(
'Override the height of the field. When omitted, the height of the placeholder text is used.',
),
matchAll: z
.boolean()
.optional()
.describe(
'When true, creates a field at every occurrence of the placeholder in the PDF. When false or omitted, only the first occurrence is used.',
),
});
const ZCreateFieldSchema = ZCreateFieldBaseSchema.and(
z.union([ZCoordinatePositionSchema, ZPlaceholderPositionSchema]),
);
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZCreateFieldSchema.array(),