import { lazy, useEffect, useMemo, useState } from 'react'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { FieldType, RecipientRole } from '@prisma/client'; import { FileTextIcon } from 'lucide-react'; import { Link } from 'react-router'; import { isDeepEqual } from 'remeda'; import { match } from 'ts-pattern'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { compositePageToBlob, getPageCanvasRefs, } from '@documenso/lib/client-only/utils/page-canvas-registry'; import type { TCheckboxFieldMeta, TDateFieldMeta, TDropdownFieldMeta, TEmailFieldMeta, TFieldMetaSchema, TInitialsFieldMeta, TNameFieldMeta, TNumberFieldMeta, TRadioFieldMeta, TSignatureFieldMeta, TTextFieldMeta, } from '@documenso/lib/types/field-meta'; import { FIELD_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; 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 { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector'; import { Separator } from '@documenso/ui/primitives/separator'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { EditorFieldCheckboxForm } from '~/components/forms/editor/editor-field-checkbox-form'; import { EditorFieldDateForm } from '~/components/forms/editor/editor-field-date-form'; import { EditorFieldDropdownForm } from '~/components/forms/editor/editor-field-dropdown-form'; import { EditorFieldEmailForm } from '~/components/forms/editor/editor-field-email-form'; import { EditorFieldInitialsForm } from '~/components/forms/editor/editor-field-initials-form'; import { EditorFieldNameForm } from '~/components/forms/editor/editor-field-name-form'; import { EditorFieldNumberForm } from '~/components/forms/editor/editor-field-number-form'; import { EditorFieldRadioForm } from '~/components/forms/editor/editor-field-radio-form'; import { EditorFieldSignatureForm } from '~/components/forms/editor/editor-field-signature-form'; import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text-form'; import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop'; import { EnvelopeRendererFileSelector } from './envelope-file-selector'; const EnvelopeEditorFieldsPageRenderer = lazy( async () => import('./envelope-editor-fields-page-renderer'), ); /** * Enforces minimum field dimensions and centers the field when expanding to meet minimums. * * AI often detects form lines as very thin fields (0.2-0.5% height). This function ensures * fields meet minimum usability requirements by expanding them to at least 30px height and * 36px width, while keeping them centered on their original position. * * @param params - Field dimensions and page size * @param params.positionX - Field X position as percentage (0-100) * @param params.positionY - Field Y position as percentage (0-100) * @param params.width - Field width as percentage (0-100) * @param params.height - Field height as percentage (0-100) * @param params.pageWidth - Page width in pixels * @param params.pageHeight - Page height in pixels * @returns Adjusted field dimensions with minimums enforced and centered * * @example * // AI detected a thin line: 0.3% height * const adjusted = enforceMinimumFieldDimensions({ * positionX: 20, positionY: 50, width: 30, height: 0.3, * pageWidth: 800, pageHeight: 1100 * }); * // Result: height expanded to ~2.7% (30px), centered on original position */ /** * Enforces minimum field dimensions with centered expansion. * * If a field is smaller than the minimum width or height, it will be expanded * to meet the minimum requirements while staying centered on its original position. */ const enforceMinimumFieldDimensions = (params: { positionX: number; positionY: number; width: number; height: number; pageWidth: number; pageHeight: number; }): { positionX: number; positionY: number; width: number; height: number; } => { const MIN_HEIGHT_PX = 30; const MIN_WIDTH_PX = 36; // Convert percentage to pixels to check against minimums const widthPx = (params.width / 100) * params.pageWidth; const heightPx = (params.height / 100) * params.pageHeight; let adjustedWidth = params.width; let adjustedHeight = params.height; let adjustedPositionX = params.positionX; let adjustedPositionY = params.positionY; if (widthPx < MIN_WIDTH_PX) { const centerXPx = (params.positionX / 100) * params.pageWidth + widthPx / 2; adjustedWidth = (MIN_WIDTH_PX / params.pageWidth) * 100; adjustedPositionX = ((centerXPx - MIN_WIDTH_PX / 2) / params.pageWidth) * 100; if (adjustedPositionX < 0) { adjustedPositionX = 0; } else if (adjustedPositionX + adjustedWidth > 100) { adjustedPositionX = 100 - adjustedWidth; } } if (heightPx < MIN_HEIGHT_PX) { const centerYPx = (params.positionY / 100) * params.pageHeight + heightPx / 2; adjustedHeight = (MIN_HEIGHT_PX / params.pageHeight) * 100; adjustedPositionY = ((centerYPx - MIN_HEIGHT_PX / 2) / params.pageHeight) * 100; if (adjustedPositionY < 0) { adjustedPositionY = 0; } else if (adjustedPositionY + adjustedHeight > 100) { adjustedPositionY = 100 - adjustedHeight; } } return { positionX: adjustedPositionX, positionY: adjustedPositionY, width: adjustedWidth, height: adjustedHeight, }; }; const FieldSettingsTypeTranslations: Record = { [FieldType.SIGNATURE]: msg`Signature Settings`, [FieldType.FREE_SIGNATURE]: msg`Free Signature Settings`, [FieldType.TEXT]: msg`Text Settings`, [FieldType.DATE]: msg`Date Settings`, [FieldType.EMAIL]: msg`Email Settings`, [FieldType.NAME]: msg`Name Settings`, [FieldType.INITIALS]: msg`Initials Settings`, [FieldType.NUMBER]: msg`Number Settings`, [FieldType.RADIO]: msg`Radio Settings`, [FieldType.CHECKBOX]: msg`Checkbox Settings`, [FieldType.DROPDOWN]: msg`Dropdown Settings`, }; export const EnvelopeEditorFieldsPage = () => { const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor(); const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { t } = useLingui(); const { toast } = useToast(); const [isAutoAddingFields, setIsAutoAddingFields] = useState(false); const selectedField = useMemo( () => structuredClone(editorFields.selectedField), [editorFields.selectedField], ); const updateSelectedFieldMeta = (fieldMeta: TFieldMetaSchema) => { if (!selectedField) { return; } const isMetaSame = isDeepEqual(selectedField.fieldMeta, fieldMeta); // Todo: Envelopes - Clean up console logs. if (!isMetaSame) { console.log('TRIGGER UPDATE'); editorFields.updateFieldByFormId(selectedField.formId, { fieldMeta, }); } else { console.log('DATA IS SAME, NO UPDATE'); } }; /** * Set the selected recipient to the first recipient in the envelope. */ useEffect(() => { const firstSelectableRecipient = envelope.recipients.find( (recipient) => recipient.role === RecipientRole.SIGNER || recipient.role === RecipientRole.APPROVER, ); editorFields.setSelectedRecipient(firstSelectableRecipient?.id ?? null); }, []); return (
{/* Horizontal envelope item selector */} {/* Document View */}
{currentEnvelopeItem !== null ? ( ) : (

No documents found

Please upload a document to continue

)}
{/* Right Section - Form Fields Panel */} {currentEnvelopeItem && (
{/* Recipient selector section. */}

Selected Recipient

{envelope.recipients.length === 0 ? ( You need at least one recipient to add fields

Click here to add a recipient

) : ( editorFields.setSelectedRecipient(recipient.id) } recipients={envelope.recipients} className="w-full" align="end" /> )} {editorFields.selectedRecipient && !canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && ( This recipient can no longer be modified as they have signed a field, or completed the document. )}
{/* Add fields section. */}

Add Fields

{/* Field details section. */} {selectedField && (

{t(FieldSettingsTypeTranslations[selectedField.type])}

{match(selectedField.type) .with(FieldType.SIGNATURE, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.CHECKBOX, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.DATE, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.DROPDOWN, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.EMAIL, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.INITIALS, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.NAME, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.NUMBER, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.RADIO, () => ( updateSelectedFieldMeta(value)} /> )) .with(FieldType.TEXT, () => ( updateSelectedFieldMeta(value)} /> )) .otherwise(() => null)}
)}
)}
); };