mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: multisign embedding (#1823)
Adds the ability to use a multisign embedding for cases where multiple documents need to be signed in a convenient manner.
This commit is contained in:
@ -332,7 +332,7 @@ export const EmbedDirectTemplateClientPage = ({
|
||||
{/* Widget */}
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
||||
|
||||
@ -290,7 +290,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
{/* Widget */}
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
|
||||
@ -0,0 +1,182 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { ReadStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { ArrowRight, EyeIcon, XCircle } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import type { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Progress } from '@documenso/ui/primitives/progress';
|
||||
|
||||
// Get the return type from getRecipientByToken
|
||||
type RecipientWithFields = Awaited<ReturnType<typeof getRecipientByToken>>;
|
||||
|
||||
interface DocumentEnvelope {
|
||||
document: DocumentAndSender;
|
||||
recipient: RecipientWithFields;
|
||||
}
|
||||
|
||||
interface MultiSignDocumentListProps {
|
||||
envelopes: DocumentEnvelope[];
|
||||
onDocumentSelect: (document: DocumentEnvelope['document']) => void;
|
||||
}
|
||||
|
||||
export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSignDocumentListProps) {
|
||||
// Calculate progress
|
||||
const completedDocuments = envelopes.filter(
|
||||
(envelope) => envelope.recipient.signingStatus === SigningStatus.SIGNED,
|
||||
);
|
||||
const totalDocuments = envelopes.length;
|
||||
const progressPercentage = (completedDocuments.length / totalDocuments) * 100;
|
||||
|
||||
// Find next document to sign (first one that's not signed and not rejected)
|
||||
const nextDocumentToSign = envelopes.find(
|
||||
(envelope) =>
|
||||
envelope.recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||
envelope.recipient.signingStatus !== SigningStatus.REJECTED,
|
||||
);
|
||||
|
||||
const allDocumentsCompleted = completedDocuments.length === totalDocuments;
|
||||
|
||||
const hasAssistantOrCcRecipient = envelopes.some(
|
||||
(envelope) =>
|
||||
envelope.recipient.role === RecipientRole.ASSISTANT ||
|
||||
envelope.recipient.role === RecipientRole.CC,
|
||||
);
|
||||
|
||||
function handleView(doc: DocumentEnvelope['document']) {
|
||||
onDocumentSelect(doc);
|
||||
}
|
||||
|
||||
function handleNextDocument() {
|
||||
if (nextDocumentToSign) {
|
||||
onDocumentSelect(nextDocumentToSign.document);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAssistantOrCcRecipient) {
|
||||
return (
|
||||
<div className="mx-auto mt-16 flex w-full max-w-lg flex-col md:mt-16 md:rounded-2xl md:border md:px-8 md:py-16 md:shadow-lg">
|
||||
<div className="flex items-center justify-center">
|
||||
<XCircle className="text-destructive h-16 w-16 md:h-24 md:w-24" strokeWidth={1.2} />
|
||||
</div>
|
||||
|
||||
<h2 className="mt-12 text-xl font-bold md:text-2xl">
|
||||
<Trans>It looks like we ran into an issue!</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mt-6">
|
||||
<Trans>
|
||||
One of the documents in the current bundle has a signing role that is not compatible
|
||||
with the current signing experience.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<Trans>
|
||||
Assistants and Copy roles are currently not compatible with the multi-sign experience.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2">
|
||||
<Trans>Please contact the site owner for further assistance.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background mx-auto w-full max-w-lg md:my-12 md:rounded-2xl md:border md:p-8 md:shadow-lg">
|
||||
<h2 className="text-foreground mb-1 text-lg font-semibold">
|
||||
<Trans>Sign Documents</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
You have been requested to sign the following documents. Review each document carefully
|
||||
and complete the signing process.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
{/* Progress Section */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground font-medium">
|
||||
<Trans>Progress</Trans>
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{completedDocuments.length} of {totalDocuments} completed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Progress value={progressPercentage} className="h-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4">
|
||||
{envelopes.map((envelope) => (
|
||||
<div
|
||||
key={envelope.document.id}
|
||||
className="border-border flex items-center gap-4 rounded-lg border px-4 py-2"
|
||||
>
|
||||
<span className="text-foreground flex-1 truncate text-sm font-medium">
|
||||
{envelope.document.title}
|
||||
</span>
|
||||
|
||||
{match(envelope.recipient)
|
||||
.with({ signingStatus: SigningStatus.SIGNED }, () => (
|
||||
<Badge size="small" variant="default">
|
||||
<Trans>Completed</Trans>
|
||||
</Badge>
|
||||
))
|
||||
.with({ signingStatus: SigningStatus.REJECTED }, () => (
|
||||
<Badge size="small" variant="destructive">
|
||||
<Trans>Rejected</Trans>
|
||||
</Badge>
|
||||
))
|
||||
.with({ readStatus: ReadStatus.OPENED }, () => (
|
||||
<Badge size="small" variant="neutral">
|
||||
<Trans>Viewed</Trans>
|
||||
</Badge>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
|
||||
<Button
|
||||
className="-mr-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleView(envelope.document)}
|
||||
>
|
||||
<EyeIcon className="mr-1 h-4 w-4" />
|
||||
<Trans>View</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Next Document Button */}
|
||||
{!allDocumentsCompleted && nextDocumentToSign && (
|
||||
<div className="mt-6">
|
||||
<Button onClick={handleNextDocument} className="w-full" size="lg">
|
||||
<Trans>View next document</Trans>
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allDocumentsCompleted && (
|
||||
<Alert className="mt-6 text-center">
|
||||
<AlertTitle>
|
||||
<Trans>All documents have been completed!</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Trans>Thank you for completing the signing process.</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,394 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client';
|
||||
import { Loader, LucideChevronDown, LucideChevronUp, X } from 'lucide-react';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type {
|
||||
TRemovedSignedFieldWithTokenMutationSchema,
|
||||
TSignFieldWithTokenMutationSchema,
|
||||
} from '@documenso/trpc/server/field-router/schema';
|
||||
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import PDFViewer from '@documenso/ui/primitives/pdf-viewer';
|
||||
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
||||
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
||||
import { EmbedDocumentFields } from '../embed-document-fields';
|
||||
|
||||
interface MultiSignDocumentSigningViewProps {
|
||||
token: string;
|
||||
recipientId: number;
|
||||
onBack: () => void;
|
||||
onDocumentCompleted?: (data: { token: string; documentId: number; recipientId: number }) => void;
|
||||
onDocumentRejected?: (data: {
|
||||
token: string;
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
reason: string;
|
||||
}) => void;
|
||||
onDocumentError?: () => void;
|
||||
onDocumentReady?: () => void;
|
||||
isNameLocked?: boolean;
|
||||
allowDocumentRejection?: boolean;
|
||||
}
|
||||
|
||||
export const MultiSignDocumentSigningView = ({
|
||||
token,
|
||||
recipientId,
|
||||
onBack,
|
||||
onDocumentCompleted,
|
||||
onDocumentRejected,
|
||||
onDocumentError,
|
||||
onDocumentReady,
|
||||
isNameLocked = false,
|
||||
allowDocumentRejection = false,
|
||||
}: MultiSignDocumentSigningViewProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { fullName, email, signature, setFullName, setSignature } =
|
||||
useRequiredDocumentSigningContext();
|
||||
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const { data: document, isLoading } = trpc.embeddingPresign.getMultiSignDocument.useQuery(
|
||||
{ token },
|
||||
{
|
||||
staleTime: 0,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
|
||||
const { mutateAsync: removeSignedFieldWithToken } =
|
||||
trpc.field.removeSignedFieldWithToken.useMutation();
|
||||
|
||||
const { mutateAsync: completeDocumentWithToken } =
|
||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||
|
||||
const hasSignatureField = document?.fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||
|
||||
const [pendingFields, completedFields] = [
|
||||
document?.fields.filter((field) => field.recipient.signingStatus !== SigningStatus.SIGNED) ??
|
||||
[],
|
||||
document?.fields.filter((field) => field.recipient.signingStatus === SigningStatus.SIGNED) ??
|
||||
[],
|
||||
];
|
||||
|
||||
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
|
||||
try {
|
||||
await signFieldWithToken(payload);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
|
||||
toast({
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while signing the document.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUnsignField = async (payload: TRemovedSignedFieldWithTokenMutationSchema) => {
|
||||
try {
|
||||
await removeSignedFieldWithToken(payload);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentComplete = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
await completeDocumentWithToken({
|
||||
documentId: document!.id,
|
||||
token,
|
||||
});
|
||||
|
||||
onBack();
|
||||
|
||||
onDocumentCompleted?.({
|
||||
token,
|
||||
documentId: document!.id,
|
||||
recipientId,
|
||||
});
|
||||
} catch (err) {
|
||||
onDocumentError?.();
|
||||
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to complete the document. Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onNextFieldClick = () => {
|
||||
setShowPendingFieldTooltip(true);
|
||||
|
||||
setIsExpanded(false);
|
||||
};
|
||||
|
||||
const onRejected = (reason: string) => {
|
||||
if (onDocumentRejected && document) {
|
||||
onDocumentRejected({
|
||||
token,
|
||||
documentId: document.id,
|
||||
recipientId,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background min-h-screen overflow-hidden">
|
||||
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
|
||||
{match({ isLoading, document })
|
||||
.with({ isLoading: true }, () => (
|
||||
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader className="text-primary h-8 w-8 animate-spin" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>Loading document...</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
.with({ isLoading: false, document: undefined }, () => (
|
||||
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>Failed to load document</Trans>
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
.with({ document: P.nonNullable }, ({ document }) => (
|
||||
<>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl items-baseline justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-2xl font-semibold">{document.title}</h1>
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" size="sm" onClick={onBack} className="p-2">
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{allowDocumentRejection && (
|
||||
<div className="embed--Actions mb-4 mt-8 flex w-full flex-row-reverse items-baseline justify-between">
|
||||
<DocumentSigningRejectDialog
|
||||
document={document}
|
||||
token={token}
|
||||
onRejected={onRejected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="embed--DocumentContainer relative mx-auto mt-8 flex w-full max-w-screen-xl flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||
<div
|
||||
className={cn('embed--DocumentViewer flex-1', {
|
||||
'md:mx-auto md:max-w-2xl': document.status === DocumentStatus.COMPLETED,
|
||||
})}
|
||||
>
|
||||
<PDFViewer
|
||||
documentData={document.documentData}
|
||||
onDocumentLoad={() => {
|
||||
setHasDocumentLoaded(true);
|
||||
onDocumentReady?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Widget */}
|
||||
{document.status !== DocumentStatus.COMPLETED && (
|
||||
<div
|
||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-0 md:z-auto md:w-[350px] md:px-0"
|
||||
data-expanded={isExpanded || undefined}
|
||||
>
|
||||
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||
{/* Header */}
|
||||
<div className="embed--DocumentWidgetHeader">
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||
<Trans>Sign document</Trans>
|
||||
</h3>
|
||||
|
||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||
{isExpanded ? (
|
||||
<LucideChevronDown
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<LucideChevronUp
|
||||
className="text-muted-foreground h-5 w-5"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
<Trans>Sign the document to complete the process.</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="embed--DocumentWidgetForm -mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
{
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="full-name">
|
||||
<Trans>Full Name</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="full-name"
|
||||
className="bg-background mt-2"
|
||||
disabled={isNameLocked}
|
||||
value={fullName}
|
||||
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">
|
||||
<Trans>Email</Trans>
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="bg-background mt-2"
|
||||
value={email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSignatureField && (
|
||||
<div>
|
||||
<Label htmlFor="Signature">
|
||||
<Trans>Signature</Trans>
|
||||
</Label>
|
||||
|
||||
<SignaturePadDialog
|
||||
className="mt-2"
|
||||
disabled={isSubmitting}
|
||||
disableAnimation
|
||||
value={signature ?? ''}
|
||||
onChange={(v) => setSignature(v ?? '')}
|
||||
typedSignatureEnabled={
|
||||
document.documentMeta?.typedSignatureEnabled
|
||||
}
|
||||
uploadSignatureEnabled={
|
||||
document.documentMeta?.uploadSignatureEnabled
|
||||
}
|
||||
drawSignatureEnabled={
|
||||
document.documentMeta?.drawSignatureEnabled
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||
|
||||
<div className="embed--DocumentWidgetFooter mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||
{pendingFields.length > 0 ? (
|
||||
<Button className="col-start-2" onClick={onNextFieldClick}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="col-span-2"
|
||||
loading={isSubmitting}
|
||||
onClick={onDocumentComplete}
|
||||
>
|
||||
<Trans>Complete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasDocumentLoaded && (
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||
<FieldToolTip
|
||||
key={pendingFields[0].id}
|
||||
field={pendingFields[0]}
|
||||
color="warning"
|
||||
>
|
||||
<Trans>Click to insert field</Trans>
|
||||
</FieldToolTip>
|
||||
)}
|
||||
</ElementVisible>
|
||||
)}
|
||||
|
||||
{/* Fields */}
|
||||
{hasDocumentLoaded && (
|
||||
<EmbedDocumentFields
|
||||
fields={pendingFields}
|
||||
metadata={document.documentMeta}
|
||||
onSignField={onSignField}
|
||||
onUnsignField={onUnsignField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Completed fields */}
|
||||
{document.status !== DocumentStatus.COMPLETED && (
|
||||
<DocumentReadOnlyFields
|
||||
documentMeta={document.documentMeta ?? undefined}
|
||||
fields={completedFields}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user