feat: polish envelopes (#2090)

## Description

The rest of the owl
This commit is contained in:
David Nguyen
2025-10-24 16:22:06 +11:00
committed by GitHub
parent 88836404d1
commit 03eb6af69a
141 changed files with 5171 additions and 2402 deletions

View File

@ -24,7 +24,6 @@ import {
mapSecondaryIdToDocumentId,
mapSecondaryIdToTemplateId,
} from '@documenso/lib/utils/envelope';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
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';
@ -32,17 +31,17 @@ 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 { useCurrentTeam } from '~/providers/team';
import { EnvelopeEditorFieldsPage } from './envelope-editor-fields-page';
import EnvelopeEditorHeader from './envelope-editor-header';
import { EnvelopeEditorPageFields } from './envelope-editor-page-fields';
import { EnvelopeEditorPagePreview } from './envelope-editor-page-preview';
import { EnvelopeEditorPageUpload } from './envelope-editor-page-upload';
import { EnvelopeEditorPreviewPage } from './envelope-editor-preview-page';
import { EnvelopeEditorUploadPage } from './envelope-editor-upload-page';
type EnvelopeEditorStep = 'upload' | 'addFields' | 'preview';
@ -74,10 +73,17 @@ export default function EnvelopeEditor() {
const { t } = useLingui();
const navigate = useNavigate();
const team = useCurrentTeam();
const { envelope, isDocument, isTemplate, isAutosaving, flushAutosave } =
useCurrentEnvelopeEditor();
const {
envelope,
isDocument,
isTemplate,
isAutosaving,
flushAutosave,
relativePath,
syncEnvelope,
editorFields,
} = useCurrentEnvelopeEditor();
const [searchParams, setSearchParams] = useSearchParams();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -100,13 +106,10 @@ export default function EnvelopeEditor() {
return 'upload';
});
const documentsPath = formatDocumentsPath(team.url);
const templatesPath = formatTemplatesPath(team.url);
const navigateToStep = (step: EnvelopeEditorStep) => {
setCurrentStep(step);
flushAutosave();
void flushAutosave();
if (!isStepLoading && isAutosaving) {
setIsStepLoading(true);
@ -128,6 +131,18 @@ export default function EnvelopeEditor() {
}
};
// 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);
@ -138,20 +153,22 @@ export default function EnvelopeEditor() {
envelopeEditorSteps.find((step) => step.id === currentStep) || envelopeEditorSteps[0];
return (
<div className="h-screen w-screen bg-gray-50">
<div className="dark:bg-background h-screen w-screen bg-gray-50">
<EnvelopeEditorHeader />
{/* Main Content Area */}
<div className="flex h-[calc(100vh-73px)] w-screen">
{/* Left Section - Step Navigation */}
<div className="flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r border-gray-200 bg-white py-4">
<div className="bg-background border-border flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4">
{/* Left section step selector. */}
<div className="px-4">
<h3 className="flex items-end justify-between text-sm font-semibold text-gray-900">
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
<span className="text-muted-foreground ml-2 rounded border bg-gray-50 px-2 py-0.5 text-xs">
Step {currentStepData.order}/{envelopeEditorSteps.length}
<span className="text-muted-foreground bg-muted/50 ml-2 rounded border px-2 py-0.5 text-xs">
<Trans context="The step counter">
Step {currentStepData.order}/{envelopeEditorSteps.length}
</Trans>
</span>
</h3>
@ -176,15 +193,17 @@ export default function EnvelopeEditor() {
key={step.id}
className={`cursor-pointer rounded-lg p-3 transition-colors ${
isActive
? 'border border-green-200 bg-green-50'
: 'border border-gray-200 hover:bg-gray-50'
? 'border border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
: 'border border-gray-200 hover:bg-gray-50 dark:border-gray-400/20 dark:hover:bg-gray-400/10'
}`}
onClick={() => navigateToStep(step.id as EnvelopeEditorStep)}
>
<div className="flex items-center space-x-3">
<div
className={`rounded border p-2 ${
isActive ? 'border-green-200 bg-green-50' : 'border-gray-100 bg-gray-100'
isActive
? 'border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
: 'border-gray-100 bg-gray-100 dark:border-gray-400/20 dark:bg-gray-400/10'
}`}
>
<Icon
@ -194,12 +213,14 @@ export default function EnvelopeEditor() {
<div>
<div
className={`text-sm font-medium ${
isActive ? 'text-green-900' : 'text-gray-700'
isActive
? 'text-green-900 dark:text-green-400'
: 'text-foreground dark:text-muted-foreground'
}`}
>
{t(step.title)}
</div>
<div className="text-xs text-gray-500">{t(step.description)}</div>
<div className="text-muted-foreground text-xs">{t(step.description)}</div>
</div>
</div>
</div>
@ -212,12 +233,25 @@ export default function EnvelopeEditor() {
{/* Quick Actions. */}
<div className="space-y-3 px-4">
<h4 className="text-sm font-semibold text-gray-900">
<h4 className="text-foreground text-sm font-semibold">
<Trans>Quick Actions</Trans>
</h4>
<EnvelopeEditorSettingsDialog
trigger={
<Button variant="ghost" size="sm" className="w-full justify-start">
<SettingsIcon className="mr-2 h-4 w-4" />
{isDocument ? <Trans>Document Settings</Trans> : <Trans>Template Settings</Trans>}
</Button>
}
/>
{isDocument && (
<EnvelopeDistributeDialog
envelope={envelope}
envelope={{
...envelope,
fields: editorFields.localFields,
}}
onDistribute={syncEnvelope}
trigger={
<Button variant="ghost" size="sm" className="w-full justify-start">
<SendIcon className="mr-2 h-4 w-4" />
@ -239,16 +273,6 @@ export default function EnvelopeEditor() {
/>
)}
<EnvelopeEditorSettingsDialog
trigger={
<Button variant="ghost" size="sm" className="w-full justify-start">
<SettingsIcon className="mr-2 h-4 w-4" />
{isDocument ? <Trans>Document Settings</Trans> : <Trans>Template Settings</Trans>}
</Button>
}
/>
{/* Todo: Envelopes */}
{/* <Button variant="ghost" size="sm" className="w-full justify-start">
<FileText className="mr-2 h-4 w-4" />
Save as Template
@ -283,11 +307,17 @@ export default function EnvelopeEditor() {
}
/>
{/* Todo: Allow selecting which document to download and/or the original */}
<Button variant="ghost" size="sm" className="w-full justify-start">
<DownloadCloudIcon className="mr-2 h-4 w-4" />
<Trans>Download PDF</Trans>
</Button>
<EnvelopeDownloadDialog
envelopeId={envelope.id}
envelopeStatus={envelope.status}
envelopeItems={envelope.envelopeItems}
trigger={
<Button variant="ghost" size="sm" className="w-full justify-start">
<DownloadCloudIcon className="mr-2 h-4 w-4" />
<Trans>Download PDF</Trans>
</Button>
}
/>
<Button
variant="ghost"
@ -309,7 +339,7 @@ export default function EnvelopeEditor() {
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onDelete={async () => {
await navigate(documentsPath);
await navigate(relativePath.documentRootPath);
}}
/>
) : (
@ -318,7 +348,7 @@ export default function EnvelopeEditor() {
open={isDeleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onDelete={async () => {
await navigate(templatesPath);
await navigate(relativePath.templateRootPath);
}}
/>
)}
@ -326,7 +356,7 @@ export default function EnvelopeEditor() {
{/* Footer of left sidebar. */}
<div className="mt-auto px-4">
<Button variant="ghost" className="w-full justify-start" asChild>
<Link to={isDocument ? documentsPath : templatesPath}>
<Link to={relativePath.basePath}>
<ArrowLeftIcon className="mr-2 h-4 w-4" />
{isDocument ? (
<Trans>Return to documents</Trans>
@ -340,13 +370,12 @@ export default function EnvelopeEditor() {
{/* Main Content - Changes based on current step */}
<div className="flex-1 overflow-y-auto">
<p>{isAutosaving ? 'Autosaving...' : 'Not autosaving'}</p>
<AnimateGenericFadeInOut key={currentStep}>
{match({ currentStep, isStepLoading })
.with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />)
.with({ currentStep: 'upload' }, () => <EnvelopeEditorPageUpload />)
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorPageFields />)
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPagePreview />)
.with({ currentStep: 'upload' }, () => <EnvelopeEditorUploadPage />)
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorFieldsPage />)
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPreviewPage />)
.exhaustive()}
</AnimateGenericFadeInOut>
</div>