mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 16:24:30 +10:00
Hides the reject dialog on the V2 signing page sidebar and mobile menu, the embed V1 signing page and the multi-sign embed view when the document's allowDocumentRejection is false. In embeds the reject option is shown only when both the embed's allowDocumentRejection option and the document setting allow it, and the embed V1 footer layout follows the same condition. Exposes the field in the multi-sign document response so the embed view can read it. Documents the relationship between the embed's allowDocumentRejection option and the document setting in the iframe and embedded editor docs, and in the API description of the field. Part of #2561
308 lines
13 KiB
TypeScript
308 lines
13 KiB
TypeScript
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
|
import { PDF_VIEWER_ERROR_MESSAGES } from '@documenso/lib/constants/pdf-viewer-i18n';
|
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
|
import { cn } from '@documenso/ui/lib/utils';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Separator } from '@documenso/ui/primitives/separator';
|
|
import { Plural, Trans, useLingui } from '@lingui/react/macro';
|
|
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
ArrowLeftIcon,
|
|
BanIcon,
|
|
DownloadCloudIcon,
|
|
PanelLeftCloseIcon,
|
|
PanelLeftOpenIcon,
|
|
PaperclipIcon,
|
|
} from 'lucide-react';
|
|
import { useMemo, useRef, useState } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
|
import { SignFieldCheckboxDialog } from '~/components/dialogs/sign-field-checkbox-dialog';
|
|
import { SignFieldDropdownDialog } from '~/components/dialogs/sign-field-dropdown-dialog';
|
|
import { SignFieldEmailDialog } from '~/components/dialogs/sign-field-email-dialog';
|
|
import { SignFieldInitialsDialog } from '~/components/dialogs/sign-field-initials-dialog';
|
|
import { SignFieldNameDialog } from '~/components/dialogs/sign-field-name-dialog';
|
|
import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-dialog';
|
|
import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signature-dialog';
|
|
import { SignFieldTextDialog } from '~/components/dialogs/sign-field-text-dialog';
|
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
|
import { EnvelopeSignerPageRenderer } from '~/components/general/envelope-signing/envelope-signer-page-renderer';
|
|
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
|
|
|
|
import { BrandingLogo } from '../branding-logo';
|
|
import { DocumentSigningAttachmentsPopover } from '../document-signing/document-signing-attachments-popover';
|
|
import { EnvelopeItemSelector } from '../envelope-editor/envelope-file-selector';
|
|
import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
|
|
import { EnvelopeSignerHeader } from '../envelope-signing/envelope-signer-header';
|
|
import { DocumentSigningMobileWidget } from './document-signing-mobile-widget';
|
|
import { DocumentSigningRejectDialog } from './document-signing-reject-dialog';
|
|
import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';
|
|
|
|
export const DocumentSigningPageViewV2 = () => {
|
|
const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
|
|
|
const scrollableContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const {
|
|
isDirectTemplate,
|
|
envelope,
|
|
recipient,
|
|
recipientFields,
|
|
recipientFieldsRemaining,
|
|
requiredRecipientFields,
|
|
selectedAssistantRecipientFields,
|
|
} = useRequiredEnvelopeSigningContext();
|
|
|
|
const {
|
|
isEmbed = false,
|
|
allowDocumentRejection = true,
|
|
hidePoweredBy = true,
|
|
onDocumentRejected,
|
|
} = useEmbedSigningContext() || {};
|
|
|
|
// Both the document settings and the embed host (if any) must allow rejection.
|
|
const isDocumentRejectionAllowed =
|
|
envelope.type === EnvelopeType.DOCUMENT && envelope.documentMeta.allowDocumentRejection && allowDocumentRejection;
|
|
|
|
const { t } = useLingui();
|
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
|
|
/**
|
|
* The total remaining fields remaining for the current recipient or selected assistant recipient.
|
|
*
|
|
* Includes both optional and required fields.
|
|
*/
|
|
const remainingFields = useMemo(() => {
|
|
if (recipient.role === RecipientRole.ASSISTANT) {
|
|
return selectedAssistantRecipientFields.filter((field) => !field.inserted);
|
|
}
|
|
|
|
return recipientFields.filter((field) => !field.inserted);
|
|
}, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]);
|
|
|
|
return (
|
|
<div className="min-h-screen w-screen bg-gray-50 dark:bg-background">
|
|
<SignFieldEmailDialog.Root />
|
|
<SignFieldTextDialog.Root />
|
|
<SignFieldNumberDialog.Root />
|
|
<SignFieldNameDialog.Root />
|
|
<SignFieldInitialsDialog.Root />
|
|
<SignFieldDropdownDialog.Root />
|
|
<SignFieldSignatureDialog.Root />
|
|
<SignFieldCheckboxDialog.Root />
|
|
|
|
<EnvelopeSignerHeader />
|
|
|
|
{/* Main Content Area */}
|
|
<div className="flex h-[calc(100vh-4rem)] w-screen">
|
|
{/* Left Section - Step Navigation */}
|
|
<div
|
|
className={cn(
|
|
'embed--DocumentWidgetContainer hidden flex-shrink-0 flex-col border-border border-r bg-background transition-[width] duration-300 lg:flex',
|
|
isSidebarCollapsed ? 'w-12' : 'w-80',
|
|
)}
|
|
>
|
|
{isSidebarCollapsed && (
|
|
<div className="flex justify-center pt-4">
|
|
<Button
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
aria-label={t`Expand sidebar`}
|
|
onClick={() => setIsSidebarCollapsed(false)}
|
|
>
|
|
<PanelLeftOpenIcon className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className={cn('flex flex-1 flex-col overflow-hidden py-4', isSidebarCollapsed && 'invisible w-0')}>
|
|
<div className="px-4">
|
|
<h3 className="flex items-end justify-between font-semibold text-foreground text-sm">
|
|
{match(recipient.role)
|
|
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
|
.with(RecipientRole.SIGNER, () => <Trans>Sign Document</Trans>)
|
|
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
|
|
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
|
|
.otherwise(() => null)}
|
|
|
|
<div className="ml-2 flex items-center gap-1">
|
|
<span className="rounded border bg-muted/50 px-2 py-0.5 text-muted-foreground text-xs">
|
|
<Plural
|
|
value={recipientFieldsRemaining.length}
|
|
one="1 Field Remaining"
|
|
other="# Fields Remaining"
|
|
/>
|
|
</span>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
className="h-7 w-7 p-0"
|
|
aria-label={t`Collapse sidebar`}
|
|
onClick={() => setIsSidebarCollapsed(true)}
|
|
>
|
|
<PanelLeftCloseIcon className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</h3>
|
|
|
|
<div className="relative my-4 h-[4px] rounded-md bg-muted">
|
|
<motion.div
|
|
layout="size"
|
|
layoutId="document-flow-container-step"
|
|
className="absolute inset-y-0 left-0 bg-primary"
|
|
style={{
|
|
width: `${requiredRecipientFields.length === 0 ? 100 : 100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="embed--DocumentWidgetContent mt-6 space-y-3">
|
|
<EnvelopeSignerForm />
|
|
</div>
|
|
</div>
|
|
|
|
<Separator className="my-6" />
|
|
|
|
{/* Quick Actions. */}
|
|
{!isDirectTemplate && (
|
|
<div className="embed--Actions space-y-3 px-4">
|
|
<h4 className="font-semibold text-foreground text-sm">
|
|
<Trans>Actions</Trans>
|
|
</h4>
|
|
|
|
<DocumentSigningAttachmentsPopover
|
|
envelopeId={envelope.id}
|
|
token={recipient.token}
|
|
trigger={
|
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
|
<PaperclipIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Attachments</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<EnvelopeDownloadDialog
|
|
envelopeId={envelope.id}
|
|
envelopeStatus={envelope.status}
|
|
envelopeItems={envelope.envelopeItems}
|
|
token={recipient.token}
|
|
trigger={
|
|
<Button variant="ghost" size="sm" className="w-full justify-start">
|
|
<DownloadCloudIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Download PDF</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{isDocumentRejectionAllowed && (
|
|
<DocumentSigningRejectDialog
|
|
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
|
token={recipient.token}
|
|
onRejected={
|
|
onDocumentRejected &&
|
|
((reason) =>
|
|
onDocumentRejected({
|
|
token: recipient.token,
|
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
envelopeId: envelope.id,
|
|
recipientId: recipient.id,
|
|
reason,
|
|
}))
|
|
}
|
|
trigger={
|
|
<Button variant="ghost" size="sm" className="w-full justify-start hover:text-destructive">
|
|
<BanIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Reject Document</Trans>
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="embed--DocumentWidgetFooter mt-auto">
|
|
{/* Footer of left sidebar. */}
|
|
{!isEmbed && (
|
|
<div className="px-4">
|
|
<Button asChild variant="ghost" className="w-full justify-start">
|
|
<Link to="/">
|
|
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Return</Trans>
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="embed--DocumentContainer min-w-0 flex-1 overflow-y-auto" ref={scrollableContainerRef}>
|
|
<div className="flex flex-col">
|
|
{/* Horizontal envelope item selector */}
|
|
{envelopeItems.length > 1 && (
|
|
<div className="flex h-fit space-x-2 overflow-x-auto p-2 pt-4 sm:p-4">
|
|
{envelopeItems.map((doc, i) => (
|
|
<EnvelopeItemSelector
|
|
key={doc.id}
|
|
number={i + 1}
|
|
primaryText={doc.title}
|
|
secondaryText={
|
|
<Plural
|
|
one="1 Field"
|
|
other="# Fields"
|
|
value={remainingFields.filter((field) => field.envelopeItemId === doc.id).length}
|
|
/>
|
|
}
|
|
isSelected={currentEnvelopeItem?.id === doc.id}
|
|
buttonProps={{ onClick: () => setCurrentEnvelopeItem(doc.id) }}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Document View */}
|
|
<div className="embed--DocumentViewer flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4">
|
|
{currentEnvelopeItem ? (
|
|
<EnvelopePdfViewer
|
|
key={currentEnvelopeItem.id}
|
|
customPageRenderer={EnvelopeSignerPageRenderer}
|
|
scrollParentRef={scrollableContainerRef}
|
|
errorMessage={PDF_VIEWER_ERROR_MESSAGES.signing}
|
|
/>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-32">
|
|
<p className="text-foreground text-sm">
|
|
<Trans>No document selected</Trans>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile widget - Additional padding to allow users to scroll */}
|
|
<div className="block pb-28 lg:hidden">
|
|
<DocumentSigningMobileWidget />
|
|
</div>
|
|
|
|
{!hidePoweredBy && (
|
|
<a
|
|
href="https://documenso.com"
|
|
target="_blank"
|
|
className="fixed right-0 bottom-0 z-40 hidden cursor-pointer rounded-tl bg-primary px-2 py-1 font-medium text-primary-foreground text-xs opacity-60 hover:opacity-100 lg:block"
|
|
rel="noopener"
|
|
>
|
|
<span>
|
|
<Trans>Powered by</Trans>
|
|
</span>
|
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|