import { useEffect, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { motion } from 'framer-motion'; import { ArrowLeftIcon, CopyPlusIcon, DownloadCloudIcon, EyeIcon, LinkIcon, MousePointer, SendIcon, SettingsIcon, Trash2Icon, Upload, } from 'lucide-react'; import { useNavigate, useSearchParams } from 'react-router'; import { Link } from 'react-router'; import { match } from 'ts-pattern'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId, } from '@documenso/lib/utils/envelope'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { Button } from '@documenso/ui/primitives/button'; import { Separator } from '@documenso/ui/primitives/separator'; import { SpinnerBox } from '@documenso/ui/primitives/spinner'; import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog'; import { EnvelopeDistributeDialog } from '~/components/dialogs/envelope-distribute-dialog'; import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; import { EnvelopeDuplicateDialog } from '~/components/dialogs/envelope-duplicate-dialog'; import { EnvelopeRedistributeDialog } from '~/components/dialogs/envelope-redistribute-dialog'; import { TemplateDeleteDialog } from '~/components/dialogs/template-delete-dialog'; import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog'; import { EnvelopeEditorSettingsDialog } from '~/components/general/envelope-editor/envelope-editor-settings-dialog'; import { EnvelopeEditorFieldsPage } from './envelope-editor-fields-page'; import EnvelopeEditorHeader from './envelope-editor-header'; import { EnvelopeEditorPreviewPage } from './envelope-editor-preview-page'; import { EnvelopeEditorUploadPage } from './envelope-editor-upload-page'; type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview'; const envelopeEditorSteps = [ { id: 'upload', order: 1, title: msg`Document & Recipients`, icon: Upload, description: msg`Upload documents and add recipients`, }, { id: 'addFields', order: 2, title: msg`Add Fields`, icon: MousePointer, description: msg`Place and configure form fields in the document`, }, { id: 'preview', order: 3, title: msg`Preview`, icon: EyeIcon, description: msg`Preview the document before sending`, }, ]; export default function EnvelopeEditor() { const { t } = useLingui(); const navigate = useNavigate(); const { envelope, isDocument, isTemplate, isAutosaving, flushAutosave, relativePath, editorFields, } = useCurrentEnvelopeEditor(); const [searchParams, setSearchParams] = useSearchParams(); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isStepLoading, setIsStepLoading] = useState(false); const [currentStep, setCurrentStep] = useState(() => { const searchParamStep = searchParams.get('step') as EnvelopeEditorStep | undefined; // Empty URL param equals upload, otherwise use the step URL param if (!searchParamStep) { return 'upload'; } const validSteps: EnvelopeEditorStep[] = ['upload', 'addFields', 'preview']; if (validSteps.includes(searchParamStep)) { return searchParamStep; } return 'upload'; }); const navigateToStep = (step: EnvelopeEditorStep) => { setCurrentStep(step); void flushAutosave(); if (!isStepLoading && isAutosaving) { setIsStepLoading(true); } // Update URL params: empty for upload, otherwise set the step if (step === 'upload') { setSearchParams((prev) => { const newParams = new URLSearchParams(prev); newParams.delete('step'); return newParams; }); } else { setSearchParams((prev) => { const newParams = new URLSearchParams(prev); newParams.set('step', step); return newParams; }); } }; // Watch the URL params and setStep if the step changes. useEffect(() => { const stepParam = searchParams.get('step') || envelopeEditorSteps[0].id; const foundStep = envelopeEditorSteps.find((step) => step.id === stepParam); if (foundStep && foundStep.id !== currentStep) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions navigateToStep(foundStep.id as EnvelopeEditorStep); } }, [searchParams]); useEffect(() => { if (!isAutosaving) { setIsStepLoading(false); } }, [isAutosaving]); const currentStepData = envelopeEditorSteps.find((step) => step.id === currentStep) || envelopeEditorSteps[0]; return (
{/* Main Content Area */}
{/* Left Section - Step Navigation */}
{/* Left section step selector. */}

{isDocument ? Document Editor : Template Editor} Step {currentStepData.order}/{envelopeEditorSteps.length}

{envelopeEditorSteps.map((step) => { const Icon = step.icon; const isActive = currentStep === step.id; return (
navigateToStep(step.id as EnvelopeEditorStep)} >
{t(step.title)}
{t(step.description)}
); })}
{/* Quick Actions. */}

Quick Actions

{isDocument ? Document Settings : Template Settings} } /> {isDocument && ( Send Document } /> )} {isDocument && ( Resend Document } /> )} {/* */} {isTemplate && ( Direct Link } /> )} {isDocument ? ( Duplicate Document ) : ( Duplicate Template )} } /> Download PDF } />
{isDocument ? ( { await navigate(relativePath.documentRootPath); }} /> ) : ( { await navigate(relativePath.templateRootPath); }} /> )} {/* Footer of left sidebar. */}
{/* Main Content - Changes based on current step */} {match({ currentStep, isStepLoading }) .with({ isStepLoading: true }, () => ) .with({ currentStep: 'upload' }, () => ) .with({ currentStep: 'addFields' }, () => ) .with({ currentStep: 'preview' }, () => ) .exhaustive()}
); }