Compare commits
24 Commits
feat/custo
...
feat/allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e9c7f1b11 | ||
|
|
bc2ec9a2d7 | ||
|
|
763b7f82c9 | ||
|
|
c670f64b1f | ||
|
|
369e16afab | ||
|
|
4a5f565591 | ||
|
|
f544eae2a6 | ||
|
|
a2ffd75c17 | ||
|
|
8619eec67a | ||
|
|
f325a04cb5 | ||
|
|
6a47b3a6e5 | ||
|
|
a7adb77e47 | ||
|
|
bfcbaea3a9 | ||
|
|
64964f420a | ||
|
|
2896673a23 | ||
|
|
b684b9574d | ||
|
|
12803d1a5e | ||
|
|
c41002313a | ||
|
|
516435fa2a | ||
|
|
0216af4ae8 | ||
|
|
3cde3cb7b2 | ||
|
|
071f5c546d | ||
|
|
9f9f6701c8 | ||
|
|
b01eaceeb8 |
@@ -14,4 +14,4 @@
|
||||
"public-api": "Public API",
|
||||
"embedding": "Embedding",
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
}
|
||||
@@ -85,13 +85,12 @@ You can also set the recipient's role, which determines their actions and permis
|
||||
|
||||
Documenso has 4 roles for recipients with different permissions and actions.
|
||||
|
||||
| Role | Function | Action required | Signature |
|
||||
| :-------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
||||
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
||||
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
||||
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
||||
| Assistant | Can help prepare the document by filling in fields on behalf of other signers. | Yes | No |
|
||||
| CC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
||||
| Role | Function | Action required | Signature |
|
||||
| :------: | :-----------------------------------------------------------------------------: | :-------------: | :-------: |
|
||||
| Signer | Needs to sign signatures fields assigned to them. | Yes | Yes |
|
||||
| Approver | Needs to approve the document as a whole. Signature optional. | Yes | Optional |
|
||||
| Viewer | Needs to confirm they viewed the document. | Yes | No |
|
||||
| BCC | Receives a copy of the signed document after completion. No action is required. | No | No |
|
||||
|
||||
### Fields
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.1-rc.2",
|
||||
"version": "1.9.1-rc.1",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,12 +14,6 @@ import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
SplitButton,
|
||||
SplitButtonAction,
|
||||
SplitButtonDropdown,
|
||||
SplitButtonDropdownItem,
|
||||
} from '@documenso/ui/primitives/split-button';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DocumentPageViewButtonProps = {
|
||||
@@ -31,9 +25,7 @@ export type DocumentPageViewButtonProps = {
|
||||
team?: Pick<Team, 'id' | 'url'>;
|
||||
};
|
||||
|
||||
export const DocumentPageViewButton = ({
|
||||
document: activeDocument,
|
||||
}: DocumentPageViewButtonProps) => {
|
||||
export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps) => {
|
||||
const { data: session } = useSession();
|
||||
const { toast } = useToast();
|
||||
const { _ } = useLingui();
|
||||
@@ -42,27 +34,25 @@ export const DocumentPageViewButton = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const recipient = activeDocument.recipients.find(
|
||||
(recipient) => recipient.email === session.user.email,
|
||||
);
|
||||
const recipient = document.recipients.find((recipient) => recipient.email === session.user.email);
|
||||
|
||||
const isRecipient = !!recipient;
|
||||
const isPending = activeDocument.status === DocumentStatus.PENDING;
|
||||
const isComplete = activeDocument.status === DocumentStatus.COMPLETED;
|
||||
const isPending = document.status === DocumentStatus.PENDING;
|
||||
const isComplete = document.status === DocumentStatus.COMPLETED;
|
||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const role = recipient?.role;
|
||||
|
||||
const documentsPath = formatDocumentsPath(activeDocument.team?.url);
|
||||
const documentsPath = formatDocumentsPath(document.team?.url);
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
||||
{
|
||||
documentId: activeDocument.id,
|
||||
documentId: document.id,
|
||||
},
|
||||
{
|
||||
context: {
|
||||
teamId: activeDocument.team?.id?.toString(),
|
||||
teamId: document.team?.id?.toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -73,10 +63,7 @@ export const DocumentPageViewButton = ({
|
||||
throw new Error('No document available');
|
||||
}
|
||||
|
||||
await downloadPDF({
|
||||
documentData,
|
||||
fileName: documentWithData.title,
|
||||
});
|
||||
await downloadPDF({ documentData, fileName: documentWithData.title });
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
@@ -86,100 +73,6 @@ export const DocumentPageViewButton = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadAuditLogClick = async () => {
|
||||
try {
|
||||
const { url } = await trpcClient.document.downloadAuditLogs.mutate({
|
||||
documentId: activeDocument.id,
|
||||
});
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
});
|
||||
|
||||
Object.assign(iframe.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '0',
|
||||
height: '0',
|
||||
});
|
||||
|
||||
const onLoaded = () => {
|
||||
if (iframe.contentDocument?.readyState === 'complete') {
|
||||
iframe.contentWindow?.print();
|
||||
|
||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||
iframe.addEventListener('load', onLoaded);
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
onLoaded();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadSigningCertificateClick = async () => {
|
||||
try {
|
||||
const { url } = await trpcClient.document.downloadCertificate.mutate({
|
||||
documentId: activeDocument.id,
|
||||
});
|
||||
|
||||
const iframe = Object.assign(document.createElement('iframe'), {
|
||||
src: url,
|
||||
});
|
||||
|
||||
Object.assign(iframe.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '0',
|
||||
height: '0',
|
||||
});
|
||||
|
||||
const onLoaded = () => {
|
||||
if (iframe.contentDocument?.readyState === 'complete') {
|
||||
iframe.contentWindow?.print();
|
||||
|
||||
iframe.contentWindow?.addEventListener('afterprint', () => {
|
||||
document.body.removeChild(iframe);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When the iframe has loaded, print the iframe and remove it from the dom
|
||||
iframe.addEventListener('load', onLoaded);
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
onLoaded();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`Sorry, we were unable to download the certificate. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return match({
|
||||
isRecipient,
|
||||
isPending,
|
||||
@@ -213,27 +106,16 @@ export const DocumentPageViewButton = ({
|
||||
))
|
||||
.with({ isComplete: false }, () => (
|
||||
<Button className="w-full" asChild>
|
||||
<Link href={`${documentsPath}/${activeDocument.id}/edit`}>
|
||||
<Link href={`${documentsPath}/${document.id}/edit`}>
|
||||
<Trans>Edit</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true }, () => (
|
||||
<SplitButton className="flex w-full">
|
||||
<SplitButtonAction className="w-full" onClick={() => void onDownloadClick()}>
|
||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</SplitButtonAction>
|
||||
<SplitButtonDropdown>
|
||||
<SplitButtonDropdownItem onClick={() => void onDownloadAuditLogClick()}>
|
||||
<Trans>Only Audit Log</Trans>
|
||||
</SplitButtonDropdownItem>
|
||||
|
||||
<SplitButtonDropdownItem onClick={() => void onDownloadSigningCertificateClick()}>
|
||||
<Trans>Only Signing Certificate</Trans>
|
||||
</SplitButtonDropdownItem>
|
||||
</SplitButtonDropdown>
|
||||
</SplitButton>
|
||||
<Button className="w-full" onClick={onDownloadClick}>
|
||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
))
|
||||
.otherwise(() => null);
|
||||
};
|
||||
|
||||
@@ -187,8 +187,6 @@ export const EditDocumentForm = ({
|
||||
title: data.title,
|
||||
externalId: data.externalId || null,
|
||||
visibility: data.visibility,
|
||||
includeSigningCertificate: data.includeSigningCertificate,
|
||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
||||
globalActionAuth: data.globalActionAuth ?? null,
|
||||
},
|
||||
|
||||
@@ -47,50 +47,22 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZAddRecipientsForNewDocumentSchema = z
|
||||
.object({
|
||||
distributeDocument: z.boolean(),
|
||||
useCustomDocument: z.boolean().default(false),
|
||||
customDocumentData: z
|
||||
.any()
|
||||
.refine((data) => data instanceof File || data === undefined)
|
||||
.optional(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
signingOrder: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
// Display exactly which rows are duplicates.
|
||||
.superRefine((items, ctx) => {
|
||||
const uniqueEmails = new Map<string, number>();
|
||||
|
||||
for (const [index, recipients] of items.recipients.entries()) {
|
||||
const email = recipients.email.toLowerCase();
|
||||
|
||||
const firstFoundIndex = uniqueEmails.get(email);
|
||||
|
||||
if (firstFoundIndex === undefined) {
|
||||
uniqueEmails.set(email, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Emails must be unique',
|
||||
path: ['recipients', index, 'email'],
|
||||
});
|
||||
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Emails must be unique',
|
||||
path: ['recipients', firstFoundIndex, 'email'],
|
||||
});
|
||||
}
|
||||
});
|
||||
const ZAddRecipientsForNewDocumentSchema = z.object({
|
||||
distributeDocument: z.boolean(),
|
||||
useCustomDocument: z.boolean().default(false),
|
||||
customDocumentData: z
|
||||
.any()
|
||||
.refine((data) => data instanceof File || data === undefined)
|
||||
.optional(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
signingOrder: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||
|
||||
|
||||
@@ -43,10 +43,9 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
||||
export interface RejectDocumentDialogProps {
|
||||
document: Pick<Document, 'id'>;
|
||||
token: string;
|
||||
onRejected?: (reason: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
|
||||
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -80,11 +79,7 @@ export function RejectDocumentDialog({ document, token, onRejected }: RejectDocu
|
||||
|
||||
setIsOpen(false);
|
||||
|
||||
if (onRejected) {
|
||||
await onRejected(reason);
|
||||
} else {
|
||||
router.push(`/sign/${token}/rejected`);
|
||||
}
|
||||
router.push(`/sign/${token}/rejected`);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
|
||||
@@ -41,7 +41,6 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
|
||||
includeSenderDetails: z.boolean(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
includeSigningCertificate: z.boolean(),
|
||||
includeAuditTrailLog: z.boolean(),
|
||||
});
|
||||
|
||||
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
|
||||
@@ -73,7 +72,6 @@ export const TeamDocumentPreferencesForm = ({
|
||||
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
||||
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
|
||||
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
||||
includeAuditTrailLog: settings?.includeAuditTrailLog ?? false,
|
||||
},
|
||||
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
||||
});
|
||||
@@ -88,7 +86,6 @@ export const TeamDocumentPreferencesForm = ({
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
typedSignatureEnabled,
|
||||
includeAuditTrailLog,
|
||||
} = data;
|
||||
|
||||
await updateTeamDocumentPreferences({
|
||||
@@ -99,7 +96,6 @@ export const TeamDocumentPreferencesForm = ({
|
||||
includeSenderDetails,
|
||||
typedSignatureEnabled,
|
||||
includeSigningCertificate,
|
||||
includeAuditTrailLog,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -304,37 +300,6 @@ export const TeamDocumentPreferencesForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditTrailLog"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Include the Audit Trail Log in the Document</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormControl className="block">
|
||||
<Switch
|
||||
ref={field.ref}
|
||||
name={field.name}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
<FormDescription>
|
||||
<Trans>
|
||||
Controls whether the audit trail log will be included in the document when it is
|
||||
downloaded. The audit trail log can still be downloaded from the logs page
|
||||
separately.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row justify-end space-x-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
<Trans>Save</Trans>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { XCircle } from 'lucide-react';
|
||||
|
||||
import type { Signature } from '@documenso/prisma/client';
|
||||
|
||||
export type EmbedDocumentRejectedPageProps = {
|
||||
name?: string;
|
||||
signature?: Signature;
|
||||
};
|
||||
|
||||
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
|
||||
return (
|
||||
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<XCircle className="text-destructive h-10 w-10" />
|
||||
|
||||
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||
<Trans>Document Rejected</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="text-destructive mt-4 flex items-center text-center text-sm">
|
||||
<Trans>You have rejected this document</Trans>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
|
||||
<Trans>
|
||||
The document owner has been notified of your decision. They may contact you with further
|
||||
instructions if necessary.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
|
||||
<Trans>No further action is required from you at this time.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,13 +10,7 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||
import {
|
||||
type DocumentData,
|
||||
type Field,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@documenso/prisma/client';
|
||||
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
@@ -32,13 +26,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
|
||||
import { Logo } from '~/components/branding/logo';
|
||||
|
||||
import { EmbedClientLoading } from '../../client-loading';
|
||||
import { EmbedDocumentCompleted } from '../../completed';
|
||||
import { EmbedDocumentFields } from '../../document-fields';
|
||||
import { EmbedDocumentRejected } from '../../rejected';
|
||||
import { injectCss } from '../../util';
|
||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||
|
||||
@@ -83,9 +75,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||
const [hasRejectedDocument, setHasRejectedDocument] = useState(
|
||||
recipient.signingStatus === SigningStatus.REJECTED,
|
||||
);
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||
);
|
||||
@@ -94,8 +83,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||
|
||||
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||
|
||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||
|
||||
@@ -174,25 +161,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onDocumentRejected = (reason: string) => {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: 'document-rejected',
|
||||
data: {
|
||||
token,
|
||||
documentId,
|
||||
recipientId: recipient.id,
|
||||
reason,
|
||||
},
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
setHasRejectedDocument(true);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
|
||||
@@ -206,7 +174,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
// Since a recipient can be provided a name we can lock it without requiring
|
||||
// a to be provided by the parent application, unlike direct templates.
|
||||
setIsNameLocked(!!data.lockName);
|
||||
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||
|
||||
if (data.darkModeDisabled) {
|
||||
document.documentElement.classList.add('dark-mode-disabled');
|
||||
@@ -241,10 +208,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
}
|
||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||
|
||||
if (hasRejectedDocument) {
|
||||
return <EmbedDocumentRejected name={fullName} />;
|
||||
}
|
||||
|
||||
if (hasCompletedDocument) {
|
||||
return (
|
||||
<EmbedDocumentCompleted
|
||||
@@ -266,16 +229,6 @@ export const EmbedSignDocumentClientPage = ({
|
||||
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||
|
||||
{allowDocumentRejection && (
|
||||
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
|
||||
<RejectDocumentDialog
|
||||
document={{ id: documentId }}
|
||||
token={token}
|
||||
onRejected={onDocumentRejected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||
{/* Viewer */}
|
||||
<div className="embed--DocumentViewer flex-1">
|
||||
@@ -467,7 +420,7 @@ export const EmbedSignDocumentClientPage = ({
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
|
||||
className="col-start-2"
|
||||
disabled={
|
||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||
}
|
||||
|
||||
@@ -13,5 +13,4 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
||||
.optional()
|
||||
.transform((value) => value || undefined),
|
||||
lockName: z.boolean().optional().default(false),
|
||||
allowDocumentRejection: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -363,40 +363,6 @@ export const DocumentHistorySheet = ({
|
||||
]}
|
||||
/>
|
||||
))
|
||||
.with(
|
||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED },
|
||||
({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Old',
|
||||
value: data.from,
|
||||
},
|
||||
{
|
||||
key: 'New',
|
||||
value: data.to,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.with(
|
||||
{ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED },
|
||||
({ data }) => (
|
||||
<DocumentHistorySheetChanges
|
||||
values={[
|
||||
{
|
||||
key: 'Old',
|
||||
value: data.from,
|
||||
},
|
||||
{
|
||||
key: 'New',
|
||||
value: data.to,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
.exhaustive()}
|
||||
|
||||
{isUserDetailsVisible && (
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.1-rc.2",
|
||||
"version": "1.9.1-rc.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.1-rc.2",
|
||||
"version": "1.9.1-rc.1",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -106,7 +106,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.1-rc.2",
|
||||
"version": "1.9.1-rc.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
@@ -35722,6 +35722,21 @@
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz",
|
||||
"integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.1-rc.2",
|
||||
"version": "1.9.1-rc.1",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@@ -270,12 +270,11 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
)
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.map((signer) => signer.email.toLowerCase());
|
||||
const ids = schema.map((signer) => signer.id);
|
||||
|
||||
return new Set(emails).size === emails.length && new Set(ids).size === ids.length;
|
||||
return new Set(ids).size === ids.length;
|
||||
},
|
||||
{ message: 'Recipient IDs and emails must be unique' },
|
||||
{ message: 'Recipient IDs must be unique' },
|
||||
),
|
||||
meta: z
|
||||
.object({
|
||||
|
||||
304
packages/app-tests/e2e/document-flow/fields-step.spec.ts
Normal file
304
packages/app-tests/e2e/document-flow/fields-step.spec.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[DOCUMENT_FLOW]: add signature fields for unique recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// Add 2 signers.
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByLabel('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Advanced settings should not be visible for non EE users.
|
||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
||||
|
||||
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('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.waitForURL('**/documents');
|
||||
|
||||
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: add signature fields for duplicate recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByLabel('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByLabel('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for each recipient
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient (recipient@documenso.com)' }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient (recipient@documenso.com)' }).nth(2).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 300,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.waitForURL('**/documents');
|
||||
|
||||
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: add signature fields for recipients with different roles', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// Add a signer
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add an approver
|
||||
await page.getByLabel('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add a viewer
|
||||
await page.getByLabel('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add a CC
|
||||
await page.getByLabel('Email').nth(3).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(3).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for signer and approver
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page
|
||||
.getByRole('option', { name: 'Documenso Recipient (recipient@documenso.com)' })
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.waitForURL('**/documents');
|
||||
|
||||
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: add signature fields for mixed recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// First recipient (unique)
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('First Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Second recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Third recipient (unique)
|
||||
await page.getByLabel('Email').nth(2).fill('recipient2@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Second Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fourth recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(3).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(3).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fifth recipient (unique)
|
||||
await page.getByLabel('Email').nth(4).fill('recipient3@documenso.com');
|
||||
await page.getByLabel('Name').nth(4).fill('Third Recipient');
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for approver and signers
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Second Recipient (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Third Recipient (recipient3@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 300,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Distribute Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
|
||||
await page.waitForURL('**/documents');
|
||||
|
||||
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
|
||||
});
|
||||
@@ -91,3 +91,191 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: add only recipients with the same email address', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByLabel('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
await page.getByLabel('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: duplicate email recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// Add a signer
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add an approver
|
||||
await page.getByLabel('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add a viewer
|
||||
await page.getByLabel('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Add a CC
|
||||
await page.getByLabel('Email').nth(3).fill('recipient@documenso.com');
|
||||
await page.getByLabel('Name').nth(3).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: same email with different roles', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// First recipient (unique)
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('First Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Second recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Third recipient (unique)
|
||||
await page.getByLabel('Email').nth(2).fill('recipient2@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Second Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fourth recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(3).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(3).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fifth recipient (unique)
|
||||
await page.getByLabel('Email').nth(4).fill('recipient3@documenso.com');
|
||||
await page.getByLabel('Name').nth(4).fill('Third Recipient');
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[DOCUMENT_FLOW]: mixed unique and duplicate recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const document = await seedBlankDocument(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/documents/${document.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
// First recipient (unique)
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('First Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Second recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(1).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Third recipient (unique)
|
||||
await page.getByLabel('Email').nth(2).fill('recipient2@documenso.com');
|
||||
await page.getByLabel('Name').nth(2).fill('Second Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fourth recipient (duplicate of first)
|
||||
await page.getByLabel('Email').nth(3).fill('recipient1@documenso.com');
|
||||
await page.getByLabel('Name').nth(3).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
await page.getByRole('button', { name: 'Add Signer' }).click();
|
||||
|
||||
// Fifth recipient (unique)
|
||||
await page.getByLabel('Email').nth(4).fill('recipient3@documenso.com');
|
||||
await page.getByLabel('Name').nth(4).fill('Third Recipient');
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEMPLATE_FLOW]: add signature fields for unique recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
||||
|
||||
// Add 2 placeholder recipients.
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
|
||||
// Advanced settings should not be visible for non EE users.
|
||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
||||
|
||||
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('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save Template' }).click();
|
||||
|
||||
await page.waitForURL('**/templates');
|
||||
|
||||
await expect(page.getByRole('link', { name: template.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: add signature fields for duplicate recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for each recipient
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient (recipient@documenso.com)' }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient (recipient@documenso.com)' }).nth(2).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 300,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save Template' }).click();
|
||||
|
||||
await page.waitForURL('**/templates');
|
||||
|
||||
await expect(page.getByRole('link', { name: template.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: add signature fields for recipients with different roles', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
||||
|
||||
// Add a placeholder recipient
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add an approver
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add a viewer
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add a CC
|
||||
await page.getByPlaceholder('Email').nth(3).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(3).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for signer and approver
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page
|
||||
.getByRole('option', { name: 'Documenso Recipient (recipient@documenso.com)' })
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save Template' }).click();
|
||||
|
||||
await page.waitForURL('**/templates');
|
||||
|
||||
await expect(page.getByRole('link', { name: template.title })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: add signature fields for mixed recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible();
|
||||
|
||||
// First placeholder recipient (unique)
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('First Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Second placeholder recipient (duplicate of first)
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Third placeholder recipient (unique)
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Second Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Fourth placeholder recipient (duplicate of first)
|
||||
await page.getByPlaceholder('Email').nth(3).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(3).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Fifth placeholder recipient (unique)
|
||||
await page.getByPlaceholder('Email').nth(4).fill('recipient3@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(4).fill('Third Recipient');
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for approver and signers
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Second Recipient (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Third Recipient (recipient3@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 300,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save Template' }).click();
|
||||
|
||||
await page.waitForURL('**/templates');
|
||||
|
||||
await expect(page.getByRole('link', { name: template.title })).toBeVisible();
|
||||
});
|
||||
@@ -98,3 +98,135 @@ test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => {
|
||||
// Advanced settings should not be visible for non EE users.
|
||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: duplicate recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Recipient');
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: same email different roles', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
|
||||
// Add a signer
|
||||
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add an approver
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add a viewer
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Add a CC
|
||||
await page.getByPlaceholder('Email').nth(3).fill('recipient@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(3).fill('Documenso Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('[TEMPLATE_FLOW]: mixed recipients', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
|
||||
// First recipient (unique)
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('First Recipient');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Needs to approve').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Second recipient (duplicate of first)
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(1).click();
|
||||
await page.getByLabel('Needs to view').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Third recipient (unique)
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Second Recipient');
|
||||
await page.getByRole('combobox').nth(2).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Fourth recipient (duplicate of first)
|
||||
await page.getByPlaceholder('Email').nth(3).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(3).fill('First Recipient');
|
||||
await page.getByRole('combobox').nth(3).click();
|
||||
await page.getByLabel('Receives copy').click();
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
// Fifth recipient (unique)
|
||||
await page.getByPlaceholder('Email').nth(4).fill('recipient3@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(4).fill('Third Recipient');
|
||||
await page.getByRole('combobox').nth(4).click();
|
||||
await page.getByLabel('Needs to sign').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Go Back' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -110,14 +110,13 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
||||
await page.getByRole('button', { name: 'Save template' }).click();
|
||||
|
||||
// Use template
|
||||
await page.waitForURL('/templates');
|
||||
await page.waitForURL('**/templates');
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the correct values.
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -250,9 +249,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the correct values.
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -353,9 +351,8 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the custom document data
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -434,9 +431,8 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the custom document data
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -500,9 +496,8 @@ test('[TEMPLATE]: should create a document from a template using template docume
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the template's document data
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -591,9 +586,8 @@ test('[TEMPLATE]: should persist document visibility when creating from template
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the correct visibility
|
||||
await page.waitForURL(/documents/);
|
||||
|
||||
const documentId = Number(page.url().split('/').pop());
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -616,3 +610,368 @@ test('[TEMPLATE]: should persist document visibility when creating from template
|
||||
// Template should not be visible to regular member
|
||||
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test verifies that we can create a document from a template with duplicate recipients
|
||||
**/
|
||||
test('[TEMPLATE]: should create a document from a template with duplicate recipients', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
const isBillingEnabled =
|
||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId;
|
||||
|
||||
await seedUserSubscription({
|
||||
userId: user.id,
|
||||
priceId: enterprisePriceId,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
// Set template title.
|
||||
await page.getByLabel('Title').fill('TEMPLATE_TITLE');
|
||||
|
||||
// Set template document access.
|
||||
await page.getByTestId('documentAccessSelectValue').click();
|
||||
await page.getByLabel('Require account').getByText('Require account').click();
|
||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||
|
||||
// Set EE action auth.
|
||||
if (isBillingEnabled) {
|
||||
await page.getByTestId('documentActionSelectValue').click();
|
||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||
}
|
||||
|
||||
// Set email options.
|
||||
await page.getByRole('button', { name: 'Email Options' }).click();
|
||||
await page.getByLabel('Subject (Optional)').fill('SUBJECT');
|
||||
await page.getByLabel('Message (Optional)').fill('MESSAGE');
|
||||
|
||||
// Set advanced options.
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click();
|
||||
await page.getByLabel('DD/MM/YYYY').click();
|
||||
|
||||
await page.locator('.time-zone-field').click();
|
||||
await page.getByRole('option', { name: 'Etc/UTC' }).click();
|
||||
await page.getByLabel('Redirect URL').fill('https://documenso.com');
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
|
||||
// Add 2 signers.
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 1');
|
||||
|
||||
// Apply require passkey for Recipient 1.
|
||||
if (isBillingEnabled) {
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByLabel('Require passkey').click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for each recipient
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save template' }).click();
|
||||
|
||||
// Use template
|
||||
await page.waitForURL('**/templates');
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the correct values.
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const documentAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
||||
isBillingEnabled ? 'PASSKEY' : null,
|
||||
);
|
||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
||||
|
||||
const recipientOne = document.recipients[0];
|
||||
const recipientTwo = document.recipients[1];
|
||||
|
||||
const recipientOneAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipientOne.authOptions,
|
||||
});
|
||||
|
||||
const recipientTwoAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipientTwo.authOptions,
|
||||
});
|
||||
|
||||
if (isBillingEnabled) {
|
||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
||||
}
|
||||
|
||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
|
||||
await page.getByRole('link', { name: 'Edit' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await expect(page.getByText('SignatureRE').first()).toBeVisible();
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'recipient1@documenso.com' }).nth(1).click();
|
||||
|
||||
await expect(page.getByText('SignatureRE').nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* This test verifies that we can create a document from a template with a mix of duplicate and unique recipients
|
||||
**/
|
||||
test('[TEMPLATE]: should create a document from a template with mixed duplicate and unique recipients', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const template = await seedBlankTemplate(user);
|
||||
|
||||
const isBillingEnabled =
|
||||
process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' && enterprisePriceId;
|
||||
|
||||
await seedUserSubscription({
|
||||
userId: user.id,
|
||||
priceId: enterprisePriceId,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
// Set template title.
|
||||
await page.getByLabel('Title').fill('TEMPLATE_MIXED_RECIPIENTS');
|
||||
|
||||
// Set template document access.
|
||||
await page.getByTestId('documentAccessSelectValue').click();
|
||||
await page.getByLabel('Require account').getByText('Require account').click();
|
||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||
|
||||
// Set EE action auth.
|
||||
if (isBillingEnabled) {
|
||||
await page.getByTestId('documentActionSelectValue').click();
|
||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||
}
|
||||
|
||||
// Set email options.
|
||||
await page.getByRole('button', { name: 'Email Options' }).click();
|
||||
await page.getByLabel('Subject (Optional)').fill('SUBJECT');
|
||||
await page.getByLabel('Message (Optional)').fill('MESSAGE');
|
||||
|
||||
// Set advanced options.
|
||||
await page.getByRole('button', { name: 'Advanced Options' }).click();
|
||||
await page.locator('button').filter({ hasText: 'YYYY-MM-DD HH:mm a' }).click();
|
||||
await page.getByLabel('DD/MM/YYYY').click();
|
||||
|
||||
await page.locator('.time-zone-field').click();
|
||||
await page.getByRole('option', { name: 'Etc/UTC' }).click();
|
||||
await page.getByLabel('Redirect URL').fill('https://documenso.com');
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||
|
||||
// Add 4 signers: 2 duplicates of recipient1 and 2 unique recipients
|
||||
await page.getByPlaceholder('Email').fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(1).fill('recipient2@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(1).fill('Recipient 2');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(2).fill('recipient1@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(2).fill('Recipient 1');
|
||||
await page.getByRole('button', { name: 'Add Placeholder Recipient' }).click();
|
||||
|
||||
await page.getByPlaceholder('Email').nth(3).fill('recipient3@documenso.com');
|
||||
await page.getByPlaceholder('Name').nth(3).fill('Recipient 3');
|
||||
|
||||
// Apply require passkey for first instance of Recipient 1
|
||||
if (isBillingEnabled) {
|
||||
await page.getByLabel('Show advanced settings').check();
|
||||
await page.getByRole('combobox').first().click();
|
||||
await page.getByLabel('Require passkey').click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
// Add signature fields for each recipient
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 1 (recipient1@documenso.com)' }).nth(1).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 300,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Recipient 3 (recipient3@documenso.com)' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Signature' }).click();
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 400,
|
||||
y: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Save template' }).click();
|
||||
|
||||
// Use template
|
||||
await page.waitForURL('**/templates');
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||
|
||||
// Review that the document was created with the correct values.
|
||||
await page.waitForURL(/\/documents\/\d+/);
|
||||
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||
|
||||
const document = await prisma.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: {
|
||||
email: 'asc',
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const documentAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
});
|
||||
|
||||
expect(document.title).toEqual('TEMPLATE_MIXED_RECIPIENTS');
|
||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
||||
isBillingEnabled ? 'PASSKEY' : null,
|
||||
);
|
||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||
expect(document.documentMeta?.subject).toEqual('SUBJECT');
|
||||
expect(document.documentMeta?.timezone).toEqual('Etc/UTC');
|
||||
|
||||
// Check auth settings for first instance of recipient1
|
||||
const firstRecipientOne = document.recipients[0];
|
||||
const firstRecipientOneAuth = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: firstRecipientOne.authOptions,
|
||||
});
|
||||
|
||||
if (isBillingEnabled) {
|
||||
expect(firstRecipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
||||
}
|
||||
|
||||
expect(firstRecipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||
|
||||
await page.getByRole('link', { name: 'Edit' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||
|
||||
await expect(page.getByText('SignatureRE').first()).toBeVisible();
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'recipient2@documenso.com' }).click();
|
||||
await expect(page.getByText('SignatureRE').nth(1)).toBeVisible();
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'recipient1@documenso.com' }).nth(1).click();
|
||||
await expect(page.getByText('SignatureRE').nth(2)).toBeVisible();
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'recipient3@documenso.com' }).click();
|
||||
await expect(page.getByText('SignatureRE').nth(3)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
documentLanguage: z.string(),
|
||||
includeSenderDetails: z.boolean(),
|
||||
includeSigningCertificate: z.boolean(),
|
||||
includeAuditTrailLog: z.boolean(),
|
||||
brandingEnabled: z.boolean(),
|
||||
brandingLogo: z.string(),
|
||||
brandingUrl: z.string(),
|
||||
|
||||
@@ -13,7 +13,6 @@ import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||
import { flattenForm } from '../../../server-only/pdf/flatten-form';
|
||||
@@ -58,7 +57,6 @@ export const run = async ({
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,36 +121,13 @@ export const run = async ({
|
||||
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
let includeSigningCertificate;
|
||||
|
||||
if (document.teamId) {
|
||||
includeSigningCertificate =
|
||||
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
||||
} else {
|
||||
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
||||
}
|
||||
|
||||
const certificateData = includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
let includeAuditTrailLog;
|
||||
|
||||
if (document.teamId) {
|
||||
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
||||
} else {
|
||||
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
||||
}
|
||||
|
||||
const auditLogData = includeAuditTrailLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||
const pdfDoc = await PDFDocument.load(pdfData);
|
||||
@@ -175,16 +150,6 @@ export const run = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLog = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await pdfDoc.copyPages(auditLog, auditLog.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
pdfDoc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.inserted) {
|
||||
await insertFieldInPDF(pdfDoc, field);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@documenso/prisma/client';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import {
|
||||
@@ -73,13 +72,6 @@ export const completeDocumentWithToken = async ({
|
||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||
}
|
||||
|
||||
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Recipient has already rejected the document',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||
|
||||
|
||||
@@ -124,8 +124,6 @@ export const createDocument = async ({
|
||||
team?.teamGlobalSettings?.documentVisibility,
|
||||
userTeamRole ?? TeamMemberRole.MEMBER,
|
||||
),
|
||||
includeSigningCertificate: team?.teamGlobalSettings?.includeSigningCertificate ?? true,
|
||||
includeAuditTrailLog: team?.teamGlobalSettings?.includeAuditTrailLog ?? true,
|
||||
formValues,
|
||||
source: DocumentSource.DOCUMENT,
|
||||
documentMeta: {
|
||||
|
||||
@@ -22,7 +22,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putPdfFile } from '../../universal/upload/put-file';
|
||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||
import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||
import { flattenForm } from '../pdf/flatten-form';
|
||||
@@ -62,7 +61,6 @@ export const sealDocument = async ({
|
||||
teamGlobalSettings: {
|
||||
select: {
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -111,36 +109,13 @@ export const sealDocument = async ({
|
||||
// !: Need to write the fields onto the document as a hard copy
|
||||
const pdfData = await getFile(documentData);
|
||||
|
||||
let includeSigningCertificate;
|
||||
|
||||
if (document.teamId) {
|
||||
includeSigningCertificate =
|
||||
document.team?.teamGlobalSettings?.includeSigningCertificate ?? true;
|
||||
} else {
|
||||
includeSigningCertificate = document.includeSigningCertificate ?? true;
|
||||
}
|
||||
|
||||
const certificateData = includeSigningCertificate
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
let includeAuditTrailLog;
|
||||
|
||||
if (document.teamId) {
|
||||
includeAuditTrailLog = document.team?.teamGlobalSettings?.includeAuditTrailLog ?? true;
|
||||
} else {
|
||||
includeAuditTrailLog = document.includeAuditTrailLog ?? true;
|
||||
}
|
||||
|
||||
const auditLogData = includeAuditTrailLog
|
||||
? await getAuditLogsPdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
const certificateData =
|
||||
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
|
||||
? await getCertificatePdf({
|
||||
documentId,
|
||||
language: document.documentMeta?.language,
|
||||
}).catch(() => null)
|
||||
: null;
|
||||
|
||||
const doc = await PDFDocument.load(pdfData);
|
||||
|
||||
@@ -159,16 +134,6 @@ export const sealDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (auditLogData) {
|
||||
const auditLog = await PDFDocument.load(auditLogData);
|
||||
|
||||
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
|
||||
|
||||
auditLogPages.forEach((page) => {
|
||||
doc.addPage(page);
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
await insertFieldInPDF(doc, field);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ export type UpdateDocumentOptions = {
|
||||
title?: string;
|
||||
externalId?: string | null;
|
||||
visibility?: DocumentVisibility | null;
|
||||
includeSigningCertificate?: boolean;
|
||||
includeAuditTrailLog?: boolean;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
||||
};
|
||||
@@ -158,12 +156,6 @@ export const updateDocument = async ({
|
||||
documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
|
||||
const isDocumentVisibilitySame =
|
||||
data.visibility === undefined || data.visibility === document.visibility;
|
||||
const isIncludeSigningCertificateSame =
|
||||
data.includeSigningCertificate === undefined ||
|
||||
data.includeSigningCertificate === document.includeSigningCertificate;
|
||||
const isIncludeAuditTrailLogSame =
|
||||
data.includeAuditTrailLog === undefined ||
|
||||
data.includeAuditTrailLog === document.includeAuditTrailLog;
|
||||
|
||||
const auditLogs: CreateDocumentAuditLogDataResponse[] = [];
|
||||
|
||||
@@ -243,34 +235,6 @@ export const updateDocument = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isIncludeSigningCertificateSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: String(document.includeSigningCertificate),
|
||||
to: String(data.includeSigningCertificate || false),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isIncludeAuditTrailLogSame) {
|
||||
auditLogs.push(
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED,
|
||||
documentId,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
from: String(document.includeAuditTrailLog),
|
||||
to: String(data.includeAuditTrailLog || false),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Early return if nothing is required.
|
||||
if (auditLogs.length === 0) {
|
||||
return document;
|
||||
@@ -290,8 +254,6 @@ export const updateDocument = async ({
|
||||
title: data.title,
|
||||
externalId: data.externalId,
|
||||
visibility: data.visibility as DocumentVisibility,
|
||||
includeSigningCertificate: data.includeSigningCertificate,
|
||||
includeAuditTrailLog: data.includeAuditTrailLog,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,6 +72,22 @@ export const setFieldsForDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Check that every signer has a signature field
|
||||
const signers = document.recipients.filter((recipient) => recipient.role === 'SIGNER');
|
||||
const hasEverySignerSignature = signers.every((signer) =>
|
||||
fields.some(
|
||||
(field) =>
|
||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||
field.recipientId === signer.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (!hasEverySignerSignature) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Every signer must have at least one signature field',
|
||||
});
|
||||
}
|
||||
|
||||
if (document.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
@@ -94,9 +110,7 @@ export const setFieldsForDocument = async ({
|
||||
const linkedFields = fields.map((field) => {
|
||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
const recipient = document.recipients.find(
|
||||
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
|
||||
);
|
||||
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
@@ -236,10 +250,8 @@ export const setFieldsForDocument = async ({
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
documentId_email: {
|
||||
documentId,
|
||||
email: fieldSignerEmail,
|
||||
},
|
||||
documentId,
|
||||
id: field.recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -340,6 +352,7 @@ type FieldData = {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerEmail: string;
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
|
||||
@@ -22,6 +22,7 @@ export type SetFieldsForTemplateOptions = {
|
||||
fields: {
|
||||
id?: number | null;
|
||||
type: FieldType;
|
||||
signerId: number;
|
||||
signerEmail: string;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
@@ -57,12 +58,29 @@ export const setFieldsForTemplate = async ({
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new Error('Template not found');
|
||||
}
|
||||
|
||||
// Check that every signer has a signature field
|
||||
const signers = template.recipients.filter((recipient) => recipient.role === 'SIGNER');
|
||||
const hasEverySignerSignature = signers.every((signer) =>
|
||||
fields.some(
|
||||
(field) =>
|
||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||
field.signerId === signer.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (!hasEverySignerSignature) {
|
||||
throw new Error('Every signer must have at least one signature field');
|
||||
}
|
||||
|
||||
const existingFields = await prisma.field.findMany({
|
||||
where: {
|
||||
templateId,
|
||||
@@ -180,10 +198,8 @@ export const setFieldsForTemplate = async ({
|
||||
},
|
||||
recipient: {
|
||||
connect: {
|
||||
templateId_email: {
|
||||
templateId,
|
||||
email: field.signerEmail.toLowerCase(),
|
||||
},
|
||||
templateId,
|
||||
id: field.signerId,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Browser } from 'playwright';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
|
||||
import { encryptSecondaryData } from '../crypto/encrypt';
|
||||
|
||||
export type GetAuditLogsPdfParams = {
|
||||
documentId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
language?: SupportedLanguageCodes | (string & {});
|
||||
};
|
||||
|
||||
export const getAuditLogsPdf = async ({ documentId, language }: GetAuditLogsPdfParams) => {
|
||||
const { chromium } = await import('playwright');
|
||||
|
||||
const encryptedId = encryptSecondaryData({
|
||||
data: documentId.toString(),
|
||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_BROWSERLESS_URL) {
|
||||
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
|
||||
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
|
||||
browser = await chromium.connectOverCDP(process.env.NEXT_PRIVATE_BROWSERLESS_URL);
|
||||
} else {
|
||||
browser = await chromium.launch();
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
throw new Error(
|
||||
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
|
||||
);
|
||||
}
|
||||
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
const lang = isValidLanguageCode(language) ? language : 'en';
|
||||
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: 'language',
|
||||
value: lang,
|
||||
url: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const result = await page.pdf({
|
||||
format: 'A4',
|
||||
});
|
||||
|
||||
await browserContext.close();
|
||||
|
||||
void browser.close();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
await browserContext.close();
|
||||
|
||||
void browser.close();
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -125,16 +125,12 @@ export const setDocumentRecipients = async ({
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
!normalizedRecipients.find(
|
||||
(recipient) =>
|
||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
||||
),
|
||||
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||
);
|
||||
|
||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -103,10 +103,7 @@ export const setTemplateRecipients = async ({
|
||||
|
||||
const removedRecipients = existingRecipients.filter(
|
||||
(existingRecipient) =>
|
||||
!normalizedRecipients.find(
|
||||
(recipient) =>
|
||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
||||
),
|
||||
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||
);
|
||||
|
||||
if (template.directLink !== null) {
|
||||
@@ -133,14 +130,10 @@ export const setTemplateRecipients = async ({
|
||||
|
||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||
const existing = existingRecipients.find(
|
||||
(existingRecipient) =>
|
||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
_persisted: existing,
|
||||
};
|
||||
return { ...recipient, _persisted: existing };
|
||||
});
|
||||
|
||||
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ export type UpdateTeamDocumentSettingsOptions = {
|
||||
includeSenderDetails: boolean;
|
||||
typedSignatureEnabled: boolean;
|
||||
includeSigningCertificate: boolean;
|
||||
includeAuditTrailLog: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,7 +36,6 @@ export const updateTeamDocumentSettings = async ({
|
||||
documentLanguage,
|
||||
includeSenderDetails,
|
||||
includeSigningCertificate,
|
||||
includeAuditTrailLog,
|
||||
typedSignatureEnabled,
|
||||
} = settings;
|
||||
|
||||
@@ -63,7 +61,6 @@ export const updateTeamDocumentSettings = async ({
|
||||
includeSenderDetails,
|
||||
typedSignatureEnabled,
|
||||
includeSigningCertificate,
|
||||
includeAuditTrailLog,
|
||||
},
|
||||
update: {
|
||||
documentVisibility,
|
||||
@@ -71,7 +68,6 @@ export const updateTeamDocumentSettings = async ({
|
||||
includeSenderDetails,
|
||||
typedSignatureEnabled,
|
||||
includeSigningCertificate,
|
||||
includeAuditTrailLog,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -141,10 +141,8 @@ export const createDocumentFromTemplateLegacy = async ({
|
||||
|
||||
return await prisma.recipient.upsert({
|
||||
where: {
|
||||
documentId_email: {
|
||||
documentId: document.id,
|
||||
email: existingRecipient?.email ?? recipient.email,
|
||||
},
|
||||
documentId: document.id,
|
||||
id: existingRecipient?.id,
|
||||
},
|
||||
update: {
|
||||
name: recipient.name,
|
||||
|
||||
@@ -256,10 +256,21 @@ export const createDocumentFromTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const recipientMapping = new Map<number, number>();
|
||||
|
||||
template.recipients.forEach((templateRecipient, index) => {
|
||||
const documentRecipient = document.recipients[index];
|
||||
|
||||
if (documentRecipient) {
|
||||
recipientMapping.set(templateRecipient.id, documentRecipient.id);
|
||||
}
|
||||
});
|
||||
|
||||
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||
|
||||
Object.values(finalRecipients).forEach(({ email, fields }) => {
|
||||
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
||||
finalRecipients.forEach(({ templateRecipientId, fields }) => {
|
||||
const documentRecipientId = recipientMapping.get(templateRecipientId);
|
||||
const recipient = document.recipients.find((r) => r.id === documentRecipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
|
||||
@@ -29,9 +29,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
|
||||
'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_PREFILLED', // When a field is prefilled by an assistant.
|
||||
'DOCUMENT_VISIBILITY_UPDATED', // When the document visibility scope is updated.
|
||||
'DOCUMENT_SIGNING_CERTIFICATE_UPDATED', // When the include signing certificate is updated.
|
||||
'DOCUMENT_AUDIT_TRAIL_UPDATED', // When the include audit trail 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_ACTION_UPDATED', // When the global action authentication is updated.
|
||||
'DOCUMENT_META_UPDATED', // When the document meta data is updated.
|
||||
@@ -399,16 +397,6 @@ export const ZDocumentAuditLogEventDocumentVisibilitySchema = z.object({
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED),
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
export const ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema = z.object({
|
||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED),
|
||||
data: ZGenericFromToSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Event: Document global authentication access updated.
|
||||
*/
|
||||
@@ -586,8 +574,6 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
|
||||
ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
|
||||
ZDocumentAuditLogEventDocumentFieldPrefilledSchema,
|
||||
ZDocumentAuditLogEventDocumentVisibilitySchema,
|
||||
ZDocumentAuditLogEventDocumentSigningCertificateUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentAuditTrailUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthAccessUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentGlobalAuthActionUpdatedSchema,
|
||||
ZDocumentAuditLogEventDocumentMetaUpdatedSchema,
|
||||
|
||||
@@ -18,8 +18,6 @@ import { ZRecipientLiteSchema } from './recipient';
|
||||
*/
|
||||
export const ZDocumentSchema = DocumentSchema.pick({
|
||||
visibility: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
status: true,
|
||||
source: true,
|
||||
id: true,
|
||||
@@ -84,8 +82,6 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -108,8 +104,6 @@ export const ZDocumentManySchema = DocumentSchema.pick({
|
||||
deletedAt: true,
|
||||
teamId: true,
|
||||
templateId: true,
|
||||
includeSigningCertificate: true,
|
||||
includeAuditTrailLog: true,
|
||||
}).extend({
|
||||
user: UserSchema.pick({
|
||||
id: true,
|
||||
|
||||
@@ -322,14 +322,6 @@ export const formatDocumentAuditLogAction = (
|
||||
anonymous: msg`Document visibility updated`,
|
||||
identified: msg`${prefix} updated the document visibility`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_CERTIFICATE_UPDATED }, () => ({
|
||||
anonymous: msg`Document signing certificate updated`,
|
||||
identified: msg`${prefix} updated the document signing certificate`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_AUDIT_TRAIL_UPDATED }, () => ({
|
||||
anonymous: msg`Document audit trail updated`,
|
||||
identified: msg`${prefix} updated the document audit trail`,
|
||||
}))
|
||||
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED }, () => ({
|
||||
anonymous: msg`Document access auth updated`,
|
||||
identified: msg`${prefix} updated the document access auth requirements`,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_documentId_email_key";
|
||||
@@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "Recipient_templateId_email_key";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Recipient_email_idx" ON "Recipient"("email");
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `expires` on the `Session` table. All the data in the column will be lost.
|
||||
- Added the required column `expiresAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `updatedAt` to the `Session` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "password" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Session" DROP COLUMN "expires",
|
||||
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
ADD COLUMN "ipAddress" TEXT,
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
ADD COLUMN "userAgent" TEXT;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ADD COLUMN "includeAuditTrail" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `includeAuditTrail` on the `Document` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" DROP COLUMN "includeAuditTrail",
|
||||
ADD COLUMN "includeAuditTrailLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -270,25 +270,18 @@ model Account {
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
password String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId Int
|
||||
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
expires DateTime
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum DocumentStatus {
|
||||
@@ -311,32 +304,30 @@ enum DocumentVisibility {
|
||||
|
||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
includeAuditTrailLog Boolean @default(false)
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
shareLinks DocumentShareLink[]
|
||||
documentDataId String
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
documentMeta DocumentMeta?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
templateId Int?
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||
source DocumentSource
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.")
|
||||
userId Int /// @zod.number.describe("The ID of the user that created this document.")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
|
||||
formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema)
|
||||
visibility DocumentVisibility @default(EVERYONE)
|
||||
title String
|
||||
status DocumentStatus @default(DRAFT)
|
||||
recipients Recipient[]
|
||||
fields Field[]
|
||||
shareLinks DocumentShareLink[]
|
||||
documentDataId String
|
||||
documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade)
|
||||
documentMeta DocumentMeta?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
completedAt DateTime?
|
||||
deletedAt DateTime?
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
templateId Int?
|
||||
template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
||||
source DocumentSource
|
||||
|
||||
auditLogs DocumentAuditLog[]
|
||||
|
||||
@@ -452,11 +443,10 @@ model Recipient {
|
||||
fields Field[]
|
||||
signatures Signature[]
|
||||
|
||||
@@unique([documentId, email])
|
||||
@@unique([templateId, email])
|
||||
@@index([documentId])
|
||||
@@index([templateId])
|
||||
@@index([token])
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
enum FieldType {
|
||||
@@ -545,7 +535,6 @@ model TeamGlobalSettings {
|
||||
includeSenderDetails Boolean @default(true)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
includeSigningCertificate Boolean @default(true)
|
||||
includeAuditTrailLog Boolean @default(false)
|
||||
|
||||
brandingEnabled Boolean @default(false)
|
||||
brandingLogo String @default("")
|
||||
|
||||
@@ -65,7 +65,7 @@ export const documentRouter = router({
|
||||
.input(ZGetDocumentByIdQuerySchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { documentId, includeCertificate, includeAuditLog } = input;
|
||||
const { documentId } = input;
|
||||
|
||||
return await getDocumentById({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -63,16 +63,6 @@ export const ZDocumentVisibilitySchema = z
|
||||
.nativeEnum(DocumentVisibility)
|
||||
.describe('The visibility of the document.');
|
||||
|
||||
export const ZDocumentIncludeSigningCertificateSchema = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to include a signing certificate in the document.');
|
||||
|
||||
export const ZDocumentIncludeAuditTrailSchema = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to include an audit trail in the document.');
|
||||
|
||||
export const ZDocumentMetaTimezoneSchema = z
|
||||
.string()
|
||||
.describe(
|
||||
@@ -151,8 +141,6 @@ export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend(
|
||||
|
||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||
documentId: z.number(),
|
||||
includeCertificate: z.boolean().default(true).optional(),
|
||||
includeAuditLog: z.boolean().default(true).optional(),
|
||||
});
|
||||
|
||||
export const ZDuplicateDocumentRequestSchema = z.object({
|
||||
@@ -204,14 +192,6 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{ message: 'Recipients must have unique emails' },
|
||||
)
|
||||
.optional(),
|
||||
meta: z
|
||||
.object({
|
||||
@@ -247,8 +227,6 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
includeSigningCertificate: ZDocumentIncludeSigningCertificateSchema.optional(),
|
||||
includeAuditTrailLog: ZDocumentIncludeAuditTrailSchema.optional(),
|
||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
||||
})
|
||||
|
||||
@@ -232,8 +232,9 @@ export const fieldRouter = router({
|
||||
teamId,
|
||||
fields: fields.map((field) => ({
|
||||
id: field.nativeId,
|
||||
signerEmail: field.signerEmail,
|
||||
recipientId: field.recipientId,
|
||||
type: field.type,
|
||||
signerEmail: field.signerEmail,
|
||||
pageNumber: field.pageNumber,
|
||||
pageX: field.pageX,
|
||||
pageY: field.pageY,
|
||||
@@ -429,6 +430,7 @@ export const fieldRouter = router({
|
||||
teamId,
|
||||
fields: fields.map((field) => ({
|
||||
id: field.nativeId,
|
||||
signerId: field.recipientId,
|
||||
signerEmail: field.signerEmail,
|
||||
type: field.type,
|
||||
pageNumber: field.pageNumber,
|
||||
|
||||
@@ -112,13 +112,15 @@ export const ZSetDocumentFieldsRequestSchema = z.object({
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
id: z.number().optional(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
recipientId: z.number(),
|
||||
signerEmail: z.string().min(1),
|
||||
pageNumber: z.number().min(1),
|
||||
pageX: z.number().min(0),
|
||||
pageY: z.number().min(0),
|
||||
pageWidth: z.number().min(0),
|
||||
pageHeight: z.number().min(0),
|
||||
pageNumber: ZFieldPageNumberSchema,
|
||||
pageX: ZFieldPageXSchema,
|
||||
pageY: ZFieldPageYSchema,
|
||||
pageWidth: ZFieldWidthSchema,
|
||||
pageHeight: ZFieldHeightSchema,
|
||||
fieldMeta: ZFieldMetaSchema,
|
||||
}),
|
||||
),
|
||||
@@ -136,6 +138,8 @@ export const ZSetFieldsForTemplateRequestSchema = z.object({
|
||||
nativeId: z.number().optional(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
signerEmail: z.string().min(1),
|
||||
recipientId: z.number().min(1),
|
||||
signerId: z.number().min(1),
|
||||
pageNumber: z.number().min(1),
|
||||
pageX: z.number().min(0),
|
||||
pageY: z.number().min(0),
|
||||
|
||||
@@ -49,16 +49,7 @@ export const ZCreateDocumentRecipientResponseSchema = ZRecipientLiteSchema;
|
||||
|
||||
export const ZCreateDocumentRecipientsRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
recipients: z.array(ZCreateRecipientSchema),
|
||||
});
|
||||
|
||||
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
||||
@@ -74,18 +65,7 @@ export const ZUpdateDocumentRecipientResponseSchema = ZRecipientSchema;
|
||||
|
||||
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email?.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
recipients: z.array(ZUpdateRecipientSchema),
|
||||
});
|
||||
|
||||
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
||||
@@ -96,29 +76,19 @@ export const ZDeleteDocumentRecipientRequestSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export const ZSetDocumentRecipientsRequestSchema = z
|
||||
.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.recipients.map((recipient) => recipient.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
|
||||
);
|
||||
export const ZSetDocumentRecipientsRequestSchema = z.object({
|
||||
documentId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ZSetDocumentRecipientsResponseSchema = z.object({
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
@@ -133,16 +103,7 @@ export const ZCreateTemplateRecipientResponseSchema = ZRecipientLiteSchema;
|
||||
|
||||
export const ZCreateTemplateRecipientsRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
recipients: z.array(ZCreateRecipientSchema),
|
||||
});
|
||||
|
||||
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
||||
@@ -158,18 +119,7 @@ export const ZUpdateTemplateRecipientResponseSchema = ZRecipientSchema;
|
||||
|
||||
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
||||
(recipients) => {
|
||||
const emails = recipients
|
||||
.filter((recipient) => recipient.email !== undefined)
|
||||
.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
{
|
||||
message: 'Recipients must have unique emails',
|
||||
},
|
||||
),
|
||||
recipients: z.array(ZUpdateRecipientSchema),
|
||||
});
|
||||
|
||||
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
|
||||
@@ -180,29 +130,19 @@ export const ZDeleteTemplateRecipientRequestSchema = z.object({
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export const ZSetTemplateRecipientsRequestSchema = z
|
||||
.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.recipients.map((recipient) => recipient.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
|
||||
);
|
||||
export const ZSetTemplateRecipientsRequestSchema = z.object({
|
||||
templateId: z.number(),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().toLowerCase().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const ZSetTemplateRecipientsResponseSchema = z.object({
|
||||
recipients: ZRecipientLiteSchema.array(),
|
||||
|
||||
@@ -206,7 +206,6 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
|
||||
includeSenderDetails: z.boolean().optional().default(false),
|
||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||
includeSigningCertificate: z.boolean().optional().default(true),
|
||||
includeAuditTrailLog: z.boolean().optional().default(true),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -51,12 +51,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.describe('The information of the recipients to create the document with.')
|
||||
.refine((recipients) => {
|
||||
const emails = recipients.map((signer) => signer.email);
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
}, 'Recipients must have unique emails'),
|
||||
.describe('The information of the recipients to create the document with.'),
|
||||
distributeDocument: z
|
||||
.boolean()
|
||||
.describe('Whether to create the document as pending and distribute it to recipients.')
|
||||
|
||||
@@ -92,6 +92,7 @@ export type FieldFormType = {
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
recipientId: number;
|
||||
signerEmail: string;
|
||||
fieldMeta?: FieldMeta;
|
||||
};
|
||||
@@ -143,6 +144,7 @@ export const AddFieldsFormPartial = ({
|
||||
pageY: Number(field.positionY),
|
||||
pageWidth: Number(field.width),
|
||||
pageHeight: Number(field.height),
|
||||
recipientId: field.recipientId,
|
||||
signerEmail:
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||
@@ -348,6 +350,7 @@ export const AddFieldsFormPartial = ({
|
||||
pageY,
|
||||
pageWidth: fieldPageWidth,
|
||||
pageHeight: fieldPageHeight,
|
||||
recipientId: selectedSigner.id,
|
||||
signerEmail: selectedSigner.email,
|
||||
fieldMeta: undefined,
|
||||
};
|
||||
@@ -441,6 +444,7 @@ export const AddFieldsFormPartial = ({
|
||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||
...structuredClone(lastActiveField),
|
||||
formId: nanoid(12),
|
||||
recipientId: selectedSigner?.id ?? lastActiveField.recipientId,
|
||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||
pageX: lastActiveField.pageX + 3,
|
||||
pageY: lastActiveField.pageY + 3,
|
||||
@@ -449,7 +453,7 @@ export const AddFieldsFormPartial = ({
|
||||
append(newField);
|
||||
}
|
||||
},
|
||||
[append, lastActiveField, selectedSigner?.email, toast],
|
||||
[append, lastActiveField, selectedSigner?.id, selectedSigner?.email, toast],
|
||||
);
|
||||
|
||||
const onFieldPaste = useCallback(
|
||||
@@ -462,13 +466,15 @@ export const AddFieldsFormPartial = ({
|
||||
append({
|
||||
...copiedField,
|
||||
formId: nanoid(12),
|
||||
recipientId: selectedSigner?.id ?? copiedField.recipientId,
|
||||
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
||||
|
||||
pageX: copiedField.pageX + 3,
|
||||
pageY: copiedField.pageY + 3,
|
||||
});
|
||||
}
|
||||
},
|
||||
[append, fieldClipboard, selectedSigner?.email],
|
||||
[append, fieldClipboard, selectedSigner?.id, selectedSigner?.email],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -567,7 +573,7 @@ export const AddFieldsFormPartial = ({
|
||||
localFields.some(
|
||||
(field) =>
|
||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||
field.signerEmail === signer.email,
|
||||
field.recipientId === signer.id,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -637,7 +643,7 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
{isDocumentPdfLoaded &&
|
||||
localFields.map((field, index) => {
|
||||
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
|
||||
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
|
||||
const hasFieldError =
|
||||
emptyCheckboxFields.find((f) => f.formId === field.formId) ||
|
||||
emptyRadioFields.find((f) => f.formId === field.formId) ||
|
||||
@@ -649,7 +655,7 @@ export const AddFieldsFormPartial = ({
|
||||
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
|
||||
field={field}
|
||||
disabled={
|
||||
selectedSigner?.email !== field.signerEmail ||
|
||||
selectedSigner?.id !== field.recipientId ||
|
||||
!canRecipientBeModified(selectedSigner, fields)
|
||||
}
|
||||
minHeight={MIN_HEIGHT_PX}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ZAddFieldsFormSchema = z.object({
|
||||
nativeId: z.number().optional(),
|
||||
type: z.nativeEnum(FieldType),
|
||||
signerEmail: z.string().min(1),
|
||||
recipientId: z.number(),
|
||||
pageNumber: z.number().min(1),
|
||||
pageX: z.number().min(0),
|
||||
pageY: z.number().min(0),
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { Combobox } from '../combobox';
|
||||
import { Input } from '../input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
|
||||
@@ -93,8 +92,6 @@ export const AddSettingsFormPartial = ({
|
||||
visibility: document.visibility || '',
|
||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
|
||||
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
|
||||
includeSigningCertificate: document.includeSigningCertificate ?? true,
|
||||
includeAuditTrailLog: document.includeAuditTrailLog ?? true,
|
||||
meta: {
|
||||
timezone:
|
||||
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
|
||||
@@ -262,111 +259,6 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="globalActionAuth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
<DocumentGlobalAuthActionTooltip />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Accordion type="multiple" className="mt-6">
|
||||
<AccordionItem value="advanced-options" className="border-none">
|
||||
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
|
||||
<Trans>Certificates</Trans>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
|
||||
<div className="flex flex-col space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeSigningCertificate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
className="h-5 w-5"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="m-0 flex flex-row items-center">
|
||||
<Trans>Include signing certificate</Trans>{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
Including the signing certificate means that the certificate
|
||||
will be attached to the document. You won't be able to remove
|
||||
it. <br />
|
||||
<br />
|
||||
If you don't include it, you can download it individually.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeAuditTrailLog"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
className="h-5 w-5"
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="m-0 flex flex-row items-center">
|
||||
<Trans>Include audit trail</Trans>{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
<Trans>
|
||||
Including the audit trail means that the log of all actions will
|
||||
be attached to the document. You won't be able to remove it.{' '}
|
||||
<br />
|
||||
<br />
|
||||
If you don't include it, you can download it individually.
|
||||
</Trans>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</FormLabel>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{isDocumentEnterprise && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -29,8 +29,6 @@ export const ZAddSettingsFormSchema = z.object({
|
||||
title: z.string().trim().min(1, { message: "Title can't be empty" }),
|
||||
externalId: z.string().optional(),
|
||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||
includeSigningCertificate: z.boolean().default(true).optional(),
|
||||
includeAuditTrailLog: z.boolean().default(true).optional(),
|
||||
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZDocumentAccessAuthTypesSchema.optional(),
|
||||
),
|
||||
|
||||
@@ -6,34 +6,22 @@ import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-a
|
||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: msg`Invalid email`.id })
|
||||
.min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: msg`Signers must have unique emails`.id, path: ['signers__root'] },
|
||||
);
|
||||
export const ZAddSignersFormSchema = z.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: msg`Invalid email`.id })
|
||||
.min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
});
|
||||
|
||||
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
||||
|
||||
@@ -8,19 +8,14 @@ import { FieldType } from '@documenso/prisma/client';
|
||||
export const ZDocumentFlowFormSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
|
||||
signers: z
|
||||
.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
}),
|
||||
)
|
||||
.refine((signers) => {
|
||||
const emails = signers.map((signer) => signer.email);
|
||||
return new Set(emails).size === emails.length;
|
||||
}, 'Signers must have unique emails'),
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
|
||||
fields: z.array(
|
||||
z.object({
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from './button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from './dropdown-menu';
|
||||
|
||||
const SplitButtonContext = React.createContext<{
|
||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||
size?: React.ComponentProps<typeof Button>['size'];
|
||||
}>({});
|
||||
|
||||
const SplitButton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
variant?: React.ComponentProps<typeof Button>['variant'];
|
||||
size?: React.ComponentProps<typeof Button>['size'];
|
||||
}
|
||||
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
return (
|
||||
<SplitButtonContext.Provider value={{ variant, size }}>
|
||||
<div ref={ref} className={cn('inline-flex', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</SplitButtonContext.Provider>
|
||||
);
|
||||
});
|
||||
SplitButton.displayName = 'SplitButton';
|
||||
|
||||
const SplitButtonAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { variant, size } = React.useContext(SplitButtonContext);
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn('rounded-r-none border-r-0', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SplitButtonAction.displayName = 'SplitButtonAction';
|
||||
|
||||
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { variant, size } = React.useContext(SplitButtonContext);
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className="rounded-l-none px-2 focus-visible:ring-offset-0"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" {...props} ref={ref}>
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
);
|
||||
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
|
||||
|
||||
const SplitButtonDropdownItem = DropdownMenuItem;
|
||||
|
||||
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };
|
||||
@@ -60,6 +60,7 @@ import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
|
||||
import { Checkbox } from '../checkbox';
|
||||
import type { FieldFormType } from '../document-flow/add-fields';
|
||||
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
|
||||
import { MissingSignatureFieldDialog } from '../document-flow/missing-signature-field-dialog';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
|
||||
import { useStep } from '../stepper';
|
||||
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
|
||||
@@ -110,6 +111,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
const [fieldClipboard, setFieldClipboard] = useState<
|
||||
TAddTemplateFieldsFormSchema['fields'][0] | null
|
||||
>(null);
|
||||
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
||||
|
||||
const form = useForm<TAddTemplateFieldsFormSchema>({
|
||||
defaultValues: {
|
||||
@@ -122,6 +124,7 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageY: Number(field.positionY),
|
||||
pageWidth: Number(field.width),
|
||||
pageHeight: Number(field.height),
|
||||
recipientId: field.recipientId ?? -1,
|
||||
signerId: field.recipientId ?? -1,
|
||||
signerEmail:
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||
@@ -177,6 +180,8 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
||||
recipientId:
|
||||
selectedSigner?.id || lastActiveField.recipientId || lastActiveField.signerId || 0,
|
||||
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
||||
pageX: lastActiveField.pageX + 3,
|
||||
pageY: lastActiveField.pageY + 3,
|
||||
@@ -201,19 +206,29 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
event.preventDefault();
|
||||
|
||||
const copiedField = structuredClone(fieldClipboard);
|
||||
const signerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||
|
||||
append({
|
||||
...copiedField,
|
||||
formId: nanoid(12),
|
||||
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
||||
signerId: selectedSigner?.id ?? copiedField.signerId,
|
||||
recipientId: selectedSigner?.id || copiedField.recipientId || copiedField.signerId || 0,
|
||||
signerToken: selectedSigner?.token ?? copiedField.signerToken,
|
||||
signerIndex: signerIndex >= 0 ? signerIndex : 0,
|
||||
pageX: copiedField.pageX + 3,
|
||||
pageY: copiedField.pageY + 3,
|
||||
});
|
||||
}
|
||||
},
|
||||
[append, fieldClipboard, selectedSigner?.email, selectedSigner?.id, selectedSigner?.token],
|
||||
[
|
||||
append,
|
||||
fieldClipboard,
|
||||
selectedSigner?.email,
|
||||
selectedSigner?.id,
|
||||
selectedSigner?.token,
|
||||
recipients,
|
||||
],
|
||||
);
|
||||
|
||||
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
|
||||
@@ -319,6 +334,8 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageX -= fieldPageWidth / 2;
|
||||
pageY -= fieldPageHeight / 2;
|
||||
|
||||
const signerIndex = recipients.findIndex((r) => r.id === selectedSigner.id);
|
||||
|
||||
append({
|
||||
formId: nanoid(12),
|
||||
type: selectedField,
|
||||
@@ -329,14 +346,17 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
pageHeight: fieldPageHeight,
|
||||
signerEmail: selectedSigner.email,
|
||||
signerId: selectedSigner.id,
|
||||
recipientId:
|
||||
selectedSigner.id || lastActiveField?.recipientId || lastActiveField?.signerId || 0,
|
||||
signerToken: selectedSigner.token ?? '',
|
||||
signerIndex: signerIndex >= 0 ? signerIndex : 0,
|
||||
fieldMeta: undefined,
|
||||
});
|
||||
|
||||
setIsFieldWithinBounds(false);
|
||||
setSelectedField(null);
|
||||
},
|
||||
[append, isWithinPageBounds, selectedField, selectedSigner, getPage],
|
||||
[append, isWithinPageBounds, selectedField, selectedSigner, getPage, recipients],
|
||||
);
|
||||
|
||||
const onFieldResize = useCallback(
|
||||
@@ -499,6 +519,23 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
|
||||
};
|
||||
|
||||
const handleGoNextClick = () => {
|
||||
const everySignerHasSignature = recipientsByRole.SIGNER.every((signer) =>
|
||||
localFields.some(
|
||||
(field) =>
|
||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||
field.recipientId === signer.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (!everySignerHasSignature) {
|
||||
setIsMissingSignatureDialogVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
void onFormSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAdvancedSettings && currentField ? (
|
||||
@@ -546,14 +583,15 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
)}
|
||||
|
||||
{localFields.map((field, index) => {
|
||||
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
|
||||
const recipientIndex =
|
||||
field.signerIndex ?? recipients.findIndex((r) => r.id === field.signerId);
|
||||
|
||||
return (
|
||||
<FieldItem
|
||||
key={index}
|
||||
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
|
||||
recipientIndex={recipientIndex >= 0 ? recipientIndex : 0}
|
||||
field={field}
|
||||
disabled={selectedSigner?.email !== field.signerEmail}
|
||||
disabled={selectedSigner?.id !== field.signerId}
|
||||
minHeight={MIN_HEIGHT_PX}
|
||||
minWidth={MIN_WIDTH_PX}
|
||||
defaultHeight={DEFAULT_HEIGHT_PX}
|
||||
@@ -993,9 +1031,14 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
previousStep();
|
||||
remove();
|
||||
}}
|
||||
onGoNextClick={() => void onFormSubmit()}
|
||||
onGoNextClick={handleGoNextClick}
|
||||
/>
|
||||
</DocumentFlowFormContainerFooter>
|
||||
|
||||
<MissingSignatureFieldDialog
|
||||
isOpen={isMissingSignatureDialogVisible}
|
||||
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
|
||||
/>
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
</>
|
||||
|
||||
@@ -11,12 +11,14 @@ export const ZAddTemplateFieldsFormSchema = z.object({
|
||||
type: z.nativeEnum(FieldType),
|
||||
signerEmail: z.string().min(1),
|
||||
signerToken: z.string(),
|
||||
signerId: z.number().optional(),
|
||||
signerId: z.number(),
|
||||
recipientId: z.number(),
|
||||
pageNumber: z.number().min(1),
|
||||
pageX: z.number().min(0),
|
||||
pageY: z.number().min(0),
|
||||
pageWidth: z.number().min(0),
|
||||
pageHeight: z.number().min(0),
|
||||
signerIndex: z.number().min(0),
|
||||
fieldMeta: ZFieldMetaSchema,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -92,6 +92,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
actionAuth: undefined,
|
||||
...generateRecipientPlaceholder(1),
|
||||
signingOrder: 1,
|
||||
signerIndex: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -104,6 +105,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
role: recipient.role,
|
||||
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined,
|
||||
signingOrder: recipient.signingOrder ?? index + 1,
|
||||
signerIndex: index,
|
||||
}));
|
||||
|
||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
@@ -174,21 +176,35 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
});
|
||||
|
||||
const onAddPlaceholderSelfRecipient = () => {
|
||||
const currentSigners = form.getValues('signers');
|
||||
const nextSignerIndex = currentSigners.length;
|
||||
|
||||
appendSigner({
|
||||
formId: nanoid(12),
|
||||
name: user?.name ?? '',
|
||||
email: user?.email ?? '',
|
||||
role: RecipientRole.SIGNER,
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
signingOrder:
|
||||
currentSigners.length > 0
|
||||
? (currentSigners[currentSigners.length - 1]?.signingOrder ?? 0) + 1
|
||||
: 1,
|
||||
signerIndex: nextSignerIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const onAddPlaceholderRecipient = () => {
|
||||
const currentSigners = form.getValues('signers');
|
||||
const nextSignerIndex = currentSigners.length;
|
||||
|
||||
appendSigner({
|
||||
formId: nanoid(12),
|
||||
role: RecipientRole.SIGNER,
|
||||
...generateRecipientPlaceholder(placeholderRecipientCount),
|
||||
signingOrder: signers.length > 0 ? (signers[signers.length - 1]?.signingOrder ?? 0) + 1 : 1,
|
||||
signingOrder:
|
||||
currentSigners.length > 0
|
||||
? (currentSigners[currentSigners.length - 1]?.signingOrder ?? 0) + 1
|
||||
: 1,
|
||||
signerIndex: nextSignerIndex,
|
||||
});
|
||||
|
||||
setPlaceholderRecipientCount((count) => count + 1);
|
||||
|
||||
@@ -5,32 +5,21 @@ import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
||||
ZRecipientActionAuthTypesSchema.optional(),
|
||||
),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
|
||||
|
||||
return new Set(emails).size === emails.length;
|
||||
},
|
||||
// Dirty hack to handle errors when .root is populated for an array type
|
||||
{ message: 'Signers must have unique emails', path: ['signers__root'] },
|
||||
);
|
||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
signingOrder: z.number().optional(),
|
||||
signerIndex: z.number().min(0),
|
||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()),
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
});
|
||||
|
||||
export type TAddTemplatePlacholderRecipientsFormSchema = z.infer<
|
||||
typeof ZAddTemplatePlacholderRecipientsFormSchema
|
||||
|
||||
Reference in New Issue
Block a user