import { useEffect, useMemo, useRef, useState } from 'react'; import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client'; import { FileTextIcon, SparklesIcon } from 'lucide-react'; import { useRevalidator, useSearchParams } 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 { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n'; import type { NormalizedFieldWithContext } from '@documenso/lib/server-only/ai/envelope/detect-fields/types'; import { FIELD_META_DEFAULT_VALUES, type TCheckboxFieldMeta, type TDateFieldMeta, type TDropdownFieldMeta, type TEmailFieldMeta, type TFieldMetaSchema, type TInitialsFieldMeta, type TNameFieldMeta, type TNumberFieldMeta, type TRadioFieldMeta, type TSignatureFieldMeta, type TTextFieldMeta, } 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 { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Separator } from '@documenso/ui/primitives/separator'; import { AiFeaturesEnableDialog } from '~/components/dialogs/ai-features-enable-dialog'; import { AiFieldDetectionDialog } from '~/components/dialogs/ai-field-detection-dialog'; 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 { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer'; import { useCurrentTeam } from '~/providers/team'; import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop'; import { EnvelopeEditorFieldsPageRenderer } from './envelope-editor-fields-page-renderer'; import { EnvelopeRendererFileSelector } from './envelope-file-selector'; import { EnvelopeRecipientSelector } from './envelope-recipient-selector'; 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 [searchParams] = useSearchParams(); const team = useCurrentTeam(); const scrollableContainerRef = useRef(null); const { envelope, editorFields, navigateToStep, editorConfig } = useCurrentEnvelopeEditor(); const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { _ } = useLingui(); const [isAiFieldDialogOpen, setIsAiFieldDialogOpen] = useState(false); const [isAiEnableDialogOpen, setIsAiEnableDialogOpen] = useState(false); const { revalidate } = useRevalidator(); const selectedField = useMemo( () => structuredClone(editorFields.selectedField), [editorFields.selectedField], ); const updateSelectedFieldMeta = (fieldMeta: TFieldMetaSchema) => { if (!selectedField) { return; } const isMetaSame = isDeepEqual(selectedField.fieldMeta, fieldMeta); if (!isMetaSame) { editorFields.updateFieldByFormId(selectedField.formId, { fieldMeta, }); } }; const onFieldDetectionComplete = (fields: NormalizedFieldWithContext[]) => { for (const field of fields) { editorFields.addField({ height: field.height, width: field.width, positionX: field.positionX, positionY: field.positionY, type: field.type, envelopeItemId: field.envelopeItemId, recipientId: field.recipientId, page: field.pageNumber, fieldMeta: structuredClone(FIELD_META_DEFAULT_VALUES[field.type]), }); } setIsAiFieldDialogOpen(false); }; /** * 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); }, []); const onDetectClick = () => { if (!team.preferences.aiFeaturesEnabled) { setIsAiEnableDialogOpen(true); return; } setIsAiFieldDialogOpen(true); }; const onAiFeaturesEnabled = () => { void revalidate().then(() => { setIsAiEnableDialogOpen(false); setIsAiFieldDialogOpen(true); }); }; return (
{/* Horizontal envelope item selector */} {/* Document View */}
{envelope.recipients.length === 0 && (
Missing Recipients You need at least one recipient to add fields
)} {currentEnvelopeItem !== null ? ( ) : (

No documents found

Please upload a document to continue

)}
{/* Right Section - Form Fields Panel */} {currentEnvelopeItem && envelope.recipients.length > 0 && (
{/* Recipient selector section. */}

Selected Recipient

editorFields.setSelectedRecipient(recipient.id) } recipients={envelope.recipients} fields={envelope.fields} 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

{editorConfig.fields?.allowAIDetection && ( <> )}
{/* Field details section. */} {selectedField && (
{searchParams.get('devmode') && ( <>

Developer Mode

Pos X:   {selectedField.positionX.toFixed(2)}

Pos Y:   {selectedField.positionY.toFixed(2)}

Width:   {selectedField.width.toFixed(2)}

Height:   {selectedField.height.toFixed(2)}

)}

{_(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)}
)}
)}
); };