feat: assistant role (#1588)
Introduces the ability for users with the **Assistant** role to prefill fields on behalf of other signers. Assistants can fill in various field types such as text, checkboxes, dates, and more, streamlining the document preparation process before it reaches the final signers.
This commit is contained in:
committed by
David Nguyen
parent
3e106c1a2d
commit
c0ae68c28b
@@ -0,0 +1,73 @@
|
|||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
|
type ConfirmationDialogProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
hasUninsertedFields: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AssistantConfirmationDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
hasUninsertedFields,
|
||||||
|
isSubmitting,
|
||||||
|
}: ConfirmationDialogProps) {
|
||||||
|
const onOpenChange = () => {
|
||||||
|
if (isSubmitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Complete Document</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
Are you sure you want to complete the document? This action cannot be undone. Please
|
||||||
|
ensure that you have completed prefilling all relevant fields before proceeding.
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<DocumentSigningDisclosure />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Submitting...' : hasUninsertedFields ? 'Proceed' : 'Continue'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export const DocumentMoveDialog = ({ documentId, open, onOpenChange }: DocumentM
|
|||||||
|
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
const [selectedTeamId, setSelectedTeamId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
const { data: teams, isPending: isLoadingTeams } = trpc.team.getTeams.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
const { mutateAsync: moveDocument, isPending } = trpc.document.moveDocumentToTeam.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const TeamTransferDialog = ({
|
|||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
refetch: refetchTeamMembers,
|
refetch: refetchTeamMembers,
|
||||||
isLoading: loadingTeamMembers,
|
isPending: loadingTeamMembers,
|
||||||
isLoadingError: loadingTeamMembersError,
|
isLoadingError: loadingTeamMembersError,
|
||||||
} = trpc.team.getTeamMembers.useQuery({
|
} = trpc.team.getTeamMembers.useQuery({
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validDirectTemplateRecipients = useMemo(
|
const validDirectTemplateRecipients = useMemo(
|
||||||
() => template.recipients.filter((recipient) => recipient.role !== RecipientRole.CC),
|
() =>
|
||||||
|
template.recipients.filter(
|
||||||
|
(recipient) =>
|
||||||
|
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||||
|
),
|
||||||
[template.recipients],
|
[template.recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -483,7 +483,6 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
|
|
||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<EmbedDocumentFields
|
<EmbedDocumentFields
|
||||||
recipient={recipient}
|
|
||||||
fields={localFields}
|
fields={localFields}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@prisma/client';
|
||||||
import { type Field, FieldType } from '@prisma/client';
|
import { type Field, FieldType } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ import { DocumentSigningSignatureField } from '~/components/general/document-sig
|
|||||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
export type EmbedDocumentFieldsProps = {
|
export type EmbedDocumentFieldsProps = {
|
||||||
recipient: Recipient;
|
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
@@ -39,7 +38,6 @@ export type EmbedDocumentFieldsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDocumentFields = ({
|
export const EmbedDocumentFields = ({
|
||||||
recipient,
|
|
||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
onSignField,
|
onSignField,
|
||||||
@@ -53,7 +51,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningSignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
typedSignatureEnabled={metadata?.typedSignatureEnabled}
|
||||||
@@ -63,7 +60,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningInitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -72,7 +68,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningNameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -81,7 +76,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningDateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
dateFormat={metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
@@ -92,7 +86,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningEmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -107,7 +100,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningTextField
|
<DocumentSigningTextField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -123,7 +115,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningNumberField
|
<DocumentSigningNumberField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -139,7 +130,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningRadioField
|
<DocumentSigningRadioField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -155,7 +145,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningCheckboxField
|
<DocumentSigningCheckboxField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -171,7 +160,6 @@ export const EmbedDocumentFields = ({
|
|||||||
<DocumentSigningDropdownField
|
<DocumentSigningDropdownField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={fieldWithMeta}
|
field={fieldWithMeta}
|
||||||
recipient={recipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
import { useEffect, useId, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentMeta, Recipient, TemplateMeta } from '@prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@prisma/client';
|
||||||
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
import { type DocumentData, type Field, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -18,6 +19,7 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ import { injectCss } from '~/utils/css-vars';
|
|||||||
|
|
||||||
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema';
|
||||||
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
|
import { DocumentSigningRecipientProvider } from '../general/document-signing/document-signing-recipient-provider';
|
||||||
import { EmbedClientLoading } from './embed-client-loading';
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
import { EmbedDocumentCompleted } from './embed-document-completed';
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
import { EmbedDocumentFields } from './embed-document-fields';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
@@ -34,12 +37,13 @@ export type EmbedSignDocumentClientPageProps = {
|
|||||||
token: string;
|
token: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
documentData: DocumentData;
|
documentData: DocumentData;
|
||||||
recipient: Recipient;
|
recipient: RecipientWithFields;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | TemplateMeta | null;
|
metadata?: DocumentMeta | TemplateMeta | null;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
hidePoweredBy?: boolean;
|
hidePoweredBy?: boolean;
|
||||||
isPlatformOrEnterprise?: boolean;
|
isPlatformOrEnterprise?: boolean;
|
||||||
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedSignDocumentClientPage = ({
|
export const EmbedSignDocumentClientPage = ({
|
||||||
@@ -52,6 +56,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
isCompleted,
|
isCompleted,
|
||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
isPlatformOrEnterprise = false,
|
isPlatformOrEnterprise = false,
|
||||||
|
allRecipients = [],
|
||||||
}: EmbedSignDocumentClientPageProps) => {
|
}: EmbedSignDocumentClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -69,17 +74,21 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||||
|
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||||
|
);
|
||||||
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
|
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500);
|
||||||
|
|
||||||
const [pendingFields, _completedFields] = [
|
const [pendingFields, _completedFields] = [
|
||||||
fields.filter((field) => !field.inserted),
|
fields.filter((field) => field.recipientId === recipient.id && !field.inserted),
|
||||||
fields.filter((field) => field.inserted),
|
fields.filter((field) => field.inserted),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -88,6 +97,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const assistantSignersId = useId();
|
||||||
|
|
||||||
const onNextFieldClick = () => {
|
const onNextFieldClick = () => {
|
||||||
validateFieldsInserted(fields);
|
validateFieldsInserted(fields);
|
||||||
|
|
||||||
@@ -213,6 +224,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
||||||
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
@@ -236,7 +248,11 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-x-2">
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
|
{isAssistantMode ? (
|
||||||
|
<Trans>Assist with signing</Trans>
|
||||||
|
) : (
|
||||||
<Trans>Sign document</Trans>
|
<Trans>Sign document</Trans>
|
||||||
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
@@ -257,7 +273,11 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
|
|
||||||
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
<div className="hidden group-data-[expanded]/document-widget:block md:block">
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
{isAssistantMode ? (
|
||||||
|
<Trans>Help complete the document for other signers.</Trans>
|
||||||
|
) : (
|
||||||
<Trans>Sign the document to complete the process.</Trans>
|
<Trans>Sign the document to complete the process.</Trans>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
@@ -266,6 +286,62 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
|
{isAssistantMode && (
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Signing for</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<fieldset className="dark:bg-background border-border mt-2 rounded-2xl border bg-white p-3">
|
||||||
|
<RadioGroup
|
||||||
|
className="gap-0 space-y-3 shadow-none"
|
||||||
|
value={selectedSignerId?.toString()}
|
||||||
|
onValueChange={(value) => setSelectedSignerId(Number(value))}
|
||||||
|
>
|
||||||
|
{allRecipients
|
||||||
|
.filter((r) => r.fields.length > 0)
|
||||||
|
.map((r) => (
|
||||||
|
<div
|
||||||
|
key={`${assistantSignersId}-${r.id}`}
|
||||||
|
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem
|
||||||
|
id={`${assistantSignersId}-${r.id}`}
|
||||||
|
value={r.id.toString()}
|
||||||
|
className="after:absolute after:inset-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grow gap-1">
|
||||||
|
<Label
|
||||||
|
className="inline-flex items-start"
|
||||||
|
htmlFor={`${assistantSignersId}-${r.id}`}
|
||||||
|
>
|
||||||
|
{r.name}
|
||||||
|
|
||||||
|
{r.id === recipient.id && (
|
||||||
|
<span className="text-muted-foreground ml-2">
|
||||||
|
{_(msg`(You)`)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<p className="text-muted-foreground text-xs">{r.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs leading-[inherit]">
|
||||||
|
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isAssistantMode && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">
|
<Label htmlFor="full-name">
|
||||||
<Trans>Full Name</Trans>
|
<Trans>Full Name</Trans>
|
||||||
@@ -329,6 +405,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -342,7 +420,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className="col-start-2"
|
className="col-start-2"
|
||||||
disabled={isThrottled || (hasSignatureField && !signatureValid)}
|
disabled={
|
||||||
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
|
}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
onClick={() => throttledOnCompleteClick()}
|
onClick={() => throttledOnCompleteClick()}
|
||||||
>
|
>
|
||||||
@@ -362,7 +442,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</ElementVisible>
|
</ElementVisible>
|
||||||
|
|
||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
|
<EmbedDocumentFields fields={fields} metadata={metadata} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!hidePoweredBy && (
|
{!hidePoweredBy && (
|
||||||
@@ -372,5 +452,6 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentSigningRecipientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
|
||||||
|
export const EmbedDocumentWaitingForTurn = () => {
|
||||||
|
const [hasPostedMessage, setHasPostedMessage] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.parent && !hasPostedMessage) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-waiting-for-turn',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasPostedMessage(true);
|
||||||
|
}, [hasPostedMessage]);
|
||||||
|
|
||||||
|
if (!hasPostedMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
|
<h3 className="text-foreground text-center text-2xl font-bold">
|
||||||
|
<Trans>Waiting for Your Turn</Trans>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mt-8 max-w-[50ch] text-center">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
<Trans>
|
||||||
|
It's currently not your turn to sign. Please check back soon as this document should be
|
||||||
|
available for you to sign shortly.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-4 text-sm">
|
||||||
|
<Trans>Please check with the parent application for more information.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -80,7 +80,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data: searchDocumentsData, isLoading: isSearchingDocuments } =
|
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
||||||
trpcReact.document.searchDocuments.useQuery(
|
trpcReact.document.searchDocuments.useQuery(
|
||||||
{
|
{
|
||||||
query: search,
|
query: search,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import { DocumentSigningRadioField } from '~/components/general/document-signing
|
|||||||
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
|
||||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
|
|
||||||
|
import { DocumentSigningRecipientProvider } from '../document-signing/document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DirectTemplateSigningFormProps = {
|
export type DirectTemplateSigningFormProps = {
|
||||||
flowStep: DocumentFlowStep;
|
flowStep: DocumentFlowStep;
|
||||||
directRecipient: Recipient;
|
directRecipient: Recipient;
|
||||||
@@ -169,7 +171,7 @@ export const DirectTemplateSigningForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocumentSigningRecipientProvider recipient={directRecipient}>
|
||||||
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
|
||||||
|
|
||||||
<DocumentFlowFormContainerContent>
|
<DocumentFlowFormContainerContent>
|
||||||
@@ -186,7 +188,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentSigningSignatureField
|
<DocumentSigningSignatureField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -195,7 +196,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentSigningInitialsField
|
<DocumentSigningInitialsField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -204,7 +204,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentSigningNameField
|
<DocumentSigningNameField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -213,7 +212,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentSigningDateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
|
||||||
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
dateFormat={template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
timezone={template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
@@ -224,7 +222,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
<DocumentSigningEmailField
|
<DocumentSigningEmailField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -241,7 +238,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -259,7 +255,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -277,7 +272,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -295,7 +289,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -313,7 +306,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
...field,
|
...field,
|
||||||
fieldMeta: parsedFieldMeta,
|
fieldMeta: parsedFieldMeta,
|
||||||
}}
|
}}
|
||||||
recipient={directRecipient}
|
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
/>
|
/>
|
||||||
@@ -383,6 +375,6 @@ export const DirectTemplateSigningForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</>
|
</DocumentSigningRecipientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -25,17 +24,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningCheckboxFieldProps = {
|
export type DocumentSigningCheckboxFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningCheckboxField = ({
|
export const DocumentSigningCheckboxField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningCheckboxFieldProps) => {
|
}: DocumentSigningCheckboxFieldProps) => {
|
||||||
@@ -43,6 +41,8 @@ export const DocumentSigningCheckboxField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
|
||||||
@@ -118,7 +118,9 @@ export const DocumentSigningCheckboxField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,7 +149,7 @@ export const DocumentSigningCheckboxField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -24,10 +23,10 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningDateFieldProps = {
|
export type DocumentSigningDateFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
dateFormat?: string | null;
|
dateFormat?: string | null;
|
||||||
timezone?: string | null;
|
timezone?: string | null;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
@@ -36,7 +35,6 @@ export type DocumentSigningDateFieldProps = {
|
|||||||
|
|
||||||
export const DocumentSigningDateField = ({
|
export const DocumentSigningDateField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
timezone = DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
onSignField,
|
onSignField,
|
||||||
@@ -46,6 +44,8 @@ export const DocumentSigningDateField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
@@ -60,9 +60,7 @@ export const DocumentSigningDateField = ({
|
|||||||
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||||
|
|
||||||
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
const isDifferentTime = field.inserted && localDateString !== field.customText;
|
||||||
|
|
||||||
const tooltipText = _(
|
const tooltipText = _(
|
||||||
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone || ''}".`,
|
msg`"${field.customText}" will appear on the document as it has a timezone of "${timezone || ''}".`,
|
||||||
);
|
);
|
||||||
@@ -95,7 +93,9 @@ export const DocumentSigningDateField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ export const DocumentSigningDateField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -28,17 +27,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningDropdownFieldProps = {
|
export type DocumentSigningDropdownFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningDropdownField = ({
|
export const DocumentSigningDropdownField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningDropdownFieldProps) => {
|
}: DocumentSigningDropdownFieldProps) => {
|
||||||
@@ -46,6 +44,8 @@ export const DocumentSigningDropdownField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||||
@@ -99,7 +99,9 @@ export const DocumentSigningDropdownField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,7 +133,7 @@ export const DocumentSigningDropdownField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -20,17 +19,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningEmailFieldProps = {
|
export type DocumentSigningEmailFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningEmailField = ({
|
export const DocumentSigningEmailField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningEmailFieldProps) => {
|
}: DocumentSigningEmailFieldProps) => {
|
||||||
@@ -40,6 +38,8 @@ export const DocumentSigningEmailField = ({
|
|||||||
|
|
||||||
const { email: providedEmail } = useRequiredDocumentSigningContext();
|
const { email: providedEmail } = useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
|
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
@@ -84,7 +84,9 @@ export const DocumentSigningEmailField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ export const DocumentSigningEmailField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type DocumentSigningFieldContainerProps = {
|
|||||||
| 'Email'
|
| 'Email'
|
||||||
| 'Name'
|
| 'Name'
|
||||||
| 'Signature'
|
| 'Signature'
|
||||||
|
| 'Text'
|
||||||
| 'Radio'
|
| 'Radio'
|
||||||
| 'Dropdown'
|
| 'Dropdown'
|
||||||
| 'Number'
|
| 'Number'
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useId, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
@@ -11,6 +13,7 @@ import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-
|
|||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -18,8 +21,11 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { AssistantConfirmationDialog } from '../../dialogs/assistant-confirmation-dialog';
|
||||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
|
||||||
@@ -29,6 +35,8 @@ export type DocumentSigningFormProps = {
|
|||||||
fields: Field[];
|
fields: Field[];
|
||||||
redirectUrl?: string | null;
|
redirectUrl?: string | null;
|
||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
|
allRecipients?: RecipientWithFields[];
|
||||||
|
setSelectedSignerId?: (id: number | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningForm = ({
|
export const DocumentSigningForm = ({
|
||||||
@@ -37,20 +45,35 @@ export const DocumentSigningForm = ({
|
|||||||
fields,
|
fields,
|
||||||
redirectUrl,
|
redirectUrl,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
|
allRecipients = [],
|
||||||
|
setSelectedSignerId,
|
||||||
}: DocumentSigningFormProps) => {
|
}: DocumentSigningFormProps) => {
|
||||||
|
const { user } = useOptionalSession();
|
||||||
|
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
|
|
||||||
const { user } = useOptionalSession();
|
const assistantSignersId = useId();
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
|
||||||
useRequiredDocumentSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||||
|
const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: completeDocumentWithToken } =
|
const { mutateAsync: completeDocumentWithToken } =
|
||||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
|
const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
|
||||||
|
defaultValues: {
|
||||||
|
selectedSignerId: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { handleSubmit, formState } = useForm();
|
const { handleSubmit, formState } = useForm();
|
||||||
|
|
||||||
// Keep the loading state going if successful since the redirect may take some time.
|
// Keep the loading state going if successful since the redirect may take some time.
|
||||||
@@ -65,7 +88,11 @@ export const DocumentSigningForm = ({
|
|||||||
|
|
||||||
const uninsertedFields = useMemo(() => {
|
const uninsertedFields = useMemo(() => {
|
||||||
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
|
return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
|
||||||
}, [fields]);
|
}, [fieldsRequiringValidation]);
|
||||||
|
|
||||||
|
const uninsertedRecipientFields = useMemo(() => {
|
||||||
|
return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
|
||||||
|
}, [fieldsRequiringValidation, recipient]);
|
||||||
|
|
||||||
const fieldsValidated = () => {
|
const fieldsValidated = () => {
|
||||||
setValidateUninsertedFields(true);
|
setValidateUninsertedFields(true);
|
||||||
@@ -86,12 +113,31 @@ export const DocumentSigningForm = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await completeDocument();
|
await completeDocument();
|
||||||
|
};
|
||||||
|
|
||||||
// Reauth is currently not required for completing the document.
|
const onAssistantFormSubmit = () => {
|
||||||
// await executeActionAuthProcedure({
|
if (uninsertedRecipientFields.length > 0) {
|
||||||
// onReauthFormSubmit: completeDocument,
|
return;
|
||||||
// actionTarget: 'DOCUMENT',
|
}
|
||||||
// });
|
|
||||||
|
setIsConfirmationDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssistantConfirmDialogSubmit = async () => {
|
||||||
|
setIsAssistantSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await completeDocument();
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while completing the document. Please try again.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsAssistantSubmitting(false);
|
||||||
|
setIsConfirmationDialogOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||||
@@ -115,7 +161,7 @@ export const DocumentSigningForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||||
{
|
{
|
||||||
@@ -123,7 +169,6 @@ export const DocumentSigningForm = ({
|
|||||||
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
|
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !user,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
>
|
>
|
||||||
{validateUninsertedFields && uninsertedFields[0] && (
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
@@ -131,17 +176,13 @@ export const DocumentSigningForm = ({
|
|||||||
</FieldToolTip>
|
</FieldToolTip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<fieldset
|
<div className="custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2">
|
||||||
disabled={isSubmitting}
|
<div className="flex flex-1 flex-col">
|
||||||
className={cn(
|
|
||||||
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn('flex flex-1 flex-col')}>
|
|
||||||
<h3 className="text-foreground text-2xl font-semibold">
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
|
{recipient.role === RecipientRole.VIEWER && <Trans>View Document</Trans>}
|
||||||
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
|
{recipient.role === RecipientRole.SIGNER && <Trans>Sign Document</Trans>}
|
||||||
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
|
{recipient.role === RecipientRole.APPROVER && <Trans>Approve Document</Trans>}
|
||||||
|
{recipient.role === RecipientRole.ASSISTANT && <Trans>Assist Document</Trans>}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{recipient.role === RecipientRole.VIEWER ? (
|
{recipient.role === RecipientRole.VIEWER ? (
|
||||||
@@ -178,15 +219,108 @@ export const DocumentSigningForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : recipient.role === RecipientRole.ASSISTANT ? (
|
||||||
|
<>
|
||||||
|
<form onSubmit={assistantForm.handleSubmit(onAssistantFormSubmit)}>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>
|
||||||
|
Complete the fields for the following signers. Once reviewed, they will inform
|
||||||
|
you if any modifications are needed.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-border my-4" />
|
||||||
|
|
||||||
|
<fieldset className="dark:bg-background border-border rounded-2xl border bg-white p-3">
|
||||||
|
<Controller
|
||||||
|
name="selectedSignerId"
|
||||||
|
control={assistantForm.control}
|
||||||
|
rules={{ required: 'Please select a signer' }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<RadioGroup
|
||||||
|
className="gap-0 space-y-3 shadow-none"
|
||||||
|
value={field.value?.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
setSelectedSignerId?.(Number(value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{allRecipients
|
||||||
|
.filter((r) => r.fields.length > 0)
|
||||||
|
.map((r) => (
|
||||||
|
<div
|
||||||
|
key={`${assistantSignersId}-${r.id}`}
|
||||||
|
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem
|
||||||
|
id={`${assistantSignersId}-${r.id}`}
|
||||||
|
value={r.id.toString()}
|
||||||
|
className="after:absolute after:inset-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grow gap-1">
|
||||||
|
<Label
|
||||||
|
className="inline-flex items-start"
|
||||||
|
htmlFor={`${assistantSignersId}-${r.id}`}
|
||||||
|
>
|
||||||
|
{r.name}
|
||||||
|
|
||||||
|
{r.id === recipient.id && (
|
||||||
|
<span className="text-muted-foreground ml-2">
|
||||||
|
{_(msg`(You)`)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<p className="text-muted-foreground text-xs">{r.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs leading-[inherit]">
|
||||||
|
{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
|
loading={isAssistantSubmitting}
|
||||||
|
disabled={isAssistantSubmitting || uninsertedRecipientFields.length > 0}
|
||||||
|
>
|
||||||
|
{isAssistantSubmitting ? <Trans>Submitting...</Trans> : <Trans>Continue</Trans>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AssistantConfirmationDialog
|
||||||
|
hasUninsertedFields={uninsertedFields.length > 0}
|
||||||
|
isOpen={isConfirmationDialogOpen}
|
||||||
|
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
|
||||||
|
onConfirm={handleAssistantConfirmDialogSubmit}
|
||||||
|
isSubmitting={isAssistantSubmitting}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
<Trans>Please review the document before signing.</Trans>
|
<Trans>Please review the document before signing.</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-border mb-8 mt-4" />
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
|
||||||
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
<fieldset
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2"
|
||||||
|
>
|
||||||
<div className="flex flex-1 flex-col gap-y-4">
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="full-name">
|
<Label htmlFor="full-name">
|
||||||
@@ -258,11 +392,12 @@ export const DocumentSigningForm = ({
|
|||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
|
</form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -19,17 +18,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningInitialsFieldProps = {
|
export type DocumentSigningInitialsFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningInitialsField = ({
|
export const DocumentSigningInitialsField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningInitialsFieldProps) => {
|
}: DocumentSigningInitialsFieldProps) => {
|
||||||
@@ -38,6 +36,8 @@ export const DocumentSigningInitialsField = ({
|
|||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const { fullName } = useRequiredDocumentSigningContext();
|
const { fullName } = useRequiredDocumentSigningContext();
|
||||||
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const initials = extractInitials(fullName);
|
const initials = extractInitials(fullName);
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
@@ -81,7 +81,9 @@ export const DocumentSigningInitialsField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { type Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -27,17 +26,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningNameFieldProps = {
|
export type DocumentSigningNameFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningNameField = ({
|
export const DocumentSigningNameField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningNameFieldProps) => {
|
}: DocumentSigningNameFieldProps) => {
|
||||||
@@ -48,6 +46,8 @@ export const DocumentSigningNameField = ({
|
|||||||
const { fullName: providedFullName, setFullName: setProvidedFullName } =
|
const { fullName: providedFullName, setFullName: setProvidedFullName } =
|
||||||
useRequiredDocumentSigningContext();
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
@@ -67,7 +67,7 @@ export const DocumentSigningNameField = ({
|
|||||||
const [localFullName, setLocalFullName] = useState('');
|
const [localFullName, setLocalFullName] = useState('');
|
||||||
|
|
||||||
const onPreSign = () => {
|
const onPreSign = () => {
|
||||||
if (!providedFullName) {
|
if (!providedFullName && !isAssistantMode) {
|
||||||
setShowFullNameModal(true);
|
setShowFullNameModal(true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -90,9 +90,9 @@ export const DocumentSigningNameField = ({
|
|||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
|
const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
|
||||||
try {
|
try {
|
||||||
const value = name || providedFullName;
|
const value = name || providedFullName || '';
|
||||||
|
|
||||||
if (!value) {
|
if (!value && !isAssistantMode) {
|
||||||
setShowFullNameModal(true);
|
setShowFullNameModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,9 @@ export const DocumentSigningNameField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -150,7 +152,7 @@ export const DocumentSigningNameField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Hash, Loader } from 'lucide-react';
|
import { Hash, Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
type ValidationErrors = {
|
type ValidationErrors = {
|
||||||
isNumber: string[];
|
isNumber: string[];
|
||||||
@@ -37,14 +37,12 @@ type ValidationErrors = {
|
|||||||
|
|
||||||
export type DocumentSigningNumberFieldProps = {
|
export type DocumentSigningNumberFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningNumberField = ({
|
export const DocumentSigningNumberField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningNumberFieldProps) => {
|
}: DocumentSigningNumberFieldProps) => {
|
||||||
@@ -52,7 +50,9 @@ export const DocumentSigningNumberField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [showRadioModal, setShowRadioModal] = useState(false);
|
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
|
const [showNumberModal, setShowNumberModal] = useState(false);
|
||||||
|
|
||||||
const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta);
|
const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta);
|
||||||
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
@@ -107,7 +107,7 @@ export const DocumentSigningNumberField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDialogSignClick = () => {
|
const onDialogSignClick = () => {
|
||||||
setShowRadioModal(false);
|
setShowNumberModal(false);
|
||||||
|
|
||||||
void executeActionAuthProcedure({
|
void executeActionAuthProcedure({
|
||||||
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
||||||
@@ -150,14 +150,20 @@ export const DocumentSigningNumberField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPreSign = () => {
|
const onPreSign = () => {
|
||||||
setShowRadioModal(true);
|
if (isAssistantMode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowNumberModal(true);
|
||||||
|
|
||||||
if (localNumber && parsedFieldMeta) {
|
if (localNumber && parsedFieldMeta) {
|
||||||
const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
|
const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
|
||||||
@@ -175,8 +181,14 @@ export const DocumentSigningNumberField = ({
|
|||||||
|
|
||||||
const onRemove = async () => {
|
const onRemove = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (isAssistantMode && !targetSigner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
||||||
|
|
||||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||||
token: recipient.token,
|
token: signingRecipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,18 +207,18 @@ export const DocumentSigningNumberField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showRadioModal) {
|
if (!showNumberModal) {
|
||||||
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
|
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
|
||||||
setErrors(initialErrors);
|
setErrors(initialErrors);
|
||||||
}
|
}
|
||||||
}, [showRadioModal]);
|
}, [showNumberModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -237,7 +249,7 @@ export const DocumentSigningNumberField = ({
|
|||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
type="Signature"
|
type="Number"
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
@@ -280,7 +292,7 @@ export const DocumentSigningNumberField = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
<Dialog open={showNumberModal} onOpenChange={setShowNumberModal}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
|
{parsedFieldMeta?.label ? parsedFieldMeta?.label : <Trans>Number</Trans>}
|
||||||
@@ -336,7 +348,7 @@ export const DocumentSigningNumberField = ({
|
|||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRadioModal(false);
|
setShowNumberModal(false);
|
||||||
setLocalNumber('');
|
setLocalNumber('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field } from '@prisma/client';
|
||||||
import { FieldType, RecipientRole } from '@prisma/client';
|
import { FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
import type { CompletedField } from '@documenso/lib/types/fields';
|
||||||
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
|
||||||
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
@@ -35,12 +38,15 @@ import { DocumentSigningSignatureField } from '~/components/general/document-sig
|
|||||||
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
|
||||||
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
|
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
|
||||||
|
|
||||||
|
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type SigningPageViewProps = {
|
export type SigningPageViewProps = {
|
||||||
document: DocumentAndSender;
|
document: DocumentAndSender;
|
||||||
recipient: Recipient;
|
recipient: RecipientWithFields;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
completedFields: CompletedField[];
|
completedFields: CompletedField[];
|
||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningPageView = ({
|
export const DocumentSigningPageView = ({
|
||||||
@@ -49,9 +55,12 @@ export const DocumentSigningPageView = ({
|
|||||||
fields,
|
fields,
|
||||||
completedFields,
|
completedFields,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
|
allRecipients = [],
|
||||||
}: SigningPageViewProps) => {
|
}: SigningPageViewProps) => {
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||||
|
|
||||||
const shouldUseTeamDetails =
|
const shouldUseTeamDetails =
|
||||||
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
|
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
|
||||||
|
|
||||||
@@ -63,7 +72,10 @@ export const DocumentSigningPageView = ({
|
|||||||
senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
|
senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DocumentSigningRecipientProvider recipient={recipient} targetSigner={selectedSigner ?? null}>
|
||||||
<div className="mx-auto w-full max-w-screen-xl">
|
<div className="mx-auto w-full max-w-screen-xl">
|
||||||
<h1
|
<h1
|
||||||
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
|
||||||
@@ -106,6 +118,15 @@ export const DocumentSigningPageView = ({
|
|||||||
<Trans>has invited you to approve this document</Trans>
|
<Trans>has invited you to approve this document</Trans>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.with(RecipientRole.ASSISTANT, () =>
|
||||||
|
document.teamId && !shouldUseTeamDetails ? (
|
||||||
|
<Trans>
|
||||||
|
on behalf of "{document.team?.name}" has invited you to assist this document
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>has invited you to assist this document</Trans>
|
||||||
|
),
|
||||||
|
)
|
||||||
.otherwise(() => null)}
|
.otherwise(() => null)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,111 +156,86 @@ export const DocumentSigningPageView = ({
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
redirectUrl={documentMeta?.redirectUrl}
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
|
allRecipients={allRecipients}
|
||||||
|
setSelectedSignerId={setSelectedSignerId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DocumentReadOnlyFields fields={completedFields} />
|
<DocumentReadOnlyFields fields={completedFields} />
|
||||||
|
|
||||||
|
{recipient.role !== RecipientRole.ASSISTANT && (
|
||||||
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
|
<DocumentSigningAutoSign recipient={recipient} fields={fields} />
|
||||||
|
)}
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
{fields.map((field) =>
|
{fields
|
||||||
|
.filter(
|
||||||
|
(field) =>
|
||||||
|
recipient.role !== RecipientRole.ASSISTANT ||
|
||||||
|
field.recipientId === selectedSigner?.id,
|
||||||
|
)
|
||||||
|
.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<DocumentSigningSignatureField
|
<DocumentSigningSignatureField key={field.id} field={field} />
|
||||||
key={field.id}
|
|
||||||
field={field}
|
|
||||||
recipient={recipient}
|
|
||||||
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
<DocumentSigningInitialsField key={field.id} field={field} recipient={recipient} />
|
<DocumentSigningInitialsField key={field.id} field={field} />
|
||||||
))
|
))
|
||||||
.with(FieldType.NAME, () => (
|
.with(FieldType.NAME, () => (
|
||||||
<DocumentSigningNameField key={field.id} field={field} recipient={recipient} />
|
<DocumentSigningNameField key={field.id} field={field} />
|
||||||
))
|
))
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DocumentSigningDateField
|
<DocumentSigningDateField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
field={field}
|
field={field}
|
||||||
recipient={recipient}
|
|
||||||
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
dateFormat={documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT}
|
||||||
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
timezone={documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.EMAIL, () => (
|
.with(FieldType.EMAIL, () => (
|
||||||
<DocumentSigningEmailField key={field.id} field={field} recipient={recipient} />
|
<DocumentSigningEmailField key={field.id} field={field} />
|
||||||
))
|
))
|
||||||
.with(FieldType.TEXT, () => {
|
.with(FieldType.TEXT, () => {
|
||||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
|
fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null,
|
||||||
};
|
};
|
||||||
return (
|
return <DocumentSigningTextField key={field.id} field={fieldWithMeta} />;
|
||||||
<DocumentSigningTextField
|
|
||||||
key={field.id}
|
|
||||||
field={fieldWithMeta}
|
|
||||||
recipient={recipient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.with(FieldType.NUMBER, () => {
|
.with(FieldType.NUMBER, () => {
|
||||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
|
fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null,
|
||||||
};
|
};
|
||||||
return (
|
return <DocumentSigningNumberField key={field.id} field={fieldWithMeta} />;
|
||||||
<DocumentSigningNumberField
|
|
||||||
key={field.id}
|
|
||||||
field={fieldWithMeta}
|
|
||||||
recipient={recipient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.with(FieldType.RADIO, () => {
|
.with(FieldType.RADIO, () => {
|
||||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
|
fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null,
|
||||||
};
|
};
|
||||||
return (
|
return <DocumentSigningRadioField key={field.id} field={fieldWithMeta} />;
|
||||||
<DocumentSigningRadioField
|
|
||||||
key={field.id}
|
|
||||||
field={fieldWithMeta}
|
|
||||||
recipient={recipient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.with(FieldType.CHECKBOX, () => {
|
.with(FieldType.CHECKBOX, () => {
|
||||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
|
fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null,
|
||||||
};
|
};
|
||||||
return (
|
return <DocumentSigningCheckboxField key={field.id} field={fieldWithMeta} />;
|
||||||
<DocumentSigningCheckboxField
|
|
||||||
key={field.id}
|
|
||||||
field={fieldWithMeta}
|
|
||||||
recipient={recipient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.with(FieldType.DROPDOWN, () => {
|
.with(FieldType.DROPDOWN, () => {
|
||||||
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
const fieldWithMeta: FieldWithSignatureAndFieldMeta = {
|
||||||
...field,
|
...field,
|
||||||
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
|
fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null,
|
||||||
};
|
};
|
||||||
return (
|
return <DocumentSigningDropdownField key={field.id} field={fieldWithMeta} />;
|
||||||
<DocumentSigningDropdownField
|
|
||||||
key={field.id}
|
|
||||||
field={fieldWithMeta}
|
|
||||||
recipient={recipient}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.otherwise(() => null),
|
.otherwise(() => null),
|
||||||
)}
|
)}
|
||||||
</ElementVisible>
|
</ElementVisible>
|
||||||
</div>
|
</div>
|
||||||
|
</DocumentSigningRecipientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -22,17 +21,16 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningRadioFieldProps = {
|
export type DocumentSigningRadioFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningRadioField = ({
|
export const DocumentSigningRadioField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningRadioFieldProps) => {
|
}: DocumentSigningRadioFieldProps) => {
|
||||||
@@ -40,6 +38,8 @@ export const DocumentSigningRadioField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
const parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||||
const values = parsedFieldMeta.values?.map((item) => ({
|
const values = parsedFieldMeta.values?.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -68,16 +68,26 @@ export const DocumentSigningRadioField = ({
|
|||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
|
if (isAssistantMode && !targetSigner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
||||||
|
|
||||||
const payload: TSignFieldWithTokenMutationSchema = {
|
const payload: TSignFieldWithTokenMutationSchema = {
|
||||||
token: recipient.token,
|
token: signingRecipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: selectedOption,
|
value: selectedOption,
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
authOptions,
|
authOptions,
|
||||||
|
...(isAssistantMode && {
|
||||||
|
isAssistantPrefill: true,
|
||||||
|
assistantId: recipient.id,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onSignField) {
|
if (onSignField) {
|
||||||
@@ -100,7 +110,9 @@ export const DocumentSigningRadioField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,7 +139,7 @@ export const DocumentSigningRadioField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the signature.`),
|
description: _(msg`An error occurred while removing the selection.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { type PropsWithChildren, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import type { Recipient } from '@prisma/client';
|
||||||
|
|
||||||
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
|
|
||||||
|
export interface DocumentSigningRecipientContextValue {
|
||||||
|
/**
|
||||||
|
* The recipient who is currently signing the document.
|
||||||
|
* In regular mode, this is the actual signer.
|
||||||
|
* In assistant mode, this is the recipient who is helping fill out the document.
|
||||||
|
*/
|
||||||
|
recipient: Recipient | RecipientWithFields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only present in assistant mode.
|
||||||
|
* The recipient on whose behalf we're filling out the document.
|
||||||
|
*/
|
||||||
|
targetSigner: RecipientWithFields | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we're in assistant mode (one recipient filling out for another)
|
||||||
|
*/
|
||||||
|
isAssistantMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentSigningRecipientContext = createContext<DocumentSigningRecipientContextValue | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface DocumentSigningRecipientProviderProps extends PropsWithChildren {
|
||||||
|
recipient: Recipient | RecipientWithFields;
|
||||||
|
targetSigner?: RecipientWithFields | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentSigningRecipientProvider = ({
|
||||||
|
children,
|
||||||
|
recipient,
|
||||||
|
targetSigner = null,
|
||||||
|
}: DocumentSigningRecipientProviderProps) => {
|
||||||
|
// console.log({
|
||||||
|
// recipient,
|
||||||
|
// targetSigner,
|
||||||
|
// isAssistantMode: !!targetSigner,
|
||||||
|
// });
|
||||||
|
return (
|
||||||
|
<DocumentSigningRecipientContext.Provider
|
||||||
|
value={{
|
||||||
|
recipient,
|
||||||
|
targetSigner,
|
||||||
|
isAssistantMode: !!targetSigner,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DocumentSigningRecipientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDocumentSigningRecipientContext() {
|
||||||
|
const context = useContext(DocumentSigningRecipientContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDocumentSigningRecipientContext must be used within a RecipientProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { type Recipient } from '@prisma/client';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -27,11 +26,11 @@ import { DocumentSigningDisclosure } from '~/components/general/document-signing
|
|||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
|
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
|
||||||
export type DocumentSigningSignatureFieldProps = {
|
export type DocumentSigningSignatureFieldProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
recipient: Recipient;
|
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
@@ -39,7 +38,6 @@ export type DocumentSigningSignatureFieldProps = {
|
|||||||
|
|
||||||
export const DocumentSigningSignatureField = ({
|
export const DocumentSigningSignatureField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
@@ -48,6 +46,8 @@ export const DocumentSigningSignatureField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
|
const { recipient } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
const signatureRef = useRef<HTMLParagraphElement>(null);
|
const signatureRef = useRef<HTMLParagraphElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [fontSize, setFontSize] = useState(2);
|
const [fontSize, setFontSize] = useState(2);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Plural, Trans } from '@lingui/react/macro';
|
import { Plural, Trans } from '@lingui/react/macro';
|
||||||
import type { Recipient } from '@prisma/client';
|
|
||||||
import { Loader, Type } from 'lucide-react';
|
import { Loader, Type } from 'lucide-react';
|
||||||
import { useRevalidator } from 'react-router';
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
@@ -26,17 +25,27 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
import { DocumentSigningFieldContainer } from './document-signing-field-container';
|
||||||
|
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
|
||||||
|
|
||||||
export type DocumentSigningTextFieldProps = {
|
export type DocumentSigningTextFieldProps = {
|
||||||
field: FieldWithSignatureAndFieldMeta;
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
recipient: Recipient;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidationErrors = {
|
||||||
|
required: string[];
|
||||||
|
characterLimit: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextFieldProps = {
|
||||||
|
field: FieldWithSignatureAndFieldMeta;
|
||||||
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentSigningTextField = ({
|
export const DocumentSigningTextField = ({
|
||||||
field,
|
field,
|
||||||
recipient,
|
|
||||||
onSignField,
|
onSignField,
|
||||||
onUnsignField,
|
onUnsignField,
|
||||||
}: DocumentSigningTextFieldProps) => {
|
}: DocumentSigningTextFieldProps) => {
|
||||||
@@ -44,11 +53,12 @@ export const DocumentSigningTextField = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { revalidate } = useRevalidator();
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const initialErrors: Record<string, string[]> = {
|
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
|
||||||
|
|
||||||
|
const initialErrors: ValidationErrors = {
|
||||||
required: [],
|
required: [],
|
||||||
characterLimit: [],
|
characterLimit: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const [errors, setErrors] = useState(initialErrors);
|
const [errors, setErrors] = useState(initialErrors);
|
||||||
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
|
||||||
|
|
||||||
@@ -166,7 +176,9 @@ export const DocumentSigningTextField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while signing the document.`),
|
description: isAssistantMode
|
||||||
|
? _(msg`An error occurred while signing as assistant.`)
|
||||||
|
: _(msg`An error occurred while signing the document.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -194,7 +206,7 @@ export const DocumentSigningTextField = ({
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(msg`Error`),
|
||||||
description: _(msg`An error occurred while removing the text.`),
|
description: _(msg`An error occurred while removing the field.`),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -234,7 +246,7 @@ export const DocumentSigningTextField = ({
|
|||||||
onPreSign={onPreSign}
|
onPreSign={onPreSign}
|
||||||
onSign={onSign}
|
onSign={onSign}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
type="Signature"
|
type="Text"
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
|
||||||
|
|||||||
@@ -351,6 +351,16 @@ export const DocumentHistorySheet = ({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, ({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Field prefilled',
|
||||||
|
value: formatGenericText(data.field.type),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
|
|
||||||
{isUserDetailsVisible && (
|
{isUserDetailsVisible && (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MailOpenIcon,
|
MailOpenIcon,
|
||||||
PenIcon,
|
PenIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
UserIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@@ -118,6 +119,12 @@ export const DocumentPageViewRecipients = ({
|
|||||||
<Trans>Viewed</Trans>
|
<Trans>Viewed</Trans>
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
|
.with(RecipientRole.ASSISTANT, () => (
|
||||||
|
<>
|
||||||
|
<UserIcon className="mr-1 h-3 w-3" />
|
||||||
|
<Trans>Assisted</Trans>
|
||||||
|
</>
|
||||||
|
))
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { Clock8 } from 'lucide-react';
|
import { Clock8 } from 'lucide-react';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||||
@@ -14,6 +14,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
|||||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
|
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
@@ -37,14 +38,14 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
|
|
||||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
const [document, recipient, fields, completedFields] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
requireAccessAuth: false,
|
requireAccessAuth: false,
|
||||||
}).catch(() => null),
|
}).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
getFieldsForToken({ token }),
|
||||||
getCompletedFieldsForToken({ token }),
|
getCompletedFieldsForToken({ token }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -57,12 +58,21 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
throw new Response('Not Found', { status: 404 });
|
throw new Response('Not Found', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientWithFields = { ...recipient, fields };
|
||||||
|
|
||||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
||||||
|
|
||||||
if (!isRecipientsTurn) {
|
if (!isRecipientsTurn) {
|
||||||
throw redirect(`/sign/${token}/waiting`);
|
throw redirect(`/sign/${token}/waiting`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allRecipients =
|
||||||
|
recipient.role === RecipientRole.ASSISTANT
|
||||||
|
? await getRecipientsForAssistant({
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
documentAuth: document.authOptions,
|
documentAuth: document.authOptions,
|
||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
@@ -133,6 +143,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
document,
|
document,
|
||||||
fields,
|
fields,
|
||||||
recipient,
|
recipient,
|
||||||
|
recipientWithFields,
|
||||||
|
allRecipients,
|
||||||
completedFields,
|
completedFields,
|
||||||
recipientSignature,
|
recipientSignature,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
@@ -153,8 +165,16 @@ export default function SigningPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { document, fields, recipient, completedFields, recipientSignature, isRecipientsTurn } =
|
const {
|
||||||
data;
|
document,
|
||||||
|
fields,
|
||||||
|
recipient,
|
||||||
|
completedFields,
|
||||||
|
recipientSignature,
|
||||||
|
isRecipientsTurn,
|
||||||
|
allRecipients,
|
||||||
|
recipientWithFields,
|
||||||
|
} = data;
|
||||||
|
|
||||||
if (document.deletedAt) {
|
if (document.deletedAt) {
|
||||||
return (
|
return (
|
||||||
@@ -218,11 +238,12 @@ export default function SigningPage() {
|
|||||||
user={user}
|
user={user}
|
||||||
>
|
>
|
||||||
<DocumentSigningPageView
|
<DocumentSigningPageView
|
||||||
recipient={recipient}
|
recipient={recipientWithFields}
|
||||||
document={document}
|
document={document}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
completedFields={completedFields}
|
completedFields={completedFields}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
|
allRecipients={allRecipients}
|
||||||
/>
|
/>
|
||||||
</DocumentSigningAuthProvider>
|
</DocumentSigningAuthProvider>
|
||||||
</DocumentSigningProvider>
|
</DocumentSigningProvider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
|
import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
|
||||||
|
|
||||||
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
|
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
|
||||||
|
import { EmbedDocumentWaitingForTurn } from '~/components/embed/embed-document-waiting-for-turn';
|
||||||
import { EmbedPaywall } from '~/components/embed/embed-paywall';
|
import { EmbedPaywall } from '~/components/embed/embed-paywall';
|
||||||
|
|
||||||
import type { Route } from './+types/_layout';
|
import type { Route } from './+types/_layout';
|
||||||
@@ -36,6 +37,10 @@ export function ErrorBoundary() {
|
|||||||
if (error.status === 403 && error.data.type === 'embed-paywall') {
|
if (error.status === 403 && error.data.type === 'embed-paywall') {
|
||||||
return <EmbedPaywall />;
|
return <EmbedPaywall />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.status === 403 && error.data.type === 'embed-waiting-for-turn') {
|
||||||
|
return <EmbedDocumentWaitingForTurn />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>Not Found</div>;
|
return <div>Not Found</div>;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|||||||
import { EmbedDirectTemplateClientPage } from '~/components/embed/embed-direct-template-client-page';
|
import { EmbedDirectTemplateClientPage } from '~/components/embed/embed-direct-template-client-page';
|
||||||
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
import { DocumentSigningRecipientProvider } from '~/components/general/document-signing/document-signing-recipient-provider';
|
||||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||||
|
|
||||||
import type { Route } from './+types/direct.$url';
|
import type { Route } from './+types/direct.$url';
|
||||||
@@ -129,6 +130,7 @@ export default function EmbedDirectTemplatePage() {
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
user={user}
|
user={user}
|
||||||
>
|
>
|
||||||
|
<DocumentSigningRecipientProvider recipient={recipient}>
|
||||||
<EmbedDirectTemplateClientPage
|
<EmbedDirectTemplateClientPage
|
||||||
token={token}
|
token={token}
|
||||||
updatedAt={template.updatedAt}
|
updatedAt={template.updatedAt}
|
||||||
@@ -139,6 +141,7 @@ export default function EmbedDirectTemplatePage() {
|
|||||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
/>
|
/>
|
||||||
|
</DocumentSigningRecipientProvider>
|
||||||
</DocumentSigningAuthProvider>
|
</DocumentSigningAuthProvider>
|
||||||
</DocumentSigningProvider>
|
</DocumentSigningProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||||
import { data } from 'react-router';
|
import { data } from 'react-router';
|
||||||
import { getLoaderSession } from 'server/utils/get-loader-session';
|
import { getLoaderSession } from 'server/utils/get-loader-session';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@@ -8,7 +8,9 @@ import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-p
|
|||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
|
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||||
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
@@ -89,6 +91,26 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
|
||||||
|
|
||||||
|
if (!isRecipientsTurnToSign) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-waiting-for-turn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRecipients =
|
||||||
|
recipient.role === RecipientRole.ASSISTANT
|
||||||
|
? await getRecipientsForAssistant({
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
const team = document.teamId
|
const team = document.teamId
|
||||||
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
|
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
@@ -99,6 +121,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
document,
|
document,
|
||||||
|
allRecipients,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
hidePoweredBy,
|
hidePoweredBy,
|
||||||
@@ -112,6 +135,7 @@ export default function EmbedSignDocumentPage() {
|
|||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
document,
|
document,
|
||||||
|
allRecipients,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
hidePoweredBy,
|
hidePoweredBy,
|
||||||
@@ -140,6 +164,7 @@ export default function EmbedSignDocumentPage() {
|
|||||||
isCompleted={document.status === DocumentStatus.COMPLETED}
|
isCompleted={document.status === DocumentStatus.COMPLETED}
|
||||||
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
|
||||||
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
|
||||||
|
allRecipients={allRecipients}
|
||||||
/>
|
/>
|
||||||
</DocumentSigningAuthProvider>
|
</DocumentSigningAuthProvider>
|
||||||
</DocumentSigningProvider>
|
</DocumentSigningProvider>
|
||||||
|
|||||||
@@ -533,12 +533,19 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
if (i > 1) {
|
if (i > 1) {
|
||||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder('Email')
|
.getByLabel('Email')
|
||||||
|
.nth(i - 1)
|
||||||
|
.focus();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByLabel('Email')
|
||||||
.nth(i - 1)
|
.nth(i - 1)
|
||||||
.fill(`user${i}@example.com`);
|
.fill(`user${i}@example.com`);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByPlaceholder('Name')
|
.getByLabel('Name')
|
||||||
.nth(i - 1)
|
.nth(i - 1)
|
||||||
.fill(`User ${i}`);
|
.fill(`User ${i}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export const TemplateDocumentInvite = ({
|
|||||||
.with(RecipientRole.VIEWER, () => <Trans>Continue by viewing the document.</Trans>)
|
.with(RecipientRole.VIEWER, () => <Trans>Continue by viewing the document.</Trans>)
|
||||||
.with(RecipientRole.APPROVER, () => <Trans>Continue by approving the document.</Trans>)
|
.with(RecipientRole.APPROVER, () => <Trans>Continue by approving the document.</Trans>)
|
||||||
.with(RecipientRole.CC, () => '')
|
.with(RecipientRole.CC, () => '')
|
||||||
|
.with(RecipientRole.ASSISTANT, () => (
|
||||||
|
<Trans>Continue by assisting with the document.</Trans>
|
||||||
|
))
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -104,6 +107,7 @@ export const TemplateDocumentInvite = ({
|
|||||||
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
.with(RecipientRole.VIEWER, () => <Trans>View Document</Trans>)
|
||||||
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
|
.with(RecipientRole.APPROVER, () => <Trans>Approve Document</Trans>)
|
||||||
.with(RecipientRole.CC, () => '')
|
.with(RecipientRole.CC, () => '')
|
||||||
|
.with(RecipientRole.ASSISTANT, () => <Trans>Assist Document</Trans>)
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const DOCUMENT_AUDIT_LOG_EMAIL_FORMAT = {
|
|||||||
[DOCUMENT_EMAIL_TYPE.APPROVE_REQUEST]: {
|
[DOCUMENT_EMAIL_TYPE.APPROVE_REQUEST]: {
|
||||||
description: 'Approval request',
|
description: 'Approval request',
|
||||||
},
|
},
|
||||||
|
[DOCUMENT_EMAIL_TYPE.ASSISTING_REQUEST]: {
|
||||||
|
description: 'Assisting request',
|
||||||
|
},
|
||||||
[DOCUMENT_EMAIL_TYPE.CC]: {
|
[DOCUMENT_EMAIL_TYPE.CC]: {
|
||||||
description: 'CC',
|
description: 'CC',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,12 +31,26 @@ export const RECIPIENT_ROLES_DESCRIPTION = {
|
|||||||
roleName: msg`Viewer`,
|
roleName: msg`Viewer`,
|
||||||
roleNamePlural: msg`Viewers`,
|
roleNamePlural: msg`Viewers`,
|
||||||
},
|
},
|
||||||
|
[RecipientRole.ASSISTANT]: {
|
||||||
|
actionVerb: msg`Assist`,
|
||||||
|
actioned: msg`Assisted`,
|
||||||
|
progressiveVerb: msg`Assisting`,
|
||||||
|
roleName: msg`Assistant`,
|
||||||
|
roleNamePlural: msg`Assistants`,
|
||||||
|
},
|
||||||
} satisfies Record<keyof typeof RecipientRole, unknown>;
|
} satisfies Record<keyof typeof RecipientRole, unknown>;
|
||||||
|
|
||||||
|
export const RECIPIENT_ROLE_TO_DISPLAY_TYPE = {
|
||||||
|
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
|
||||||
|
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
|
||||||
|
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
export const RECIPIENT_ROLE_TO_EMAIL_TYPE = {
|
||||||
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
|
[RecipientRole.SIGNER]: `SIGNING_REQUEST`,
|
||||||
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
|
[RecipientRole.VIEWER]: `VIEW_REQUEST`,
|
||||||
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
|
[RecipientRole.APPROVER]: `APPROVE_REQUEST`,
|
||||||
|
[RecipientRole.ASSISTANT]: `ASSISTING_REQUEST`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
||||||
@@ -44,4 +58,5 @@ export const RECIPIENT_ROLE_SIGNING_REASONS = {
|
|||||||
[RecipientRole.APPROVER]: msg`I am an approver of this document`,
|
[RecipientRole.APPROVER]: msg`I am an approver of this document`,
|
||||||
[RecipientRole.CC]: msg`I am required to receive a copy of this document`,
|
[RecipientRole.CC]: msg`I am required to receive a copy of this document`,
|
||||||
[RecipientRole.VIEWER]: msg`I am a viewer of this document`,
|
[RecipientRole.VIEWER]: msg`I am a viewer of this document`,
|
||||||
|
[RecipientRole.ASSISTANT]: msg`I am an assistant of this document`,
|
||||||
} satisfies Record<keyof typeof RecipientRole, MessageDescriptor>;
|
} satisfies Record<keyof typeof RecipientRole, MessageDescriptor>;
|
||||||
|
|||||||
@@ -1,15 +1,55 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { FieldType, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export type GetFieldsForTokenOptions = {
|
export type GetFieldsForTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Missing token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient = await prisma.recipient.findFirst({
|
||||||
|
where: { token },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||||
return await prisma.field.findMany({
|
return await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
recipient: {
|
OR: [
|
||||||
token,
|
{
|
||||||
|
type: {
|
||||||
|
not: FieldType.SIGNATURE,
|
||||||
},
|
},
|
||||||
|
recipient: {
|
||||||
|
signingStatus: {
|
||||||
|
not: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
signingOrder: {
|
||||||
|
gte: recipient.signingOrder ?? 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documentId: recipient.documentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
signature: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
recipientId: recipient.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
signature: true,
|
signature: true,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentStatus, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
@@ -16,11 +16,28 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
fieldId,
|
fieldId,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: RemovedSignedFieldWithTokenOptions) => {
|
}: RemovedSignedFieldWithTokenOptions) => {
|
||||||
|
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const field = await prisma.field.findFirstOrThrow({
|
const field = await prisma.field.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: fieldId,
|
id: fieldId,
|
||||||
recipient: {
|
recipient: {
|
||||||
token,
|
...(recipient.role !== RecipientRole.ASSISTANT
|
||||||
|
? {
|
||||||
|
id: recipient.id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
signingOrder: {
|
||||||
|
gte: recipient.signingOrder ?? 0,
|
||||||
|
},
|
||||||
|
signingStatus: {
|
||||||
|
not: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -29,7 +46,7 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { document, recipient } = field;
|
const { document } = field;
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error(`Document not found for field ${field.id}`);
|
throw new Error(`Document not found for field ${field.id}`);
|
||||||
@@ -39,7 +56,10 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
throw new Error(`Document ${document.id} must be pending`);
|
throw new Error(`Document ${document.id} must be pending`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
if (
|
||||||
|
recipient?.signingStatus === SigningStatus.SIGNED ||
|
||||||
|
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||||
|
) {
|
||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +85,14 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (recipient.role !== RecipientRole.ASSISTANT) {
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient?.name,
|
name: recipient.name,
|
||||||
email: recipient?.email,
|
email: recipient.email,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
@@ -80,5 +101,6 @@ export const removeSignedFieldWithToken = async ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -54,20 +54,41 @@ export const signFieldWithToken = async ({
|
|||||||
authOptions,
|
authOptions,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: SignFieldWithTokenOptions) => {
|
}: SignFieldWithTokenOptions) => {
|
||||||
|
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const field = await prisma.field.findFirstOrThrow({
|
const field = await prisma.field.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: fieldId,
|
id: fieldId,
|
||||||
recipient: {
|
recipient: {
|
||||||
token,
|
...(recipient.role !== RecipientRole.ASSISTANT
|
||||||
|
? {
|
||||||
|
id: recipient.id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
signingStatus: {
|
||||||
|
not: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
signingOrder: {
|
||||||
|
gte: recipient.signingOrder ?? 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
document: true,
|
document: {
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
recipient: true,
|
recipient: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { document, recipient } = field;
|
const { document } = field;
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error(`Document not found for field ${field.id}`);
|
throw new Error(`Document not found for field ${field.id}`);
|
||||||
@@ -85,7 +106,10 @@ export const signFieldWithToken = async ({
|
|||||||
throw new Error(`Document ${document.id} must be pending for signing`);
|
throw new Error(`Document ${document.id} must be pending for signing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient?.signingStatus === SigningStatus.SIGNED) {
|
if (
|
||||||
|
recipient.signingStatus === SigningStatus.SIGNED ||
|
||||||
|
field.recipient.signingStatus === SigningStatus.SIGNED
|
||||||
|
) {
|
||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +205,8 @@ export const signFieldWithToken = async ({
|
|||||||
throw new Error('Typed signatures are not allowed. Please draw your signature');
|
throw new Error('Typed signatures are not allowed. Please draw your signature');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assistant = recipient.role === RecipientRole.ASSISTANT ? recipient : undefined;
|
||||||
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
const updatedField = await tx.field.update({
|
const updatedField = await tx.field.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -217,11 +243,14 @@ export const signFieldWithToken = async ({
|
|||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
type:
|
||||||
|
assistant && field.recipientId !== assistant.id
|
||||||
|
? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED
|
||||||
|
: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
user: {
|
user: {
|
||||||
email: recipient.email,
|
email: assistant?.email ?? recipient.email,
|
||||||
name: recipient.name,
|
name: assistant?.name ?? recipient.name,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -9,5 +9,8 @@ export const getRecipientByToken = async ({ token }: GetRecipientByTokenOptions)
|
|||||||
where: {
|
where: {
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
fields: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { FieldType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
|
||||||
|
export interface GetRecipientsForAssistantOptions {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssistantOptions) => {
|
||||||
|
const assistant = await prisma.recipient.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!assistant) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Assistant not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let recipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: assistant.documentId,
|
||||||
|
signingOrder: {
|
||||||
|
gte: assistant.signingOrder ?? 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
fields: {
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
recipientId: assistant.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
not: FieldType.SIGNATURE,
|
||||||
|
},
|
||||||
|
documentId: assistant.documentId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Omit the token for recipients other than the assistant so
|
||||||
|
// it doesn't get sent to the client.
|
||||||
|
recipients = recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
token: recipient.id === assistant.id ? token : '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return recipients;
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
|||||||
'DOCUMENT_DELETED', // When the document is soft deleted.
|
'DOCUMENT_DELETED', // When the document is soft deleted.
|
||||||
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
|
||||||
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
|
||||||
|
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
|
||||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated
|
||||||
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
'DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED', // When the global access authentication is updated.
|
||||||
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
'DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED', // When the global action authentication is updated.
|
||||||
@@ -44,6 +45,7 @@ export const ZDocumentAuditLogEmailTypeSchema = z.enum([
|
|||||||
'SIGNING_REQUEST',
|
'SIGNING_REQUEST',
|
||||||
'VIEW_REQUEST',
|
'VIEW_REQUEST',
|
||||||
'APPROVE_REQUEST',
|
'APPROVE_REQUEST',
|
||||||
|
'ASSISTING_REQUEST',
|
||||||
'CC',
|
'CC',
|
||||||
'DOCUMENT_COMPLETED',
|
'DOCUMENT_COMPLETED',
|
||||||
]);
|
]);
|
||||||
@@ -312,6 +314,83 @@ export const ZDocumentAuditLogEventDocumentFieldUninsertedSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event: Document field prefilled by assistant.
|
||||||
|
*/
|
||||||
|
export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED),
|
||||||
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
|
fieldId: z.string(),
|
||||||
|
|
||||||
|
// Organised into union to allow us to extend each field if required.
|
||||||
|
field: z.union([
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.INITIALS),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.EMAIL),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.DATE),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.NAME),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.TEXT),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.union([z.literal(FieldType.SIGNATURE), z.literal(FieldType.FREE_SIGNATURE)]),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.RADIO),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.CHECKBOX),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.DROPDOWN),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal(FieldType.NUMBER),
|
||||||
|
data: z.string(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
fieldSecurity: z.preprocess(
|
||||||
|
(input) => {
|
||||||
|
const legacyNoneSecurityType = JSON.stringify({
|
||||||
|
type: 'NONE',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace legacy 'NONE' field security type with undefined.
|
||||||
|
if (
|
||||||
|
typeof input === 'object' &&
|
||||||
|
input !== null &&
|
||||||
|
JSON.stringify(input) === legacyNoneSecurityType
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
},
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: ZRecipientActionAuthTypesSchema,
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED),
|
||||||
data: ZGenericFromToSchema,
|
data: ZGenericFromToSchema,
|
||||||
@@ -492,6 +571,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
|||||||
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
ZDocumentAuditLogEventDocumentMovedToTeamSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldInsertedSchema,
|
||||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||||
|
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||||
|
|||||||
@@ -313,6 +313,10 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Field unsigned`,
|
anonymous: msg`Field unsigned`,
|
||||||
identified: msg`${prefix} unsigned a field`,
|
identified: msg`${prefix} unsigned a field`,
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED }, () => ({
|
||||||
|
anonymous: msg`Field prefilled by assistant`,
|
||||||
|
identified: msg`${prefix} prefilled a field`,
|
||||||
|
}))
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED }, () => ({
|
||||||
anonymous: msg`Document visibility updated`,
|
anonymous: msg`Document visibility updated`,
|
||||||
identified: msg`${prefix} updated the document visibility`,
|
identified: msg`${prefix} updated the document visibility`,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "RecipientRole" ADD VALUE 'ASSISTANT';
|
||||||
@@ -425,6 +425,7 @@ enum RecipientRole {
|
|||||||
SIGNER
|
SIGNER
|
||||||
VIEWER
|
VIEWER
|
||||||
APPROVER
|
APPROVER
|
||||||
|
ASSISTANT
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
|
/// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
|
||||||
|
|||||||
5
packages/prisma/types/recipient-with-fields.ts
Normal file
5
packages/prisma/types/recipient-with-fields.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type RecipientWithFields = Recipient & {
|
||||||
|
fields: Field[];
|
||||||
|
};
|
||||||
@@ -9,12 +9,15 @@ import { ROLE_ICONS } from '@documenso/ui/primitives/recipient-role-icons';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@documenso/ui/primitives/select';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
export type RecipientRoleSelectProps = SelectProps & {
|
export type RecipientRoleSelectProps = SelectProps & {
|
||||||
hideCCRecipients?: boolean;
|
hideCCRecipients?: boolean;
|
||||||
|
isAssistantEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
|
export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSelectProps>(
|
||||||
({ hideCCRecipients, ...props }, ref) => (
|
({ hideCCRecipients, isAssistantEnabled = true, ...props }, ref) => (
|
||||||
<Select {...props}>
|
<Select {...props}>
|
||||||
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
|
<SelectTrigger ref={ref} className="bg-background w-[50px] p-2">
|
||||||
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
|
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
|
||||||
@@ -108,6 +111,42 @@ export const RecipientRoleSelect = forwardRef<HTMLButtonElement, RecipientRoleSe
|
|||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SelectItem
|
||||||
|
value={RecipientRole.ASSISTANT}
|
||||||
|
disabled={!isAssistantEnabled}
|
||||||
|
className={cn(
|
||||||
|
!isAssistantEnabled &&
|
||||||
|
'cursor-not-allowed opacity-50 data-[disabled]:pointer-events-auto',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex w-[150px] items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.ASSISTANT]}</span>
|
||||||
|
<Trans>Can prepare</Trans>
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
{isAssistantEnabled ? (
|
||||||
|
<Trans>
|
||||||
|
The recipient can prepare the document for later signers by pre-filling
|
||||||
|
suggest values.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Assistant role is only available when the document is in sequential signing
|
||||||
|
mode.
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -498,7 +498,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
const recipientsByRoleToDisplay = recipients.filter(
|
||||||
|
(recipient) =>
|
||||||
|
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||||
|
);
|
||||||
|
|
||||||
|
setSelectedSigner(
|
||||||
|
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
|
||||||
|
recipientsByRoleToDisplay[0],
|
||||||
|
);
|
||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
const recipientsByRole = useMemo(() => {
|
const recipientsByRole = useMemo(() => {
|
||||||
@@ -507,6 +515,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
VIEWER: [],
|
VIEWER: [],
|
||||||
SIGNER: [],
|
SIGNER: [],
|
||||||
APPROVER: [],
|
APPROVER: [],
|
||||||
|
ASSISTANT: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
recipients.forEach((recipient) => {
|
recipients.forEach((recipient) => {
|
||||||
@@ -519,7 +528,12 @@ export const AddFieldsFormPartial = ({
|
|||||||
const recipientsByRoleToDisplay = useMemo(() => {
|
const recipientsByRoleToDisplay = useMemo(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
|
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
|
||||||
.filter(([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER)
|
.filter(
|
||||||
|
([role]) =>
|
||||||
|
role !== RecipientRole.CC &&
|
||||||
|
role !== RecipientRole.VIEWER &&
|
||||||
|
role !== RecipientRole.ASSISTANT,
|
||||||
|
)
|
||||||
.map(
|
.map(
|
||||||
([role, roleRecipients]) =>
|
([role, roleRecipients]) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
@@ -671,9 +685,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedSigner?.email && (
|
{!selectedSigner?.email && (
|
||||||
<span className="gradie flex-1 truncate text-left">
|
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
|
||||||
{selectedSigner?.email}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { ShowFieldItem } from './show-field-item';
|
import { ShowFieldItem } from './show-field-item';
|
||||||
|
import { SigningOrderConfirmation } from './signing-order-confirmation';
|
||||||
import type { DocumentFlowStep } from './types';
|
import type { DocumentFlowStep } from './types';
|
||||||
|
|
||||||
export type AddSignersFormProps = {
|
export type AddSignersFormProps = {
|
||||||
@@ -120,6 +121,7 @@ export const AddSignersFormPartial = ({
|
|||||||
}, [recipients, form]);
|
}, [recipients, form]);
|
||||||
|
|
||||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
|
||||||
|
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setValue,
|
setValue,
|
||||||
@@ -131,6 +133,10 @@ export const AddSignersFormPartial = ({
|
|||||||
const watchedSigners = watch('signers');
|
const watchedSigners = watch('signers');
|
||||||
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
|
const isSigningOrderSequential = watch('signingOrder') === DocumentSigningOrder.SEQUENTIAL;
|
||||||
|
|
||||||
|
const hasAssistantRole = useMemo(() => {
|
||||||
|
return watchedSigners.some((signer) => signer.role === RecipientRole.ASSISTANT);
|
||||||
|
}, [watchedSigners]);
|
||||||
|
|
||||||
const normalizeSigningOrders = (signers: typeof watchedSigners) => {
|
const normalizeSigningOrders = (signers: typeof watchedSigners) => {
|
||||||
return signers
|
return signers
|
||||||
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
|
.sort((a, b) => (a.signingOrder ?? 0) - (b.signingOrder ?? 0))
|
||||||
@@ -230,6 +236,7 @@ export const AddSignersFormPartial = ({
|
|||||||
const items = Array.from(watchedSigners);
|
const items = Array.from(watchedSigners);
|
||||||
const [reorderedSigner] = items.splice(result.source.index, 1);
|
const [reorderedSigner] = items.splice(result.source.index, 1);
|
||||||
|
|
||||||
|
// Find next valid position
|
||||||
let insertIndex = result.destination.index;
|
let insertIndex = result.destination.index;
|
||||||
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
|
while (insertIndex < items.length && !canRecipientBeModified(items[insertIndex].nativeId)) {
|
||||||
insertIndex++;
|
insertIndex++;
|
||||||
@@ -237,126 +244,116 @@ export const AddSignersFormPartial = ({
|
|||||||
|
|
||||||
items.splice(insertIndex, 0, reorderedSigner);
|
items.splice(insertIndex, 0, reorderedSigner);
|
||||||
|
|
||||||
const updatedSigners = items.map((item, index) => ({
|
const updatedSigners = items.map((signer, index) => ({
|
||||||
...item,
|
...signer,
|
||||||
signingOrder: !canRecipientBeModified(item.nativeId) ? item.signingOrder : index + 1,
|
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : index + 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
updatedSigners.forEach((item, index) => {
|
form.setValue('signers', updatedSigners);
|
||||||
const keys: (keyof typeof item)[] = [
|
|
||||||
'formId',
|
|
||||||
'nativeId',
|
|
||||||
'email',
|
|
||||||
'name',
|
|
||||||
'role',
|
|
||||||
'signingOrder',
|
|
||||||
'actionAuth',
|
|
||||||
];
|
|
||||||
keys.forEach((key) => {
|
|
||||||
form.setValue(`signers.${index}.${key}` as const, item[key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentLength = form.getValues('signers').length;
|
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||||
if (currentLength > updatedSigners.length) {
|
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||||
for (let i = updatedSigners.length; i < currentLength; i++) {
|
toast({
|
||||||
form.unregister(`signers.${i}`);
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
}
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await form.trigger('signers');
|
await form.trigger('signers');
|
||||||
},
|
},
|
||||||
[form, canRecipientBeModified, watchedSigners],
|
[form, canRecipientBeModified, watchedSigners, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const triggerDragAndDrop = useCallback(
|
const handleRoleChange = useCallback(
|
||||||
(fromIndex: number, toIndex: number) => {
|
(index: number, role: RecipientRole) => {
|
||||||
if (!$sensorApi.current) {
|
const currentSigners = form.getValues('signers');
|
||||||
|
const signingOrder = form.getValues('signingOrder');
|
||||||
|
|
||||||
|
// Handle parallel to sequential conversion for assistants
|
||||||
|
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||||
|
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||||
|
toast({
|
||||||
|
title: _(msg`Signing order is enabled.`),
|
||||||
|
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const draggableId = signers[fromIndex].id;
|
const updatedSigners = currentSigners.map((signer, idx) => ({
|
||||||
|
|
||||||
const preDrag = $sensorApi.current.tryGetLock(draggableId);
|
|
||||||
|
|
||||||
if (!preDrag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const drag = preDrag.snapLift();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Move directly to the target index
|
|
||||||
if (fromIndex < toIndex) {
|
|
||||||
for (let i = fromIndex; i < toIndex; i++) {
|
|
||||||
drag.moveDown();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = fromIndex; i > toIndex; i--) {
|
|
||||||
drag.moveUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
drag.drop();
|
|
||||||
}, 500);
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
[signers],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateSigningOrders = useCallback(
|
|
||||||
(newIndex: number, oldIndex: number) => {
|
|
||||||
const updatedSigners = form.getValues('signers').map((signer, index) => {
|
|
||||||
if (index === oldIndex) {
|
|
||||||
return { ...signer, signingOrder: newIndex + 1 };
|
|
||||||
} else if (index >= newIndex && index < oldIndex) {
|
|
||||||
return {
|
|
||||||
...signer,
|
...signer,
|
||||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
role: idx === index ? role : signer.role,
|
||||||
? signer.signingOrder
|
signingOrder: !canRecipientBeModified(signer.nativeId) ? signer.signingOrder : idx + 1,
|
||||||
: (signer.signingOrder ?? index + 1) + 1,
|
}));
|
||||||
};
|
|
||||||
} else if (index <= newIndex && index > oldIndex) {
|
|
||||||
return {
|
|
||||||
...signer,
|
|
||||||
signingOrder: !canRecipientBeModified(signer.nativeId)
|
|
||||||
? signer.signingOrder
|
|
||||||
: Math.max(1, (signer.signingOrder ?? index + 1) - 1),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return signer;
|
|
||||||
});
|
|
||||||
|
|
||||||
updatedSigners.forEach((signer, index) => {
|
form.setValue('signers', updatedSigners);
|
||||||
form.setValue(`signers.${index}.signingOrder`, signer.signingOrder);
|
|
||||||
|
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[form, canRecipientBeModified],
|
[form, toast, canRecipientBeModified],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSigningOrderChange = useCallback(
|
const handleSigningOrderChange = useCallback(
|
||||||
(index: number, newOrderString: string) => {
|
(index: number, newOrderString: string) => {
|
||||||
const newOrder = parseInt(newOrderString, 10);
|
const trimmedOrderString = newOrderString.trim();
|
||||||
|
if (!trimmedOrderString) {
|
||||||
if (!newOrderString.trim()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isNaN(newOrder)) {
|
const newOrder = Number(trimmedOrderString);
|
||||||
form.setValue(`signers.${index}.signingOrder`, index + 1);
|
if (!Number.isInteger(newOrder) || newOrder < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = newOrder - 1;
|
const currentSigners = form.getValues('signers');
|
||||||
if (index !== newIndex) {
|
const signer = currentSigners[index];
|
||||||
updateSigningOrders(newIndex, index);
|
|
||||||
triggerDragAndDrop(index, newIndex);
|
// Remove signer from current position and insert at new position
|
||||||
|
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
|
||||||
|
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
|
||||||
|
remainingSigners.splice(newPosition, 0, signer);
|
||||||
|
|
||||||
|
const updatedSigners = remainingSigners.map((s, idx) => ({
|
||||||
|
...s,
|
||||||
|
signingOrder: !canRecipientBeModified(s.nativeId) ? s.signingOrder : idx + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.setValue('signers', updatedSigners);
|
||||||
|
|
||||||
|
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[form, triggerDragAndDrop, updateSigningOrders],
|
[form, canRecipientBeModified, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSigningOrderDisable = useCallback(() => {
|
||||||
|
setShowSigningOrderConfirmation(false);
|
||||||
|
|
||||||
|
const currentSigners = form.getValues('signers');
|
||||||
|
const updatedSigners = currentSigners.map((signer) => ({
|
||||||
|
...signer,
|
||||||
|
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.setValue('signers', updatedSigners);
|
||||||
|
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@@ -381,11 +378,16 @@ export const AddSignersFormPartial = ({
|
|||||||
{...field}
|
{...field}
|
||||||
id="signingOrder"
|
id="signingOrder"
|
||||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => {
|
||||||
|
if (!checked && hasAssistantRole) {
|
||||||
|
setShowSigningOrderConfirmation(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
field.onChange(
|
field.onChange(
|
||||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
disabled={isSubmitting || hasDocumentBeenSent}
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -610,7 +612,11 @@ export const AddSignersFormPartial = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RecipientRoleSelect
|
<RecipientRoleSelect
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={field.onChange}
|
isAssistantEnabled={isSigningOrderSequential}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
handleRoleChange(index, value as RecipientRole)
|
||||||
|
}
|
||||||
disabled={
|
disabled={
|
||||||
snapshot.isDragging ||
|
snapshot.isDragging ||
|
||||||
isSubmitting ||
|
isSubmitting ||
|
||||||
@@ -707,6 +713,12 @@ export const AddSignersFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</AnimateGenericFadeInOut>
|
</AnimateGenericFadeInOut>
|
||||||
|
|
||||||
|
<SigningOrderConfirmation
|
||||||
|
open={showSigningOrderConfirmation}
|
||||||
|
onOpenChange={setShowSigningOrderConfirmation}
|
||||||
|
onConfirm={handleSigningOrderDisable}
|
||||||
|
/>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<DocumentFlowFormContainerFooter>
|
<DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/alert-dialog';
|
||||||
|
|
||||||
|
export type SigningOrderConfirmationProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SigningOrderConfirmation({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
}: SigningOrderConfirmationProps) {
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Warning</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
You have an assistant role on the signers list, removing the signing order will change
|
||||||
|
the assistant role to signer.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={onConfirm}>Proceed</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,18 +17,18 @@ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|||||||
const RadioGroupItem = React.forwardRef<
|
const RadioGroupItem = React.forwardRef<
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
>(({ className, children: _children, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-input ring-offset-background focus:ring-ring h-4 w-4 rounded-full border focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
'border-primary text-primary focus-visible:ring-ring aspect-square h-4 w-4 rounded-full border shadow focus:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
<Circle className="fill-primary text-primary h-2.5 w-2.5" />
|
<Circle className="fill-primary h-2.5 w-2.5" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { RecipientRole } from '@prisma/client';
|
import type { RecipientRole } from '@prisma/client';
|
||||||
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
|
import { BadgeCheck, Copy, Eye, PencilLine, User } from 'lucide-react';
|
||||||
|
|
||||||
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
export const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
||||||
SIGNER: <PencilLine className="h-4 w-4" />,
|
SIGNER: <PencilLine className="h-4 w-4" />,
|
||||||
APPROVER: <BadgeCheck className="h-4 w-4" />,
|
APPROVER: <BadgeCheck className="h-4 w-4" />,
|
||||||
CC: <Copy className="h-4 w-4" />,
|
CC: <Copy className="h-4 w-4" />,
|
||||||
VIEWER: <Eye className="h-4 w-4" />,
|
VIEWER: <Eye className="h-4 w-4" />,
|
||||||
|
ASSISTANT: <User className="h-4 w-4" />,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { msg } from '@lingui/core/macro';
|
|||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
import { FieldType, RecipientRole } from '@prisma/client';
|
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
@@ -428,6 +428,7 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
VIEWER: [],
|
VIEWER: [],
|
||||||
SIGNER: [],
|
SIGNER: [],
|
||||||
APPROVER: [],
|
APPROVER: [],
|
||||||
|
ASSISTANT: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
recipients.forEach((recipient) => {
|
recipients.forEach((recipient) => {
|
||||||
@@ -437,10 +438,25 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
return recipientsByRole;
|
return recipientsByRole;
|
||||||
}, [recipients]);
|
}, [recipients]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const recipientsByRoleToDisplay = recipients.filter(
|
||||||
|
(recipient) =>
|
||||||
|
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
|
||||||
|
);
|
||||||
|
|
||||||
|
setSelectedSigner(
|
||||||
|
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
|
||||||
|
recipientsByRoleToDisplay[0],
|
||||||
|
);
|
||||||
|
}, [recipients]);
|
||||||
|
|
||||||
const recipientsByRoleToDisplay = useMemo(() => {
|
const recipientsByRoleToDisplay = useMemo(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
|
||||||
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
|
([role]) =>
|
||||||
|
role !== RecipientRole.CC &&
|
||||||
|
role !== RecipientRole.VIEWER &&
|
||||||
|
role !== RecipientRole.ASSISTANT,
|
||||||
);
|
);
|
||||||
}, [recipientsByRole]);
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { toast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { Checkbox } from '../checkbox';
|
import { Checkbox } from '../checkbox';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from '../document-flow/document-flow-root';
|
} from '../document-flow/document-flow-root';
|
||||||
import { ShowFieldItem } from '../document-flow/show-field-item';
|
import { ShowFieldItem } from '../document-flow/show-field-item';
|
||||||
|
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
|
||||||
import type { DocumentFlowStep } from '../document-flow/types';
|
import type { DocumentFlowStep } from '../document-flow/types';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
@@ -205,41 +207,30 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
|
|
||||||
const items = Array.from(watchedSigners);
|
const items = Array.from(watchedSigners);
|
||||||
const [reorderedSigner] = items.splice(result.source.index, 1);
|
const [reorderedSigner] = items.splice(result.source.index, 1);
|
||||||
|
|
||||||
const insertIndex = result.destination.index;
|
const insertIndex = result.destination.index;
|
||||||
|
|
||||||
items.splice(insertIndex, 0, reorderedSigner);
|
items.splice(insertIndex, 0, reorderedSigner);
|
||||||
|
|
||||||
const updatedSigners = items.map((item, index) => ({
|
const updatedSigners = items.map((signer, index) => ({
|
||||||
...item,
|
...signer,
|
||||||
signingOrder: index + 1,
|
signingOrder: index + 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
updatedSigners.forEach((item, index) => {
|
form.setValue('signers', updatedSigners);
|
||||||
const keys: (keyof typeof item)[] = [
|
|
||||||
'formId',
|
|
||||||
'nativeId',
|
|
||||||
'email',
|
|
||||||
'name',
|
|
||||||
'role',
|
|
||||||
'signingOrder',
|
|
||||||
'actionAuth',
|
|
||||||
];
|
|
||||||
keys.forEach((key) => {
|
|
||||||
form.setValue(`signers.${index}.${key}` as const, item[key]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentLength = form.getValues('signers').length;
|
const lastSigner = updatedSigners[updatedSigners.length - 1];
|
||||||
if (currentLength > updatedSigners.length) {
|
if (lastSigner.role === RecipientRole.ASSISTANT) {
|
||||||
for (let i = updatedSigners.length; i < currentLength; i++) {
|
toast({
|
||||||
form.unregister(`signers.${i}`);
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
}
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await form.trigger('signers');
|
await form.trigger('signers');
|
||||||
},
|
},
|
||||||
[form, watchedSigners],
|
[form, watchedSigners, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const triggerDragAndDrop = useCallback(
|
const triggerDragAndDrop = useCallback(
|
||||||
@@ -300,26 +291,94 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
|
|
||||||
const handleSigningOrderChange = useCallback(
|
const handleSigningOrderChange = useCallback(
|
||||||
(index: number, newOrderString: string) => {
|
(index: number, newOrderString: string) => {
|
||||||
const newOrder = parseInt(newOrderString, 10);
|
const trimmedOrderString = newOrderString.trim();
|
||||||
|
if (!trimmedOrderString) {
|
||||||
if (!newOrderString.trim()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isNaN(newOrder)) {
|
const newOrder = Number(trimmedOrderString);
|
||||||
form.setValue(`signers.${index}.signingOrder`, index + 1);
|
if (!Number.isInteger(newOrder) || newOrder < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = newOrder - 1;
|
const currentSigners = form.getValues('signers');
|
||||||
if (index !== newIndex) {
|
const signer = currentSigners[index];
|
||||||
updateSigningOrders(newIndex, index);
|
|
||||||
triggerDragAndDrop(index, newIndex);
|
// Remove signer from current position and insert at new position
|
||||||
|
const remainingSigners = currentSigners.filter((_, idx) => idx !== index);
|
||||||
|
const newPosition = Math.min(Math.max(0, newOrder - 1), currentSigners.length - 1);
|
||||||
|
remainingSigners.splice(newPosition, 0, signer);
|
||||||
|
|
||||||
|
const updatedSigners = remainingSigners.map((s, idx) => ({
|
||||||
|
...s,
|
||||||
|
signingOrder: idx + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.setValue('signers', updatedSigners);
|
||||||
|
|
||||||
|
if (signer.role === RecipientRole.ASSISTANT && newPosition === remainingSigners.length - 1) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[form, triggerDragAndDrop, updateSigningOrders],
|
[form, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRoleChange = useCallback(
|
||||||
|
(index: number, role: RecipientRole) => {
|
||||||
|
const currentSigners = form.getValues('signers');
|
||||||
|
const signingOrder = form.getValues('signingOrder');
|
||||||
|
|
||||||
|
// Handle parallel to sequential conversion for assistants
|
||||||
|
if (role === RecipientRole.ASSISTANT && signingOrder === DocumentSigningOrder.PARALLEL) {
|
||||||
|
form.setValue('signingOrder', DocumentSigningOrder.SEQUENTIAL);
|
||||||
|
toast({
|
||||||
|
title: _(msg`Signing order is enabled.`),
|
||||||
|
description: _(msg`You cannot add assistants when signing order is disabled.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSigners = currentSigners.map((signer, idx) => ({
|
||||||
|
...signer,
|
||||||
|
role: idx === index ? role : signer.role,
|
||||||
|
signingOrder: idx + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.setValue('signers', updatedSigners);
|
||||||
|
|
||||||
|
if (role === RecipientRole.ASSISTANT && index === updatedSigners.length - 1) {
|
||||||
|
toast({
|
||||||
|
title: _(msg`Warning: Assistant as last signer`),
|
||||||
|
description: _(
|
||||||
|
msg`Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist.`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form, toast],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
|
||||||
|
|
||||||
|
const handleSigningOrderDisable = useCallback(() => {
|
||||||
|
setShowSigningOrderConfirmation(false);
|
||||||
|
|
||||||
|
const currentSigners = form.getValues('signers');
|
||||||
|
const updatedSigners = currentSigners.map((signer) => ({
|
||||||
|
...signer,
|
||||||
|
role: signer.role === RecipientRole.ASSISTANT ? RecipientRole.SIGNER : signer.role,
|
||||||
|
}));
|
||||||
|
|
||||||
|
form.setValue('signers', updatedSigners);
|
||||||
|
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@@ -345,11 +404,19 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
{...field}
|
{...field}
|
||||||
id="signingOrder"
|
id="signingOrder"
|
||||||
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
checked={field.value === DocumentSigningOrder.SEQUENTIAL}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => {
|
||||||
|
if (
|
||||||
|
!checked &&
|
||||||
|
watchedSigners.some((s) => s.role === RecipientRole.ASSISTANT)
|
||||||
|
) {
|
||||||
|
setShowSigningOrderConfirmation(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
field.onChange(
|
field.onChange(
|
||||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -548,7 +615,10 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<RecipientRoleSelect
|
<RecipientRoleSelect
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={field.onChange}
|
onValueChange={(value) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
handleRoleChange(index, value as RecipientRole)
|
||||||
|
}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
hideCCRecipients={isSignerDirectRecipient(signer)}
|
hideCCRecipients={isSignerDirectRecipient(signer)}
|
||||||
/>
|
/>
|
||||||
@@ -669,6 +739,12 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={() => void onFormSubmit()}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|
||||||
|
<SigningOrderConfirmation
|
||||||
|
open={showSigningOrderConfirmation}
|
||||||
|
onOpenChange={setShowSigningOrderConfirmation}
|
||||||
|
onConfirm={handleSigningOrderDisable}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user