mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 17:51:49 +10:00
Compare commits
15 Commits
feat/dicta
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| ce94cf0051 | |||
| bccf4cd368 | |||
| c13988bb8f | |||
| 9f1831afcb | |||
| 574a7449fa | |||
| 1aee1bb4cd | |||
| 634dc2afd0 | |||
| 21d68f3275 | |||
| 63c98949bb | |||
| 4348a949dd | |||
| 2a098f89fa | |||
| bb805ea93b | |||
| cc8b972fbc | |||
| b55c419074 | |||
| f9e3993519 |
@ -6,6 +6,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|||||||
|
|
||||||
import { msg } from '@lingui/macro';
|
import { msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@ -55,6 +56,7 @@ export const EditDocumentForm = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const team = useOptionalCurrentTeam();
|
const team = useOptionalCurrentTeam();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||||
|
|
||||||
@ -73,7 +75,7 @@ export const EditDocumentForm = ({
|
|||||||
|
|
||||||
const { recipients, fields } = document;
|
const { recipients, fields } = document;
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
|
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: (newData) => {
|
onSuccess: (newData) => {
|
||||||
utils.document.getDocumentWithDetailsById.setData(
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
@ -85,6 +87,19 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSigningOrderForDocument } =
|
||||||
|
trpc.document.setSigningOrderForDocument.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
documentId: initialDocument.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
|
||||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
onSuccess: ({ fields: newFields }) => {
|
onSuccess: ({ fields: newFields }) => {
|
||||||
@ -121,6 +136,18 @@ export const EditDocumentForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: selfSignDocument } = trpc.document.selfSignDocument.useMutation({
|
||||||
|
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
|
onSuccess: (newData) => {
|
||||||
|
utils.document.getDocumentWithDetailsById.setData(
|
||||||
|
{
|
||||||
|
documentId: initialDocument.id,
|
||||||
|
},
|
||||||
|
(oldData) => ({ ...(oldData || initialDocument), ...newData }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: setPasswordForDocument } =
|
const { mutateAsync: setPasswordForDocument } =
|
||||||
trpc.document.setPasswordForDocument.useMutation();
|
trpc.document.setPasswordForDocument.useMutation();
|
||||||
|
|
||||||
@ -203,12 +230,9 @@ export const EditDocumentForm = ({
|
|||||||
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updateDocument({
|
setSigningOrderForDocument({
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
meta: {
|
signingOrder: data.signingOrder,
|
||||||
signingOrder: data.signingOrder,
|
|
||||||
modifyNextSigner: data.modifyNextSigner,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRecipients({
|
setRecipients({
|
||||||
@ -259,10 +283,22 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router refresh is here to clear the router cache for when navigating to /documents.
|
const hasSameOwnerAsRecipient =
|
||||||
router.refresh();
|
recipients.length === 1 && recipients[0].email === session?.user?.email;
|
||||||
|
|
||||||
setStep('subject');
|
if (hasSameOwnerAsRecipient) {
|
||||||
|
await selfSignDocument({
|
||||||
|
documentId: document.id,
|
||||||
|
teamId: team?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(`/sign/${recipients[0].token}`);
|
||||||
|
} else {
|
||||||
|
// Router refresh is here to clear the router cache for when navigating to /documents.
|
||||||
|
router.refresh();
|
||||||
|
|
||||||
|
setStep('subject');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -381,7 +417,6 @@ export const EditDocumentForm = ({
|
|||||||
documentFlow={documentFlow.signers}
|
documentFlow={documentFlow.signers}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
signingOrder={document.documentMeta?.signingOrder}
|
signingOrder={document.documentMeta?.signingOrder}
|
||||||
modifyNextSigner={document.documentMeta?.modifyNextSigner}
|
|
||||||
fields={fields}
|
fields={fields}
|
||||||
isDocumentEnterprise={isDocumentEnterprise}
|
isDocumentEnterprise={isDocumentEnterprise}
|
||||||
onSubmit={onAddSignersFormSubmit}
|
onSubmit={onAddSignersFormSubmit}
|
||||||
|
|||||||
@ -76,7 +76,6 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
|
|||||||
? {
|
? {
|
||||||
...templateMeta,
|
...templateMeta,
|
||||||
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
|
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
|
||||||
modifyNextSigner: templateMeta.modifyNextSigner ?? false,
|
|
||||||
documentId: 0,
|
documentId: 0,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@ -15,12 +15,7 @@ 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 { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import {
|
import { type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
DocumentSigningOrder,
|
|
||||||
type Field,
|
|
||||||
FieldType,
|
|
||||||
RecipientRole,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-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';
|
||||||
@ -45,12 +40,6 @@ export type SigningFormProps = {
|
|||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
setSelectedSignerId?: (id: number | null) => void;
|
setSelectedSignerId?: (id: number | null) => void;
|
||||||
isLastRecipient: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SigningFormData = {
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningForm = ({
|
export const SigningForm = ({
|
||||||
@ -61,7 +50,6 @@ export const SigningForm = ({
|
|||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
setSelectedSignerId,
|
setSelectedSignerId,
|
||||||
isLastRecipient,
|
|
||||||
}: SigningFormProps) => {
|
}: SigningFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -89,7 +77,7 @@ export const SigningForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, formState } = useForm<SigningFormData>();
|
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.
|
||||||
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||||
@ -114,58 +102,20 @@ export const SigningForm = ({
|
|||||||
validateFieldsInserted(fieldsRequiringValidation);
|
validateFieldsInserted(fieldsRequiringValidation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const completeDocument = async (
|
const onFormSubmit = async () => {
|
||||||
authOptions?: TRecipientActionAuth,
|
setValidateUninsertedFields(true);
|
||||||
nextSigner?: { email: string; name: string },
|
|
||||||
) => {
|
|
||||||
const payload = {
|
|
||||||
token: recipient.token,
|
|
||||||
documentId: document.id,
|
|
||||||
authOptions,
|
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
await completeDocumentWithToken(payload);
|
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||||
|
|
||||||
analytics.capture('App: Recipient has completed signing', {
|
if (hasSignatureField && !signatureValid) {
|
||||||
signerId: recipient.id,
|
return;
|
||||||
documentId: document.id,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFormSubmit = async (data: SigningFormData) => {
|
|
||||||
try {
|
|
||||||
setValidateUninsertedFields(true);
|
|
||||||
|
|
||||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
|
||||||
|
|
||||||
if (hasSignatureField && !signatureValid) {
|
|
||||||
throw new Error('Please provide a valid signature');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFieldsValid) {
|
|
||||||
throw new Error('Please complete all required fields');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSigner =
|
|
||||||
data.email && data.name
|
|
||||||
? {
|
|
||||||
email: data.email,
|
|
||||||
name: data.name,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await completeDocument(undefined, nextSigner);
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: 'Error',
|
|
||||||
description: 'An error occurred while completing the document. Please try again.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFieldsValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssistantFormSubmit = () => {
|
const onAssistantFormSubmit = () => {
|
||||||
@ -193,6 +143,22 @@ export const SigningForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||||
|
await completeDocumentWithToken({
|
||||||
|
token: recipient.token,
|
||||||
|
documentId: document.id,
|
||||||
|
authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
|
signerId: recipient.id,
|
||||||
|
documentId: document.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -242,21 +208,12 @@ export const SigningForm = ({
|
|||||||
|
|
||||||
<SignDialog
|
<SignDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={async (nextSigner) => {
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
await handleSubmit(async (formData) =>
|
|
||||||
onFormSubmit({ ...formData, ...nextSigner }),
|
|
||||||
)();
|
|
||||||
}}
|
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
canModifyNextSigner={
|
|
||||||
document.documentMeta?.modifyNextSigner &&
|
|
||||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
|
||||||
!isLastRecipient
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -426,21 +383,12 @@ export const SigningForm = ({
|
|||||||
|
|
||||||
<SignDialog
|
<SignDialog
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
onSignatureComplete={async (nextSigner) => {
|
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||||
await handleSubmit(async (formData) =>
|
|
||||||
onFormSubmit({ ...formData, ...nextSigner }),
|
|
||||||
)();
|
|
||||||
}}
|
|
||||||
documentTitle={document.title}
|
documentTitle={document.title}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
fieldsValidated={fieldsValidated}
|
fieldsValidated={fieldsValidated}
|
||||||
role={recipient.role}
|
role={recipient.role}
|
||||||
disabled={!isRecipientsTurn}
|
disabled={!isRecipientsTurn}
|
||||||
canModifyNextSigner={
|
|
||||||
document.documentMeta?.modifyNextSigner &&
|
|
||||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
|
||||||
!isLastRecipient
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
|
|||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-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 { getIsLastRecipient } from '@documenso/lib/server-only/recipient/get-is-last-recipient';
|
|
||||||
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';
|
||||||
@ -45,7 +44,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
const [document, recipient, fields, completedFields, isLastRecipient] = await Promise.all([
|
const [document, recipient, fields, completedFields] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@ -54,7 +53,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getCompletedFieldsForToken({ token }),
|
getCompletedFieldsForToken({ token }),
|
||||||
getIsLastRecipient({ token }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -171,7 +169,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
completedFields={completedFields}
|
completedFields={completedFields}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
isLastRecipient={isLastRecipient}
|
|
||||||
/>
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
|
|||||||
@ -37,6 +37,11 @@ export const RecipientProvider = ({
|
|||||||
recipient,
|
recipient,
|
||||||
targetSigner = null,
|
targetSigner = null,
|
||||||
}: RecipientProviderProps) => {
|
}: RecipientProviderProps) => {
|
||||||
|
// console.log({
|
||||||
|
// recipient,
|
||||||
|
// targetSigner,
|
||||||
|
// isAssistantMode: !!targetSigner,
|
||||||
|
// });
|
||||||
return (
|
return (
|
||||||
<RecipientContext.Provider
|
<RecipientContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@ -1,36 +1,18 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Trans } from '@lingui/macro';
|
import { Trans } from '@lingui/macro';
|
||||||
import { ArrowRight } from 'lucide-react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import type { Field } from '@documenso/prisma/client';
|
import type { Field } from '@documenso/prisma/client';
|
||||||
import { RecipientRole } from '@documenso/prisma/client';
|
import { RecipientRole } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@documenso/ui/primitives/form/form';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
||||||
|
|
||||||
@ -39,26 +21,12 @@ export type SignDialogProps = {
|
|||||||
documentTitle: string;
|
documentTitle: string;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
fieldsValidated: () => void | Promise<void>;
|
fieldsValidated: () => void | Promise<void>;
|
||||||
onSignatureComplete: (nextSigner?: { email?: string; name?: string }) => void | Promise<void>;
|
onSignatureComplete: () => void | Promise<void>;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
canModifyNextSigner?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
export const SignDialog = ({
|
||||||
modifyNextSigner: z.boolean().default(false),
|
|
||||||
nextSigner: z
|
|
||||||
.object({
|
|
||||||
email: z.string().email({ message: 'Please enter a valid email address' }).optional(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
export function SignDialog({
|
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
documentTitle,
|
documentTitle,
|
||||||
fields,
|
fields,
|
||||||
@ -66,9 +34,7 @@ export function SignDialog({
|
|||||||
onSignatureComplete,
|
onSignatureComplete,
|
||||||
role,
|
role,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
canModifyNextSigner = false,
|
}: SignDialogProps) => {
|
||||||
}: SignDialogProps) {
|
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||||
@ -81,336 +47,104 @@ export function SignDialog({
|
|||||||
setShowDialog(open);
|
setShowDialog(open);
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalSteps = 2;
|
|
||||||
|
|
||||||
const handleContinue = () => {
|
|
||||||
if (step < totalSteps) {
|
|
||||||
setStep(step + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm<TFormSchema>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFormSubmit = async (data: TFormSchema) => {
|
|
||||||
try {
|
|
||||||
await fieldsValidated();
|
|
||||||
|
|
||||||
await onSignatureComplete({
|
|
||||||
email: data.nextSigner.email?.trim().toLowerCase(),
|
|
||||||
name: data.nextSigner.name?.trim(),
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowDialog(false);
|
|
||||||
form.reset();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
||||||
{!canModifyNextSigner ? (
|
<DialogTrigger asChild>
|
||||||
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
<Button
|
||||||
<DialogTrigger asChild>
|
className="w-full"
|
||||||
<Button
|
type="button"
|
||||||
className="w-full"
|
size="lg"
|
||||||
type="button"
|
onClick={fieldsValidated}
|
||||||
size="lg"
|
loading={isSubmitting}
|
||||||
onClick={fieldsValidated}
|
disabled={disabled}
|
||||||
loading={isSubmitting}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>
|
|
||||||
<div className="text-foreground text-xl font-semibold">
|
|
||||||
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
|
|
||||||
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
|
|
||||||
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
|
|
||||||
</div>
|
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
|
||||||
{role === RecipientRole.VIEWER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete viewing "
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
{documentTitle}
|
|
||||||
</span>
|
|
||||||
".
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.SIGNER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete signing "
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
{documentTitle}
|
|
||||||
</span>
|
|
||||||
".
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.APPROVER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete approving{' '}
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
"{documentTitle}"
|
|
||||||
</span>
|
|
||||||
.
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setShowDialog(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!isComplete}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await onSignatureComplete();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
|
||||||
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
|
||||||
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
) : (
|
|
||||||
<Dialog
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (open) setStep(1);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>
|
||||||
|
<div className="text-foreground text-xl font-semibold">
|
||||||
|
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
|
||||||
|
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
|
||||||
|
</div>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground max-w-[50ch]">
|
||||||
|
{role === RecipientRole.VIEWER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete viewing "
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
{documentTitle}
|
||||||
|
</span>
|
||||||
|
".
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{role === RecipientRole.SIGNER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete signing "
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
{documentTitle}
|
||||||
|
</span>
|
||||||
|
".
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{role === RecipientRole.APPROVER && (
|
||||||
|
<span>
|
||||||
|
<Trans>
|
||||||
|
<span className="inline-flex flex-wrap">
|
||||||
|
You are about to complete approving{' '}
|
||||||
|
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||||
|
"{documentTitle}"
|
||||||
|
</span>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
<br /> Are you sure?
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SigningDisclosure className="mt-4" />
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||||
onClick={fieldsValidated}
|
variant="secondary"
|
||||||
loading={isSubmitting}
|
onClick={() => {
|
||||||
disabled={disabled}
|
setShowDialog(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
|
<Trans>Cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>
|
|
||||||
{step === 1 && (
|
|
||||||
<div className="text-foreground text-base font-semibold">
|
|
||||||
<Trans>
|
|
||||||
Modify Next Signer <span className="text-muted-foreground">(Optional)</span>
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
<Button
|
||||||
<div className="text-foreground text-xl font-semibold">
|
type="button"
|
||||||
{role === RecipientRole.VIEWER && <Trans>Complete Viewing</Trans>}
|
className="flex-1"
|
||||||
{role === RecipientRole.SIGNER && <Trans>Complete Signing</Trans>}
|
disabled={!isComplete}
|
||||||
{role === RecipientRole.APPROVER && <Trans>Complete Approval</Trans>}
|
loading={isSubmitting}
|
||||||
</div>
|
onClick={onSignatureComplete}
|
||||||
)}
|
>
|
||||||
</DialogTitle>
|
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
||||||
|
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
||||||
{step === 1 && (
|
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
||||||
<Form {...form}>
|
</Button>
|
||||||
<form onSubmit={form.handleSubmit(onFormSubmit)} className="flex flex-col gap-y-4">
|
</div>
|
||||||
<FormField
|
</DialogFooter>
|
||||||
control={form.control}
|
</DialogContent>
|
||||||
name="modifyNextSigner"
|
</Dialog>
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">
|
|
||||||
<Trans>Modify next signer details</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.watch('modifyNextSigner') && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="nextSigner.email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans>Next Signer Email</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input type="email" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="nextSigner.name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans>Next Signer Name</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<>
|
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
|
||||||
{role === RecipientRole.VIEWER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete viewing "
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
{documentTitle}
|
|
||||||
</span>
|
|
||||||
".
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.SIGNER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete signing "
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
{documentTitle}
|
|
||||||
</span>
|
|
||||||
".
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.APPROVER && (
|
|
||||||
<span>
|
|
||||||
<Trans>
|
|
||||||
<span className="inline-flex flex-wrap">
|
|
||||||
You are about to complete approving{' '}
|
|
||||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
|
||||||
"{documentTitle}"
|
|
||||||
</span>
|
|
||||||
.
|
|
||||||
</span>
|
|
||||||
<br /> Are you sure?
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
|
||||||
<div className="flex justify-center space-x-1.5 max-sm:order-1">
|
|
||||||
{[...Array(totalSteps)].map((_, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
onClick={() => setStep(index + 1)}
|
|
||||||
className={cn(
|
|
||||||
'bg-primary h-1.5 w-1.5 rounded-full',
|
|
||||||
index + 1 === step ? 'bg-primary' : 'opacity-20',
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
aria-label={`Go to step ${index + 1}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button type="button" variant="ghost">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
|
|
||||||
{step === 1 && (
|
|
||||||
<Button className="group" type="button" onClick={handleContinue}>
|
|
||||||
Next
|
|
||||||
<ArrowRight
|
|
||||||
className="-me-1 ms-2 opacity-60 transition-transform group-hover:translate-x-0.5"
|
|
||||||
size={16}
|
|
||||||
strokeWidth={2}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!isComplete}
|
|
||||||
loading={isSubmitting}
|
|
||||||
onClick={form.handleSubmit(onFormSubmit)}
|
|
||||||
>
|
|
||||||
{role === RecipientRole.VIEWER && <Trans>Mark as Viewed</Trans>}
|
|
||||||
{role === RecipientRole.SIGNER && <Trans>Sign</Trans>}
|
|
||||||
{role === RecipientRole.APPROVER && <Trans>Approve</Trans>}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -49,7 +49,6 @@ export type SigningPageViewProps = {
|
|||||||
completedFields: CompletedField[];
|
completedFields: CompletedField[];
|
||||||
isRecipientsTurn: boolean;
|
isRecipientsTurn: boolean;
|
||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
isLastRecipient: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningPageView = ({
|
export const SigningPageView = ({
|
||||||
@ -59,7 +58,6 @@ export const SigningPageView = ({
|
|||||||
completedFields,
|
completedFields,
|
||||||
isRecipientsTurn,
|
isRecipientsTurn,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
isLastRecipient,
|
|
||||||
}: SigningPageViewProps) => {
|
}: SigningPageViewProps) => {
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
|
|
||||||
@ -161,7 +159,6 @@ export const SigningPageView = ({
|
|||||||
redirectUrl={documentMeta?.redirectUrl}
|
redirectUrl={documentMeta?.redirectUrl}
|
||||||
isRecipientsTurn={isRecipientsTurn}
|
isRecipientsTurn={isRecipientsTurn}
|
||||||
allRecipients={allRecipients}
|
allRecipients={allRecipients}
|
||||||
isLastRecipient={isLastRecipient}
|
|
||||||
setSelectedSignerId={setSelectedSignerId}
|
setSelectedSignerId={setSelectedSignerId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type EmbedDocumentCompletedPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
|
||||||
|
console.log({ signature });
|
||||||
return (
|
return (
|
||||||
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
<h3 className="text-foreground text-2xl font-semibold">
|
<h3 className="text-foreground text-2xl font-semibold">
|
||||||
|
|||||||
@ -336,6 +336,16 @@ export const DocumentHistorySheet = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN }, ({ data }) => (
|
||||||
|
<DocumentHistorySheetChanges
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
key: 'Signed by',
|
||||||
|
value: data.recipientEmail,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))
|
||||||
.with(
|
.with(
|
||||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
|
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED },
|
||||||
({ data }) => (
|
({ data }) => (
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import { expect, test } from '@playwright/test';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
||||||
|
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
@ -612,7 +614,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(`/sign/${recipient?.token}`);
|
await page.goto(`/sign/${recipient!.token}`);
|
||||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
|
|
||||||
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||||
@ -630,24 +632,22 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
|||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
await page.waitForURL(`/sign/${recipient?.token}/complete`);
|
await page.waitForURL(`/sign/${recipient!.token}/complete`);
|
||||||
await expect(page.getByText('Document Signed')).toBeVisible();
|
await expect(page.getByText('Document Signed')).toBeVisible();
|
||||||
|
|
||||||
const updatedRecipient = await prisma.recipient.findFirst({
|
const updatedRecipient = await getRecipientById({
|
||||||
where: { id: recipient?.id },
|
documentId: document.id,
|
||||||
|
id: recipient!.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the document to be signed.
|
// Wait for the document to be signed.
|
||||||
await page.waitForTimeout(5000);
|
await expect(async () => {
|
||||||
|
const signedDocument = await getDocumentById({ id: document.id, userId: user.id });
|
||||||
const finalDocument = await prisma.document.findFirst({
|
expect(signedDocument?.status).toBe(DocumentStatus.COMPLETED);
|
||||||
where: { id: createdDocument?.id },
|
}).toPass();
|
||||||
});
|
|
||||||
|
|
||||||
expect(finalDocument?.status).toBe(DocumentStatus.COMPLETED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
|
test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', async ({
|
||||||
@ -655,7 +655,7 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode',
|
|||||||
}) => {
|
}) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
|
|
||||||
const { document, recipients } = await seedPendingDocumentWithFullFields({
|
const { recipients } = await seedPendingDocumentWithFullFields({
|
||||||
owner: user,
|
owner: user,
|
||||||
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
||||||
fields: [FieldType.SIGNATURE],
|
fields: [FieldType.SIGNATURE],
|
||||||
@ -682,3 +682,85 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode',
|
|||||||
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
|
await expect(page).not.toHaveURL(`/sign/${activeRecipient?.token}/waiting`);
|
||||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[DOCUMENT_FLOW]: should be able to self sign a document', async ({ page }) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/documents/${document.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const documentTitle = `Self-Signing-${Date.now()}.pdf`;
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||||
|
await page.getByLabel('Title').fill(documentTitle);
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||||
|
await page.getByRole('button', { name: 'Add myself' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Signature' }).click();
|
||||||
|
await page.locator('canvas').click({
|
||||||
|
position: {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||||
|
|
||||||
|
const documentRecipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: document.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token, email, id: recipientId } = documentRecipients[0];
|
||||||
|
|
||||||
|
expect(documentRecipients.length).toBe(1);
|
||||||
|
expect(email).toBe(user.email);
|
||||||
|
|
||||||
|
await page.waitForURL(`/sign/${token}`);
|
||||||
|
await expect(page.getByRole('heading', { name: documentTitle })).toBeVisible();
|
||||||
|
|
||||||
|
const { status } = await getDocumentByToken(token);
|
||||||
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
|
const fields = await prisma.field.findMany({
|
||||||
|
where: { recipientId, documentId: document.id },
|
||||||
|
});
|
||||||
|
const recipientField = fields[0];
|
||||||
|
expect(recipientField).not.toBeNull();
|
||||||
|
|
||||||
|
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas#signature');
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
if (box) {
|
||||||
|
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||||
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Sign' }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(`/sign/${token}/complete`);
|
||||||
|
await expect(page.getByText('Document Signed')).toBeVisible();
|
||||||
|
|
||||||
|
const updatedRecipient = await getRecipientById({ documentId: document.id, id: recipientId });
|
||||||
|
expect(updatedRecipient?.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const signedDocument = await getDocumentById({ id: document.id, userId: user.id });
|
||||||
|
expect(signedDocument?.status).toBe(DocumentStatus.COMPLETED);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export type CreateDocumentMetaOptions = {
|
|||||||
distributionMethod?: DocumentDistributionMethod;
|
distributionMethod?: DocumentDistributionMethod;
|
||||||
typedSignatureEnabled?: boolean;
|
typedSignatureEnabled?: boolean;
|
||||||
language?: SupportedLanguageCodes;
|
language?: SupportedLanguageCodes;
|
||||||
modifyNextSigner?: boolean;
|
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,7 +46,6 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
modifyNextSigner,
|
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentMetaOptions) => {
|
}: CreateDocumentMetaOptions) => {
|
||||||
const document = await prisma.document.findFirst({
|
const document = await prisma.document.findFirst({
|
||||||
@ -100,7 +98,6 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
modifyNextSigner,
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
subject,
|
subject,
|
||||||
@ -114,7 +111,6 @@ export const upsertDocumentMeta = async ({
|
|||||||
distributionMethod,
|
distributionMethod,
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
language,
|
language,
|
||||||
modifyNextSigner,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -28,10 +28,6 @@ export type CompleteDocumentWithTokenOptions = {
|
|||||||
userId?: number;
|
userId?: number;
|
||||||
authOptions?: TRecipientActionAuth;
|
authOptions?: TRecipientActionAuth;
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
nextSigner?: {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||||
@ -55,53 +51,10 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const delegateNextSigner = async ({
|
|
||||||
documentId,
|
|
||||||
currentRecipientId,
|
|
||||||
nextSigner,
|
|
||||||
}: {
|
|
||||||
documentId: number;
|
|
||||||
currentRecipientId: number;
|
|
||||||
nextSigner: { email: string; name: string };
|
|
||||||
}) => {
|
|
||||||
const document = await prisma.document.findUnique({
|
|
||||||
where: { id: documentId },
|
|
||||||
include: {
|
|
||||||
recipients: {
|
|
||||||
orderBy: [{ signingOrder: 'asc' }, { id: 'asc' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new Error('Document not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRecipient = document.recipients.find((r) => r.id === currentRecipientId);
|
|
||||||
const nextRecipient = document.recipients.find(
|
|
||||||
(r) => r.signingOrder === (currentRecipient?.signingOrder ?? 0) + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!nextRecipient) {
|
|
||||||
throw new Error('Next recipient not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.recipient.update({
|
|
||||||
where: { id: nextRecipient.id },
|
|
||||||
data: {
|
|
||||||
email: nextSigner.email,
|
|
||||||
name: nextSigner.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return nextRecipient;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
nextSigner,
|
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const document = await getDocument({ token, documentId });
|
const document = await getDocument({ token, documentId });
|
||||||
|
|
||||||
@ -159,18 +112,6 @@ export const completeDocumentWithToken = async ({
|
|||||||
// throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
|
// throw new AppError(AppErrorCode.UNAUTHORIZED, 'Invalid authentication values');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (
|
|
||||||
nextSigner &&
|
|
||||||
document.documentMeta?.modifyNextSigner &&
|
|
||||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL
|
|
||||||
) {
|
|
||||||
await delegateNextSigner({
|
|
||||||
documentId: document.id,
|
|
||||||
currentRecipientId: recipient.id,
|
|
||||||
nextSigner,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.recipient.update({
|
await tx.recipient.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
177
packages/lib/server-only/document/self-sign-document.ts
Normal file
177
packages/lib/server-only/document/self-sign-document.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { jobs } from '../../jobs/client';
|
||||||
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
|
|
||||||
|
export type SelfSignDocumentOptions = {
|
||||||
|
documentId: number;
|
||||||
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
|
requestMetadata?: ApiRequestMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selfSignDocument = async ({
|
||||||
|
documentId,
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
requestMetadata,
|
||||||
|
}: SelfSignDocumentOptions) => {
|
||||||
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await prisma.document.findUnique({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
...(teamId
|
||||||
|
? {
|
||||||
|
team: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
userId,
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: {
|
||||||
|
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
||||||
|
},
|
||||||
|
documentMeta: true,
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
throw new Error('Document not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.recipients.length === 0) {
|
||||||
|
throw new Error('Document has no recipients');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.recipients.length !== 1 || document.recipients[0].email !== user.email) {
|
||||||
|
throw new Error('Invalid document for self-signing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.status === DocumentStatus.COMPLETED) {
|
||||||
|
throw new Error('Can not sign completed document');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { documentData } = document;
|
||||||
|
|
||||||
|
if (!documentData || !documentData.data) {
|
||||||
|
throw new Error('Document data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.formValues) {
|
||||||
|
const file = await getFile(documentData);
|
||||||
|
|
||||||
|
const prefilled = await insertFormValuesInPdf({
|
||||||
|
pdf: Buffer.from(file),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newDocumentData = await putPdfFile({
|
||||||
|
name: document.title,
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prisma.document.update({
|
||||||
|
where: {
|
||||||
|
id: document.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
documentDataId: newDocumentData.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(document, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipientHasNoActionToTake =
|
||||||
|
document.recipients[0].role === RecipientRole.CC ||
|
||||||
|
document.recipients[0].signingStatus === SigningStatus.SIGNED;
|
||||||
|
|
||||||
|
if (recipientHasNoActionToTake) {
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'internal.seal-document',
|
||||||
|
payload: {
|
||||||
|
documentId,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDocument = await prisma.$transaction(async (tx) => {
|
||||||
|
if (document.status === DocumentStatus.DRAFT) {
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN,
|
||||||
|
documentId: document.id,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
user,
|
||||||
|
data: {
|
||||||
|
recipientId: document.recipients[0].id,
|
||||||
|
recipientEmail: document.recipients[0].email,
|
||||||
|
recipientName: document.recipients[0].name,
|
||||||
|
recipientRole: document.recipients[0].role,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.recipient.update({
|
||||||
|
where: {
|
||||||
|
id: document.recipients[0].id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sendStatus: SendStatus.SENT,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await tx.document.update({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: DocumentStatus.PENDING,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedDocument;
|
||||||
|
};
|
||||||
@ -94,7 +94,7 @@ export const sendDocument = async ({
|
|||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
if (!documentData.data) {
|
if (!documentData || !documentData.data) {
|
||||||
throw new Error('Document data not found');
|
throw new Error('Document data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { DocumentSigningOrder, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
export type GetIsLastRecipientOptions = {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getIsLastRecipient({ token }: GetIsLastRecipientOptions) {
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
recipients: {
|
|
||||||
some: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
recipients: {
|
|
||||||
where: {
|
|
||||||
role: {
|
|
||||||
not: RecipientRole.CC,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) {
|
|
||||||
const unsignedRecipients = document.recipients.filter(
|
|
||||||
(recipient) => recipient.signingStatus !== SigningStatus.SIGNED,
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsignedRecipients.length <= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { recipients } = document;
|
|
||||||
const currentRecipientIndex = recipients.findIndex((r) => r.token === token);
|
|
||||||
|
|
||||||
if (currentRecipientIndex === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentRecipientIndex === recipients.length - 1;
|
|
||||||
}
|
|
||||||
@ -13,6 +13,7 @@ import { ZRecipientAccessAuthTypesSchema, ZRecipientActionAuthTypesSchema } from
|
|||||||
export const ZDocumentAuditLogTypeSchema = z.enum([
|
export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||||
// Document actions.
|
// Document actions.
|
||||||
'EMAIL_SENT',
|
'EMAIL_SENT',
|
||||||
|
'SELF_SIGN',
|
||||||
|
|
||||||
// Document modification events.
|
// Document modification events.
|
||||||
'FIELD_CREATED',
|
'FIELD_CREATED',
|
||||||
@ -181,6 +182,14 @@ export const ZDocumentAuditLogEventEmailSentSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event: Self sign
|
||||||
|
*/
|
||||||
|
export const ZDocumentAuditLogSelfSignSchema = z.object({
|
||||||
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN),
|
||||||
|
data: ZBaseRecipientDataSchema,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event: Document completed.
|
* Event: Document completed.
|
||||||
*/
|
*/
|
||||||
@ -566,6 +575,7 @@ export const ZDocumentAuditLogBaseSchema = z.object({
|
|||||||
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||||
z.union([
|
z.union([
|
||||||
ZDocumentAuditLogEventEmailSentSchema,
|
ZDocumentAuditLogEventEmailSentSchema,
|
||||||
|
ZDocumentAuditLogSelfSignSchema,
|
||||||
ZDocumentAuditLogEventDocumentCompletedSchema,
|
ZDocumentAuditLogEventDocumentCompletedSchema,
|
||||||
ZDocumentAuditLogEventDocumentCreatedSchema,
|
ZDocumentAuditLogEventDocumentCreatedSchema,
|
||||||
ZDocumentAuditLogEventDocumentDeletedSchema,
|
ZDocumentAuditLogEventDocumentDeletedSchema,
|
||||||
|
|||||||
@ -55,7 +55,6 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
|||||||
typedSignatureEnabled: true,
|
typedSignatureEnabled: true,
|
||||||
language: true,
|
language: true,
|
||||||
emailSettings: true,
|
emailSettings: true,
|
||||||
modifyNextSigner: true,
|
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
recipients: ZRecipientLiteSchema.array(),
|
recipients: ZRecipientLiteSchema.array(),
|
||||||
fields: ZFieldSchema.array(),
|
fields: ZFieldSchema.array(),
|
||||||
|
|||||||
@ -369,16 +369,6 @@ export const formatDocumentAuditLogAction = (
|
|||||||
identified: result,
|
identified: result,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => {
|
|
||||||
const userName = prefix || _(msg`Recipient`);
|
|
||||||
|
|
||||||
const result = msg`${userName} rejected the document`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
anonymous: result,
|
|
||||||
identified: result,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
|
||||||
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
|
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
|
||||||
identified: data.isResending
|
identified: data.isResending
|
||||||
@ -389,6 +379,14 @@ export const formatDocumentAuditLogAction = (
|
|||||||
anonymous: msg`Document completed`,
|
anonymous: msg`Document completed`,
|
||||||
identified: msg`Document completed`,
|
identified: msg`Document completed`,
|
||||||
}))
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.SELF_SIGN }, () => ({
|
||||||
|
anonymous: msg`Self-signed document`,
|
||||||
|
identified: msg`${prefix} self-signed the document`,
|
||||||
|
}))
|
||||||
|
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => ({
|
||||||
|
anonymous: msg`Document rejected`,
|
||||||
|
identified: msg`${prefix} rejected the document: ${data.reason}`,
|
||||||
|
}))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "DocumentMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "TemplateMeta" ADD COLUMN "modifyNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@ -390,7 +390,6 @@ model DocumentMeta {
|
|||||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
redirectUrl String?
|
redirectUrl String?
|
||||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||||
modifyNextSigner Boolean @default(false)
|
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
@ -660,7 +659,6 @@ model TemplateMeta {
|
|||||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||||
modifyNextSigner Boolean @default(false)
|
|
||||||
|
|
||||||
templateId Int @unique
|
templateId Int @unique
|
||||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@ -5,18 +5,6 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
|||||||
|
|
||||||
import { prisma } from '..';
|
import { prisma } from '..';
|
||||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||||
import { seedPendingDocument } from './documents';
|
|
||||||
import { seedDirectTemplate, seedTemplate } from './templates';
|
|
||||||
|
|
||||||
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
|
||||||
return prisma.documentData.create({
|
|
||||||
data: {
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: documentData,
|
|
||||||
initialData: documentData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const seedDatabase = async () => {
|
export const seedDatabase = async () => {
|
||||||
const examplePdf = fs
|
const examplePdf = fs
|
||||||
@ -51,80 +39,35 @@ export const seedDatabase = async () => {
|
|||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = 1; i <= 4; i++) {
|
const examplePdfData = await prisma.documentData.upsert({
|
||||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
where: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
await prisma.document.create({
|
|
||||||
data: {
|
|
||||||
source: DocumentSource.DOCUMENT,
|
|
||||||
title: `Example Document ${i}`,
|
|
||||||
documentDataId: documentData.id,
|
|
||||||
userId: exampleUser.id,
|
|
||||||
recipients: {
|
|
||||||
create: {
|
|
||||||
name: String(adminUser.name),
|
|
||||||
email: adminUser.email,
|
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i <= 4; i++) {
|
|
||||||
const documentData = await createDocumentData({ documentData: examplePdf });
|
|
||||||
|
|
||||||
await prisma.document.create({
|
|
||||||
data: {
|
|
||||||
source: DocumentSource.DOCUMENT,
|
|
||||||
title: `Document ${i}`,
|
|
||||||
documentDataId: documentData.id,
|
|
||||||
userId: adminUser.id,
|
|
||||||
recipients: {
|
|
||||||
create: {
|
|
||||||
name: String(exampleUser.name),
|
|
||||||
email: exampleUser.email,
|
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await seedPendingDocument(exampleUser, [adminUser], {
|
|
||||||
key: 'example-pending',
|
|
||||||
createDocumentOptions: {
|
|
||||||
title: 'Pending Document',
|
|
||||||
},
|
},
|
||||||
|
create: {
|
||||||
|
id: 'clmn0kv5k0000pe04vcqg5zla',
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: examplePdf,
|
||||||
|
initialData: examplePdf,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
await seedPendingDocument(adminUser, [exampleUser], {
|
await prisma.document.create({
|
||||||
key: 'admin-pending',
|
data: {
|
||||||
createDocumentOptions: {
|
source: DocumentSource.DOCUMENT,
|
||||||
title: 'Pending Document',
|
title: 'Example Document',
|
||||||
|
documentDataId: examplePdfData.id,
|
||||||
|
userId: exampleUser.id,
|
||||||
|
recipients: {
|
||||||
|
create: {
|
||||||
|
name: String(adminUser.name),
|
||||||
|
email: adminUser.email,
|
||||||
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
seedTemplate({
|
|
||||||
title: 'Template 1',
|
|
||||||
userId: exampleUser.id,
|
|
||||||
}),
|
|
||||||
seedDirectTemplate({
|
|
||||||
title: 'Direct Template 1',
|
|
||||||
userId: exampleUser.id,
|
|
||||||
}),
|
|
||||||
|
|
||||||
seedTemplate({
|
|
||||||
title: 'Template 1',
|
|
||||||
userId: adminUser.id,
|
|
||||||
}),
|
|
||||||
seedDirectTemplate({
|
|
||||||
title: 'Direct Template 1',
|
|
||||||
userId: adminUser.id,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const testUsers = [
|
const testUsers = [
|
||||||
'test@documenso.com',
|
'test@documenso.com',
|
||||||
'test2@documenso.com',
|
'test2@documenso.com',
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/
|
|||||||
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team';
|
||||||
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
||||||
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
|
||||||
|
import { selfSignDocument } from '@documenso/lib/server-only/document/self-sign-document';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
@ -50,6 +51,7 @@ import {
|
|||||||
ZMoveDocumentToTeamSchema,
|
ZMoveDocumentToTeamSchema,
|
||||||
ZResendDocumentMutationSchema,
|
ZResendDocumentMutationSchema,
|
||||||
ZSearchDocumentsMutationSchema,
|
ZSearchDocumentsMutationSchema,
|
||||||
|
ZSelfSignDocumentMutationSchema,
|
||||||
ZSetPasswordForDocumentMutationSchema,
|
ZSetPasswordForDocumentMutationSchema,
|
||||||
ZSetSigningOrderForDocumentMutationSchema,
|
ZSetSigningOrderForDocumentMutationSchema,
|
||||||
ZSuccessResponseSchema,
|
ZSuccessResponseSchema,
|
||||||
@ -266,14 +268,15 @@ export const documentRouter = router({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
*
|
||||||
|
* Todo: Refactor to updateDocument.
|
||||||
*/
|
*/
|
||||||
updateDocument: authenticatedProcedure
|
setSettingsForDocument: authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/document/update',
|
path: '/document/update',
|
||||||
summary: 'Update document',
|
summary: 'Update document',
|
||||||
description: 'Update an existing document',
|
|
||||||
tags: ['Document'],
|
tags: ['Document'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -285,9 +288,9 @@ export const documentRouter = router({
|
|||||||
|
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
if (Object.keys(meta).length > 0) {
|
if (Object.values(meta).length > 0) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
userId,
|
userId: ctx.user.id,
|
||||||
teamId,
|
teamId,
|
||||||
documentId,
|
documentId,
|
||||||
subject: meta.subject,
|
subject: meta.subject,
|
||||||
@ -300,7 +303,6 @@ export const documentRouter = router({
|
|||||||
distributionMethod: meta.distributionMethod,
|
distributionMethod: meta.distributionMethod,
|
||||||
signingOrder: meta.signingOrder,
|
signingOrder: meta.signingOrder,
|
||||||
emailSettings: meta.emailSettings,
|
emailSettings: meta.emailSettings,
|
||||||
modifyNextSigner: meta.modifyNextSigner,
|
|
||||||
requestMetadata: ctx.metadata,
|
requestMetadata: ctx.metadata,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -467,6 +469,28 @@ export const documentRouter = router({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
selfSignDocument: authenticatedProcedure
|
||||||
|
.input(ZSelfSignDocumentMutationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const { documentId, teamId } = input;
|
||||||
|
|
||||||
|
return await selfSignDocument({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
documentId,
|
||||||
|
teamId,
|
||||||
|
requestMetadata: ctx.metadata,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'We were unable to self sign this document. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
|||||||
@ -251,7 +251,6 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
language: ZDocumentMetaLanguageSchema.optional(),
|
language: ZDocumentMetaLanguageSchema.optional(),
|
||||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||||
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
emailSettings: ZDocumentEmailSettingsSchema.optional(),
|
||||||
modifyNextSigner: z.boolean().optional(),
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@ -294,6 +293,11 @@ export const ZDistributeDocumentRequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZSelfSignDocumentMutationSchema = z.object({
|
||||||
|
documentId: z.number(),
|
||||||
|
teamId: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
|
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
|
||||||
|
|
||||||
export const ZSetPasswordForDocumentMutationSchema = z.object({
|
export const ZSetPasswordForDocumentMutationSchema = z.object({
|
||||||
|
|||||||
@ -437,14 +437,13 @@ export const recipientRouter = router({
|
|||||||
completeDocumentWithToken: procedure
|
completeDocumentWithToken: procedure
|
||||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { token, documentId, authOptions, nextSigner } = input;
|
const { token, documentId, authOptions } = input;
|
||||||
|
|
||||||
return await completeDocumentWithToken({
|
return await completeDocumentWithToken({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
authOptions,
|
authOptions,
|
||||||
userId: ctx.user?.id,
|
userId: ctx.user?.id,
|
||||||
nextSigner,
|
|
||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -212,12 +212,6 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
|||||||
token: z.string(),
|
token: z.string(),
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
authOptions: ZRecipientActionAuthSchema.optional(),
|
authOptions: ZRecipientActionAuthSchema.optional(),
|
||||||
nextSigner: z
|
|
||||||
.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
Type,
|
Type,
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { prop, sortBy } from 'remeda';
|
import { prop, sortBy } from 'remeda';
|
||||||
@ -120,6 +121,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
teamId,
|
teamId,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { data: session } = useSession();
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
||||||
@ -558,6 +560,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
}, [recipientsByRole]);
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
|
const hasSameOwnerAsRecipient =
|
||||||
|
recipientsByRole.SIGNER.length === 1 &&
|
||||||
|
recipientsByRole.SIGNER[0].email === session?.user?.email;
|
||||||
|
|
||||||
const handleAdvancedSettings = () => {
|
const handleAdvancedSettings = () => {
|
||||||
setShowAdvancedSettings((prev) => !prev);
|
setShowAdvancedSettings((prev) => !prev);
|
||||||
};
|
};
|
||||||
@ -1132,6 +1138,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
documentFlow.onBackStep?.();
|
documentFlow.onBackStep?.();
|
||||||
}}
|
}}
|
||||||
goBackLabel={canRenderBackButtonAsRemove ? msg`Remove` : undefined}
|
goBackLabel={canRenderBackButtonAsRemove ? msg`Remove` : undefined}
|
||||||
|
goNextLabel={hasSameOwnerAsRecipient ? msg`Sign` : undefined}
|
||||||
onGoNextClick={handleGoNextClick}
|
onGoNextClick={handleGoNextClick}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|||||||
@ -49,7 +49,6 @@ export type AddSignersFormProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
signingOrder?: DocumentSigningOrder | null;
|
signingOrder?: DocumentSigningOrder | null;
|
||||||
modifyNextSigner?: boolean | null;
|
|
||||||
isDocumentEnterprise: boolean;
|
isDocumentEnterprise: boolean;
|
||||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
@ -60,7 +59,6 @@ export const AddSignersFormPartial = ({
|
|||||||
recipients,
|
recipients,
|
||||||
fields,
|
fields,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
modifyNextSigner,
|
|
||||||
isDocumentEnterprise,
|
isDocumentEnterprise,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
@ -109,7 +107,6 @@ export const AddSignersFormPartial = ({
|
|||||||
)
|
)
|
||||||
: defaultRecipients,
|
: defaultRecipients,
|
||||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||||
modifyNextSigner: modifyNextSigner ?? false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -407,35 +404,6 @@ export const AddSignersFormPartial = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isSigningOrderSequential && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="modifyNextSigner"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
id="modifyNextSigner"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
field.onChange(checked);
|
|
||||||
}}
|
|
||||||
disabled={isSubmitting || hasDocumentBeenSent}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormLabel
|
|
||||||
htmlFor="modifyNextSigner"
|
|
||||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
<Trans>Modify next signer</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
sensors={[
|
sensors={[
|
||||||
|
|||||||
@ -25,7 +25,6 @@ export const ZAddSignersFormSchema = z
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
modifyNextSigner: z.boolean(),
|
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
|
|||||||
@ -183,12 +183,12 @@ const FormMessage = React.forwardRef<
|
|||||||
FormMessage.displayName = 'FormMessage';
|
FormMessage.displayName = 'FormMessage';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
useFormField,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
useFormField,
|
FormField,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user