Compare commits
28 Commits
v1.9.1-rc.
...
feat/allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e9c7f1b11 | ||
|
|
bc2ec9a2d7 | ||
|
|
763b7f82c9 | ||
|
|
c670f64b1f | ||
|
|
369e16afab | ||
|
|
4a5f565591 | ||
|
|
f544eae2a6 | ||
|
|
a2ffd75c17 | ||
|
|
8619eec67a | ||
|
|
f325a04cb5 | ||
|
|
2ff330f9d4 | ||
|
|
6a47b3a6e5 | ||
|
|
a7adb77e47 | ||
|
|
bfcbaea3a9 | ||
|
|
64964f420a | ||
|
|
2896673a23 | ||
|
|
b684b9574d | ||
|
|
12803d1a5e | ||
|
|
c41002313a | ||
|
|
ce1c93b2a6 | ||
|
|
82337e4e3a | ||
|
|
516435fa2a | ||
|
|
7d9a3f9776 | ||
|
|
0216af4ae8 | ||
|
|
3cde3cb7b2 | ||
|
|
071f5c546d | ||
|
|
9f9f6701c8 | ||
|
|
b01eaceeb8 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.0",
|
"version": "1.9.1-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -47,50 +47,22 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
|||||||
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
import type { Toast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z.object({
|
||||||
.object({
|
distributeDocument: z.boolean(),
|
||||||
distributeDocument: z.boolean(),
|
useCustomDocument: z.boolean().default(false),
|
||||||
useCustomDocument: z.boolean().default(false),
|
customDocumentData: z
|
||||||
customDocumentData: z
|
.any()
|
||||||
.any()
|
.refine((data) => data instanceof File || data === undefined)
|
||||||
.refine((data) => data instanceof File || data === undefined)
|
.optional(),
|
||||||
.optional(),
|
recipients: z.array(
|
||||||
recipients: z.array(
|
z.object({
|
||||||
z.object({
|
id: z.number(),
|
||||||
id: z.number(),
|
email: z.string().email(),
|
||||||
email: z.string().email(),
|
name: z.string(),
|
||||||
name: z.string(),
|
signingOrder: z.number().optional(),
|
||||||
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'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
type TAddRecipientsForNewDocumentSchema = z.infer<typeof ZAddRecipientsForNewDocumentSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
field={field}
|
field={field}
|
||||||
onSignField={onSignField}
|
onSignField={onSignField}
|
||||||
onUnsignField={onUnsignField}
|
onUnsignField={onUnsignField}
|
||||||
|
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with(FieldType.INITIALS, () => (
|
.with(FieldType.INITIALS, () => (
|
||||||
@@ -342,6 +343,7 @@ export const SignDirectTemplateForm = ({
|
|||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSignature(value);
|
setSignature(value);
|
||||||
}}
|
}}
|
||||||
|
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -179,14 +179,8 @@ export const NumberField = ({ field, onSignField, onUnsignField }: NumberFieldPr
|
|||||||
|
|
||||||
const onRemove = async () => {
|
const onRemove = async () => {
|
||||||
try {
|
try {
|
||||||
if (isAssistantMode && !targetSigner) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
|
||||||
|
|
||||||
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
|
||||||
token: signingRecipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,26 +68,16 @@ export const RadioField = ({ field, onSignField, onUnsignField }: RadioFieldProp
|
|||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
try {
|
try {
|
||||||
if (isAssistantMode && !targetSigner) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signingRecipient = isAssistantMode && targetSigner ? targetSigner : recipient;
|
|
||||||
|
|
||||||
const payload: TSignFieldWithTokenMutationSchema = {
|
const payload: TSignFieldWithTokenMutationSchema = {
|
||||||
token: signingRecipient.token,
|
token: recipient.token,
|
||||||
fieldId: field.id,
|
fieldId: field.id,
|
||||||
value: selectedOption,
|
value: selectedOption,
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
authOptions,
|
authOptions,
|
||||||
...(isAssistantMode && {
|
|
||||||
isAssistantPrefill: true,
|
|
||||||
assistantId: recipient.id,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onSignField) {
|
if (onSignField) {
|
||||||
|
|||||||
@@ -179,7 +179,13 @@ export const SigningPageView = ({
|
|||||||
)
|
)
|
||||||
.map((field) =>
|
.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.SIGNATURE, () => <SignatureField key={field.id} field={field} />)
|
.with(FieldType.SIGNATURE, () => (
|
||||||
|
<SignatureField
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
|
||||||
|
/>
|
||||||
|
))
|
||||||
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
|
.with(FieldType.INITIALS, () => <InitialsField key={field.id} field={field} />)
|
||||||
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
|
.with(FieldType.NAME, () => <NameField key={field.id} field={field} />)
|
||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.0",
|
"version": "1.9.1-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.1-rc.0",
|
"version": "1.9.1-rc.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.1-rc.0",
|
"version": "1.9.1-rc.1",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@@ -35722,6 +35722,21 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"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,
|
"private": true,
|
||||||
"version": "1.9.1-rc.0",
|
"version": "1.9.1-rc.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@@ -270,12 +270,11 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(schema) => {
|
(schema) => {
|
||||||
const emails = schema.map((signer) => signer.email.toLowerCase());
|
|
||||||
const ids = schema.map((signer) => signer.id);
|
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
|
meta: z
|
||||||
.object({
|
.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 page.getByRole('button', { name: 'Go Back' }).click();
|
||||||
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
|
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.
|
// Advanced settings should not be visible for non EE users.
|
||||||
await expect(page.getByLabel('Show advanced settings')).toBeHidden();
|
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();
|
await page.getByRole('button', { name: 'Save template' }).click();
|
||||||
|
|
||||||
// Use template
|
// Use template
|
||||||
await page.waitForURL('/templates');
|
await page.waitForURL('**/templates');
|
||||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the correct values.
|
// Review that the document was created with the correct values.
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
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();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the correct values.
|
// Review that the document was created with the correct values.
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
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();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
// Review that the document was created with the custom document data
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
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();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the custom document data
|
// Review that the document was created with the custom document data
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
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();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the template's document data
|
// Review that the document was created with the template's document data
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -591,9 +586,8 @@ test('[TEMPLATE]: should persist document visibility when creating from template
|
|||||||
await page.getByRole('button', { name: 'Create as draft' }).click();
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
// Review that the document was created with the correct visibility
|
// Review that the document was created with the correct visibility
|
||||||
await page.waitForURL(/documents/);
|
await page.waitForURL(/\/documents\/\d+/);
|
||||||
|
const documentId = Number(page.url().match(/\/documents\/(\d+)/)?.[1]);
|
||||||
const documentId = Number(page.url().split('/').pop());
|
|
||||||
|
|
||||||
const document = await prisma.document.findFirstOrThrow({
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -616,3 +610,368 @@ test('[TEMPLATE]: should persist document visibility when creating from template
|
|||||||
// Template should not be visible to regular member
|
// Template should not be visible to regular member
|
||||||
await expect(page.getByRole('button', { name: 'Use Template' })).not.toBeVisible();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -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) {
|
if (document.completedAt) {
|
||||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
message: 'Document already complete',
|
message: 'Document already complete',
|
||||||
@@ -94,9 +110,7 @@ export const setFieldsForDocument = async ({
|
|||||||
const linkedFields = fields.map((field) => {
|
const linkedFields = fields.map((field) => {
|
||||||
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
||||||
|
|
||||||
const recipient = document.recipients.find(
|
const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||||
(recipient) => recipient.email.toLowerCase() === field.signerEmail.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Each field MUST have a recipient associated with it.
|
// Each field MUST have a recipient associated with it.
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
@@ -236,10 +250,8 @@ export const setFieldsForDocument = async ({
|
|||||||
},
|
},
|
||||||
recipient: {
|
recipient: {
|
||||||
connect: {
|
connect: {
|
||||||
documentId_email: {
|
documentId,
|
||||||
documentId,
|
id: field.recipientId,
|
||||||
email: fieldSignerEmail,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -340,6 +352,7 @@ type FieldData = {
|
|||||||
id?: number | null;
|
id?: number | null;
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
signerEmail: string;
|
signerEmail: string;
|
||||||
|
recipientId: number;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
pageX: number;
|
pageX: number;
|
||||||
pageY: number;
|
pageY: number;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type SetFieldsForTemplateOptions = {
|
|||||||
fields: {
|
fields: {
|
||||||
id?: number | null;
|
id?: number | null;
|
||||||
type: FieldType;
|
type: FieldType;
|
||||||
|
signerId: number;
|
||||||
signerEmail: string;
|
signerEmail: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
pageX: number;
|
pageX: number;
|
||||||
@@ -57,12 +58,29 @@ export const setFieldsForTemplate = async ({
|
|||||||
teamId: null,
|
teamId: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
throw new Error('Template not found');
|
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({
|
const existingFields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
@@ -180,10 +198,8 @@ export const setFieldsForTemplate = async ({
|
|||||||
},
|
},
|
||||||
recipient: {
|
recipient: {
|
||||||
connect: {
|
connect: {
|
||||||
templateId_email: {
|
templateId,
|
||||||
templateId,
|
id: field.signerId,
|
||||||
email: field.signerEmail.toLowerCase(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -125,16 +125,12 @@ export const setDocumentRecipients = async ({
|
|||||||
|
|
||||||
const removedRecipients = existingRecipients.filter(
|
const removedRecipients = existingRecipients.filter(
|
||||||
(existingRecipient) =>
|
(existingRecipient) =>
|
||||||
!normalizedRecipients.find(
|
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||||
(recipient) =>
|
|
||||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||||
const existing = existingRecipients.find(
|
const existing = existingRecipients.find(
|
||||||
(existingRecipient) =>
|
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ export const setTemplateRecipients = async ({
|
|||||||
|
|
||||||
const removedRecipients = existingRecipients.filter(
|
const removedRecipients = existingRecipients.filter(
|
||||||
(existingRecipient) =>
|
(existingRecipient) =>
|
||||||
!normalizedRecipients.find(
|
!normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id),
|
||||||
(recipient) =>
|
|
||||||
recipient.id === existingRecipient.id || recipient.email === existingRecipient.email,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (template.directLink !== null) {
|
if (template.directLink !== null) {
|
||||||
@@ -133,14 +130,10 @@ export const setTemplateRecipients = async ({
|
|||||||
|
|
||||||
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
const linkedRecipients = normalizedRecipients.map((recipient) => {
|
||||||
const existing = existingRecipients.find(
|
const existing = existingRecipients.find(
|
||||||
(existingRecipient) =>
|
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||||
existingRecipient.id === recipient.id || existingRecipient.email === recipient.email,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return { ...recipient, _persisted: existing };
|
||||||
...recipient,
|
|
||||||
_persisted: existing,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||||
|
|||||||
@@ -141,10 +141,8 @@ export const createDocumentFromTemplateLegacy = async ({
|
|||||||
|
|
||||||
return await prisma.recipient.upsert({
|
return await prisma.recipient.upsert({
|
||||||
where: {
|
where: {
|
||||||
documentId_email: {
|
documentId: document.id,
|
||||||
documentId: document.id,
|
id: existingRecipient?.id,
|
||||||
email: existingRecipient?.email ?? recipient.email,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: recipient.name,
|
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'>[] = [];
|
let fieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];
|
||||||
|
|
||||||
Object.values(finalRecipients).forEach(({ email, fields }) => {
|
finalRecipients.forEach(({ templateRecipientId, fields }) => {
|
||||||
const recipient = document.recipients.find((recipient) => recipient.email === email);
|
const documentRecipientId = recipientMapping.get(templateRecipientId);
|
||||||
|
const recipient = document.recipients.find((r) => r.id === documentRecipientId);
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
throw new Error('Recipient not found.');
|
throw new Error('Recipient not found.');
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -443,11 +443,10 @@ model Recipient {
|
|||||||
fields Field[]
|
fields Field[]
|
||||||
signatures Signature[]
|
signatures Signature[]
|
||||||
|
|
||||||
@@unique([documentId, email])
|
|
||||||
@@unique([templateId, email])
|
|
||||||
@@index([documentId])
|
@@index([documentId])
|
||||||
@@index([templateId])
|
@@index([templateId])
|
||||||
@@index([token])
|
@@index([token])
|
||||||
|
@@index([email])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FieldType {
|
enum FieldType {
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ import { hashSync } from '@documenso/lib/server-only/auth/hash';
|
|||||||
|
|
||||||
import { prisma } from '..';
|
import { prisma } from '..';
|
||||||
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
import { DocumentDataType, DocumentSource, Role, TeamMemberRole } from '../client';
|
||||||
|
import { seedPendingDocument } from './documents';
|
||||||
|
import { seedDirectTemplate, seedTemplate } from './templates';
|
||||||
|
|
||||||
|
const createDocumentData = async ({ documentData }: { documentData: string }) => {
|
||||||
|
return prisma.documentData.create({
|
||||||
|
data: {
|
||||||
|
type: DocumentDataType.BYTES_64,
|
||||||
|
data: documentData,
|
||||||
|
initialData: documentData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const seedDatabase = async () => {
|
export const seedDatabase = async () => {
|
||||||
const examplePdf = fs
|
const examplePdf = fs
|
||||||
@@ -39,35 +51,80 @@ export const seedDatabase = async () => {
|
|||||||
update: {},
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const examplePdfData = await prisma.documentData.upsert({
|
for (let i = 1; i <= 4; i++) {
|
||||||
where: {
|
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: 'clmn0kv5k0000pe04vcqg5zla',
|
|
||||||
type: DocumentDataType.BYTES_64,
|
|
||||||
data: examplePdf,
|
|
||||||
initialData: examplePdf,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.document.create({
|
await prisma.document.create({
|
||||||
data: {
|
data: {
|
||||||
source: DocumentSource.DOCUMENT,
|
source: DocumentSource.DOCUMENT,
|
||||||
title: 'Example Document',
|
title: `Example Document ${i}`,
|
||||||
documentDataId: examplePdfData.id,
|
documentDataId: documentData.id,
|
||||||
userId: exampleUser.id,
|
userId: exampleUser.id,
|
||||||
recipients: {
|
recipients: {
|
||||||
create: {
|
create: {
|
||||||
name: String(adminUser.name),
|
name: String(adminUser.name),
|
||||||
email: adminUser.email,
|
email: adminUser.email,
|
||||||
token: Math.random().toString(36).slice(2, 9),
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
const documentData = await createDocumentData({ documentData: examplePdf });
|
||||||
|
|
||||||
|
await prisma.document.create({
|
||||||
|
data: {
|
||||||
|
source: DocumentSource.DOCUMENT,
|
||||||
|
title: `Document ${i}`,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
userId: adminUser.id,
|
||||||
|
recipients: {
|
||||||
|
create: {
|
||||||
|
name: String(exampleUser.name),
|
||||||
|
email: exampleUser.email,
|
||||||
|
token: Math.random().toString(36).slice(2, 9),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await seedPendingDocument(exampleUser, [adminUser], {
|
||||||
|
key: 'example-pending',
|
||||||
|
createDocumentOptions: {
|
||||||
|
title: 'Pending Document',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await seedPendingDocument(adminUser, [exampleUser], {
|
||||||
|
key: 'admin-pending',
|
||||||
|
createDocumentOptions: {
|
||||||
|
title: 'Pending Document',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
seedTemplate({
|
||||||
|
title: 'Template 1',
|
||||||
|
userId: exampleUser.id,
|
||||||
|
}),
|
||||||
|
seedDirectTemplate({
|
||||||
|
title: 'Direct Template 1',
|
||||||
|
userId: exampleUser.id,
|
||||||
|
}),
|
||||||
|
|
||||||
|
seedTemplate({
|
||||||
|
title: 'Template 1',
|
||||||
|
userId: adminUser.id,
|
||||||
|
}),
|
||||||
|
seedDirectTemplate({
|
||||||
|
title: 'Direct Template 1',
|
||||||
|
userId: adminUser.id,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const testUsers = [
|
const testUsers = [
|
||||||
'test@documenso.com',
|
'test@documenso.com',
|
||||||
'test2@documenso.com',
|
'test2@documenso.com',
|
||||||
|
|||||||
@@ -192,14 +192,6 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.refine(
|
|
||||||
(recipients) => {
|
|
||||||
const emails = recipients.map((recipient) => recipient.email);
|
|
||||||
|
|
||||||
return new Set(emails).size === emails.length;
|
|
||||||
},
|
|
||||||
{ message: 'Recipients must have unique emails' },
|
|
||||||
)
|
|
||||||
.optional(),
|
.optional(),
|
||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -232,8 +232,9 @@ export const fieldRouter = router({
|
|||||||
teamId,
|
teamId,
|
||||||
fields: fields.map((field) => ({
|
fields: fields.map((field) => ({
|
||||||
id: field.nativeId,
|
id: field.nativeId,
|
||||||
signerEmail: field.signerEmail,
|
recipientId: field.recipientId,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
|
signerEmail: field.signerEmail,
|
||||||
pageNumber: field.pageNumber,
|
pageNumber: field.pageNumber,
|
||||||
pageX: field.pageX,
|
pageX: field.pageX,
|
||||||
pageY: field.pageY,
|
pageY: field.pageY,
|
||||||
@@ -429,6 +430,7 @@ export const fieldRouter = router({
|
|||||||
teamId,
|
teamId,
|
||||||
fields: fields.map((field) => ({
|
fields: fields.map((field) => ({
|
||||||
id: field.nativeId,
|
id: field.nativeId,
|
||||||
|
signerId: field.recipientId,
|
||||||
signerEmail: field.signerEmail,
|
signerEmail: field.signerEmail,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
pageNumber: field.pageNumber,
|
pageNumber: field.pageNumber,
|
||||||
|
|||||||
@@ -112,13 +112,15 @@ export const ZSetDocumentFieldsRequestSchema = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
formId: z.string().min(1),
|
formId: z.string().min(1),
|
||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
|
id: z.number().optional(),
|
||||||
type: z.nativeEnum(FieldType),
|
type: z.nativeEnum(FieldType),
|
||||||
|
recipientId: z.number(),
|
||||||
signerEmail: z.string().min(1),
|
signerEmail: z.string().min(1),
|
||||||
pageNumber: z.number().min(1),
|
pageNumber: ZFieldPageNumberSchema,
|
||||||
pageX: z.number().min(0),
|
pageX: ZFieldPageXSchema,
|
||||||
pageY: z.number().min(0),
|
pageY: ZFieldPageYSchema,
|
||||||
pageWidth: z.number().min(0),
|
pageWidth: ZFieldWidthSchema,
|
||||||
pageHeight: z.number().min(0),
|
pageHeight: ZFieldHeightSchema,
|
||||||
fieldMeta: ZFieldMetaSchema,
|
fieldMeta: ZFieldMetaSchema,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -136,6 +138,8 @@ export const ZSetFieldsForTemplateRequestSchema = z.object({
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
type: z.nativeEnum(FieldType),
|
type: z.nativeEnum(FieldType),
|
||||||
signerEmail: z.string().min(1),
|
signerEmail: z.string().min(1),
|
||||||
|
recipientId: z.number().min(1),
|
||||||
|
signerId: z.number().min(1),
|
||||||
pageNumber: z.number().min(1),
|
pageNumber: z.number().min(1),
|
||||||
pageX: z.number().min(0),
|
pageX: z.number().min(0),
|
||||||
pageY: z.number().min(0),
|
pageY: z.number().min(0),
|
||||||
|
|||||||
@@ -49,16 +49,7 @@ export const ZCreateDocumentRecipientResponseSchema = ZRecipientLiteSchema;
|
|||||||
|
|
||||||
export const ZCreateDocumentRecipientsRequestSchema = z.object({
|
export const ZCreateDocumentRecipientsRequestSchema = z.object({
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
recipients: z.array(ZCreateRecipientSchema),
|
||||||
(recipients) => {
|
|
||||||
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
|
|
||||||
|
|
||||||
return new Set(emails).size === emails.length;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Recipients must have unique emails',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
export const ZCreateDocumentRecipientsResponseSchema = z.object({
|
||||||
@@ -74,18 +65,7 @@ export const ZUpdateDocumentRecipientResponseSchema = ZRecipientSchema;
|
|||||||
|
|
||||||
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
|
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
|
||||||
documentId: z.number(),
|
documentId: z.number(),
|
||||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
recipients: z.array(ZUpdateRecipientSchema),
|
||||||
(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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
|
||||||
@@ -96,29 +76,19 @@ export const ZDeleteDocumentRecipientRequestSchema = z.object({
|
|||||||
recipientId: z.number(),
|
recipientId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZSetDocumentRecipientsRequestSchema = z
|
export const ZSetDocumentRecipientsRequestSchema = z.object({
|
||||||
.object({
|
documentId: z.number(),
|
||||||
documentId: z.number(),
|
recipients: z.array(
|
||||||
recipients: z.array(
|
z.object({
|
||||||
z.object({
|
nativeId: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z.string().toLowerCase().email().min(1),
|
||||||
email: z.string().toLowerCase().email().min(1),
|
name: z.string(),
|
||||||
name: z.string(),
|
role: z.nativeEnum(RecipientRole),
|
||||||
role: z.nativeEnum(RecipientRole),
|
signingOrder: z.number().optional(),
|
||||||
signingOrder: z.number().optional(),
|
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||||
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 ZSetDocumentRecipientsResponseSchema = z.object({
|
export const ZSetDocumentRecipientsResponseSchema = z.object({
|
||||||
recipients: ZRecipientLiteSchema.array(),
|
recipients: ZRecipientLiteSchema.array(),
|
||||||
@@ -133,16 +103,7 @@ export const ZCreateTemplateRecipientResponseSchema = ZRecipientLiteSchema;
|
|||||||
|
|
||||||
export const ZCreateTemplateRecipientsRequestSchema = z.object({
|
export const ZCreateTemplateRecipientsRequestSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
recipients: z.array(ZCreateRecipientSchema).refine(
|
recipients: z.array(ZCreateRecipientSchema),
|
||||||
(recipients) => {
|
|
||||||
const emails = recipients.map((recipient) => recipient.email);
|
|
||||||
|
|
||||||
return new Set(emails).size === emails.length;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Recipients must have unique emails',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
export const ZCreateTemplateRecipientsResponseSchema = z.object({
|
||||||
@@ -158,18 +119,7 @@ export const ZUpdateTemplateRecipientResponseSchema = ZRecipientSchema;
|
|||||||
|
|
||||||
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
|
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
recipients: z.array(ZUpdateRecipientSchema).refine(
|
recipients: z.array(ZUpdateRecipientSchema),
|
||||||
(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',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
|
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
|
||||||
@@ -180,29 +130,19 @@ export const ZDeleteTemplateRecipientRequestSchema = z.object({
|
|||||||
recipientId: z.number(),
|
recipientId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZSetTemplateRecipientsRequestSchema = z
|
export const ZSetTemplateRecipientsRequestSchema = z.object({
|
||||||
.object({
|
templateId: z.number(),
|
||||||
templateId: z.number(),
|
recipients: z.array(
|
||||||
recipients: z.array(
|
z.object({
|
||||||
z.object({
|
nativeId: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z.string().toLowerCase().email().min(1),
|
||||||
email: z.string().toLowerCase().email().min(1),
|
name: z.string(),
|
||||||
name: z.string(),
|
role: z.nativeEnum(RecipientRole),
|
||||||
role: z.nativeEnum(RecipientRole),
|
signingOrder: z.number().optional(),
|
||||||
signingOrder: z.number().optional(),
|
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
|
||||||
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 ZSetTemplateRecipientsResponseSchema = z.object({
|
export const ZSetTemplateRecipientsResponseSchema = z.object({
|
||||||
recipients: ZRecipientLiteSchema.array(),
|
recipients: ZRecipientLiteSchema.array(),
|
||||||
|
|||||||
@@ -51,12 +51,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.describe('The information of the recipients to create the document with.')
|
.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'),
|
|
||||||
distributeDocument: z
|
distributeDocument: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.describe('Whether to create the document as pending and distribute it to recipients.')
|
.describe('Whether to create the document as pending and distribute it to recipients.')
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export type FieldFormType = {
|
|||||||
pageY: number;
|
pageY: number;
|
||||||
pageWidth: number;
|
pageWidth: number;
|
||||||
pageHeight: number;
|
pageHeight: number;
|
||||||
|
recipientId: number;
|
||||||
signerEmail: string;
|
signerEmail: string;
|
||||||
fieldMeta?: FieldMeta;
|
fieldMeta?: FieldMeta;
|
||||||
};
|
};
|
||||||
@@ -143,6 +144,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
pageY: Number(field.positionY),
|
pageY: Number(field.positionY),
|
||||||
pageWidth: Number(field.width),
|
pageWidth: Number(field.width),
|
||||||
pageHeight: Number(field.height),
|
pageHeight: Number(field.height),
|
||||||
|
recipientId: field.recipientId,
|
||||||
signerEmail:
|
signerEmail:
|
||||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||||
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
||||||
@@ -348,6 +350,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
pageY,
|
pageY,
|
||||||
pageWidth: fieldPageWidth,
|
pageWidth: fieldPageWidth,
|
||||||
pageHeight: fieldPageHeight,
|
pageHeight: fieldPageHeight,
|
||||||
|
recipientId: selectedSigner.id,
|
||||||
signerEmail: selectedSigner.email,
|
signerEmail: selectedSigner.email,
|
||||||
fieldMeta: undefined,
|
fieldMeta: undefined,
|
||||||
};
|
};
|
||||||
@@ -441,6 +444,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||||
...structuredClone(lastActiveField),
|
...structuredClone(lastActiveField),
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
|
recipientId: selectedSigner?.id ?? lastActiveField.recipientId,
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
pageX: lastActiveField.pageX + 3,
|
pageX: lastActiveField.pageX + 3,
|
||||||
pageY: lastActiveField.pageY + 3,
|
pageY: lastActiveField.pageY + 3,
|
||||||
@@ -449,7 +453,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
append(newField);
|
append(newField);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedSigner?.email, toast],
|
[append, lastActiveField, selectedSigner?.id, selectedSigner?.email, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldPaste = useCallback(
|
const onFieldPaste = useCallback(
|
||||||
@@ -462,13 +466,15 @@ export const AddFieldsFormPartial = ({
|
|||||||
append({
|
append({
|
||||||
...copiedField,
|
...copiedField,
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
|
recipientId: selectedSigner?.id ?? copiedField.recipientId,
|
||||||
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
||||||
|
|
||||||
pageX: copiedField.pageX + 3,
|
pageX: copiedField.pageX + 3,
|
||||||
pageY: copiedField.pageY + 3,
|
pageY: copiedField.pageY + 3,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, fieldClipboard, selectedSigner?.email],
|
[append, fieldClipboard, selectedSigner?.id, selectedSigner?.email],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -567,7 +573,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
localFields.some(
|
localFields.some(
|
||||||
(field) =>
|
(field) =>
|
||||||
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
(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 &&
|
{isDocumentPdfLoaded &&
|
||||||
localFields.map((field, index) => {
|
localFields.map((field, index) => {
|
||||||
const recipientIndex = recipients.findIndex((r) => r.email === field.signerEmail);
|
const recipientIndex = recipients.findIndex((r) => r.id === field.recipientId);
|
||||||
const hasFieldError =
|
const hasFieldError =
|
||||||
emptyCheckboxFields.find((f) => f.formId === field.formId) ||
|
emptyCheckboxFields.find((f) => f.formId === field.formId) ||
|
||||||
emptyRadioFields.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}
|
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
|
||||||
field={field}
|
field={field}
|
||||||
disabled={
|
disabled={
|
||||||
selectedSigner?.email !== field.signerEmail ||
|
selectedSigner?.id !== field.recipientId ||
|
||||||
!canRecipientBeModified(selectedSigner, fields)
|
!canRecipientBeModified(selectedSigner, fields)
|
||||||
}
|
}
|
||||||
minHeight={MIN_HEIGHT_PX}
|
minHeight={MIN_HEIGHT_PX}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const ZAddFieldsFormSchema = z.object({
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
type: z.nativeEnum(FieldType),
|
type: z.nativeEnum(FieldType),
|
||||||
signerEmail: z.string().min(1),
|
signerEmail: z.string().min(1),
|
||||||
|
recipientId: z.number(),
|
||||||
pageNumber: z.number().min(1),
|
pageNumber: z.number().min(1),
|
||||||
pageX: z.number().min(0),
|
pageX: z.number().min(0),
|
||||||
pageY: z.number().min(0),
|
pageY: z.number().min(0),
|
||||||
|
|||||||
@@ -6,34 +6,22 @@ import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-a
|
|||||||
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
import { ZMapNegativeOneToUndefinedSchema } from './add-settings.types';
|
||||||
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
|
import { DocumentSigningOrder, RecipientRole } from '.prisma/client';
|
||||||
|
|
||||||
export const ZAddSignersFormSchema = z
|
export const ZAddSignersFormSchema = z.object({
|
||||||
.object({
|
signers: z.array(
|
||||||
signers: z.array(
|
z.object({
|
||||||
z.object({
|
formId: z.string().min(1),
|
||||||
formId: z.string().min(1),
|
nativeId: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z
|
||||||
email: z
|
.string()
|
||||||
.string()
|
.email({ message: msg`Invalid email`.id })
|
||||||
.email({ message: msg`Invalid email`.id })
|
.min(1),
|
||||||
.min(1),
|
name: z.string(),
|
||||||
name: z.string(),
|
role: z.nativeEnum(RecipientRole),
|
||||||
role: z.nativeEnum(RecipientRole),
|
signingOrder: z.number().optional(),
|
||||||
signingOrder: z.number().optional(),
|
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()),
|
||||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
}),
|
||||||
ZRecipientActionAuthTypesSchema.optional(),
|
),
|
||||||
),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
}),
|
});
|
||||||
),
|
|
||||||
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 type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;
|
||||||
|
|||||||
@@ -8,19 +8,14 @@ import { FieldType } from '@documenso/prisma/client';
|
|||||||
export const ZDocumentFlowFormSchema = z.object({
|
export const ZDocumentFlowFormSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
|
|
||||||
signers: z
|
signers: z.array(
|
||||||
.array(
|
z.object({
|
||||||
z.object({
|
formId: z.string().min(1),
|
||||||
formId: z.string().min(1),
|
nativeId: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z.string().min(1).email(),
|
||||||
email: z.string().min(1).email(),
|
name: z.string(),
|
||||||
name: z.string(),
|
}),
|
||||||
}),
|
),
|
||||||
)
|
|
||||||
.refine((signers) => {
|
|
||||||
const emails = signers.map((signer) => signer.email);
|
|
||||||
return new Set(emails).size === emails.length;
|
|
||||||
}, 'Signers must have unique emails'),
|
|
||||||
|
|
||||||
fields: z.array(
|
fields: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
|
|||||||
import { Checkbox } from '../checkbox';
|
import { Checkbox } from '../checkbox';
|
||||||
import type { FieldFormType } from '../document-flow/add-fields';
|
import type { FieldFormType } from '../document-flow/add-fields';
|
||||||
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
|
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 { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
|
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
|
||||||
@@ -110,6 +111,7 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
const [fieldClipboard, setFieldClipboard] = useState<
|
const [fieldClipboard, setFieldClipboard] = useState<
|
||||||
TAddTemplateFieldsFormSchema['fields'][0] | null
|
TAddTemplateFieldsFormSchema['fields'][0] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
||||||
|
|
||||||
const form = useForm<TAddTemplateFieldsFormSchema>({
|
const form = useForm<TAddTemplateFieldsFormSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -122,6 +124,7 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
pageY: Number(field.positionY),
|
pageY: Number(field.positionY),
|
||||||
pageWidth: Number(field.width),
|
pageWidth: Number(field.width),
|
||||||
pageHeight: Number(field.height),
|
pageHeight: Number(field.height),
|
||||||
|
recipientId: field.recipientId ?? -1,
|
||||||
signerId: field.recipientId ?? -1,
|
signerId: field.recipientId ?? -1,
|
||||||
signerEmail:
|
signerEmail:
|
||||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||||
@@ -177,6 +180,8 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
||||||
|
recipientId:
|
||||||
|
selectedSigner?.id || lastActiveField.recipientId || lastActiveField.signerId || 0,
|
||||||
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
|
||||||
pageX: lastActiveField.pageX + 3,
|
pageX: lastActiveField.pageX + 3,
|
||||||
pageY: lastActiveField.pageY + 3,
|
pageY: lastActiveField.pageY + 3,
|
||||||
@@ -201,19 +206,29 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const copiedField = structuredClone(fieldClipboard);
|
const copiedField = structuredClone(fieldClipboard);
|
||||||
|
const signerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
|
||||||
|
|
||||||
append({
|
append({
|
||||||
...copiedField,
|
...copiedField,
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
signerEmail: selectedSigner?.email ?? copiedField.signerEmail,
|
||||||
signerId: selectedSigner?.id ?? copiedField.signerId,
|
signerId: selectedSigner?.id ?? copiedField.signerId,
|
||||||
|
recipientId: selectedSigner?.id || copiedField.recipientId || copiedField.signerId || 0,
|
||||||
signerToken: selectedSigner?.token ?? copiedField.signerToken,
|
signerToken: selectedSigner?.token ?? copiedField.signerToken,
|
||||||
|
signerIndex: signerIndex >= 0 ? signerIndex : 0,
|
||||||
pageX: copiedField.pageX + 3,
|
pageX: copiedField.pageX + 3,
|
||||||
pageY: copiedField.pageY + 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));
|
useHotkeys(['ctrl+c', 'meta+c'], (evt) => onFieldCopy(evt));
|
||||||
@@ -319,6 +334,8 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
pageX -= fieldPageWidth / 2;
|
pageX -= fieldPageWidth / 2;
|
||||||
pageY -= fieldPageHeight / 2;
|
pageY -= fieldPageHeight / 2;
|
||||||
|
|
||||||
|
const signerIndex = recipients.findIndex((r) => r.id === selectedSigner.id);
|
||||||
|
|
||||||
append({
|
append({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
type: selectedField,
|
type: selectedField,
|
||||||
@@ -329,14 +346,17 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
pageHeight: fieldPageHeight,
|
pageHeight: fieldPageHeight,
|
||||||
signerEmail: selectedSigner.email,
|
signerEmail: selectedSigner.email,
|
||||||
signerId: selectedSigner.id,
|
signerId: selectedSigner.id,
|
||||||
|
recipientId:
|
||||||
|
selectedSigner.id || lastActiveField?.recipientId || lastActiveField?.signerId || 0,
|
||||||
signerToken: selectedSigner.token ?? '',
|
signerToken: selectedSigner.token ?? '',
|
||||||
|
signerIndex: signerIndex >= 0 ? signerIndex : 0,
|
||||||
fieldMeta: undefined,
|
fieldMeta: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsFieldWithinBounds(false);
|
setIsFieldWithinBounds(false);
|
||||||
setSelectedField(null);
|
setSelectedField(null);
|
||||||
},
|
},
|
||||||
[append, isWithinPageBounds, selectedField, selectedSigner, getPage],
|
[append, isWithinPageBounds, selectedField, selectedSigner, getPage, recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldResize = useCallback(
|
const onFieldResize = useCallback(
|
||||||
@@ -499,6 +519,23 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{showAdvancedSettings && currentField ? (
|
{showAdvancedSettings && currentField ? (
|
||||||
@@ -546,14 +583,15 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{localFields.map((field, index) => {
|
{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 (
|
return (
|
||||||
<FieldItem
|
<FieldItem
|
||||||
key={index}
|
key={index}
|
||||||
recipientIndex={recipientIndex === -1 ? 0 : recipientIndex}
|
recipientIndex={recipientIndex >= 0 ? recipientIndex : 0}
|
||||||
field={field}
|
field={field}
|
||||||
disabled={selectedSigner?.email !== field.signerEmail}
|
disabled={selectedSigner?.id !== field.signerId}
|
||||||
minHeight={MIN_HEIGHT_PX}
|
minHeight={MIN_HEIGHT_PX}
|
||||||
minWidth={MIN_WIDTH_PX}
|
minWidth={MIN_WIDTH_PX}
|
||||||
defaultHeight={DEFAULT_HEIGHT_PX}
|
defaultHeight={DEFAULT_HEIGHT_PX}
|
||||||
@@ -993,9 +1031,14 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
previousStep();
|
previousStep();
|
||||||
remove();
|
remove();
|
||||||
}}
|
}}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={handleGoNextClick}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|
||||||
|
<MissingSignatureFieldDialog
|
||||||
|
isOpen={isMissingSignatureDialogVisible}
|
||||||
|
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ export const ZAddTemplateFieldsFormSchema = z.object({
|
|||||||
type: z.nativeEnum(FieldType),
|
type: z.nativeEnum(FieldType),
|
||||||
signerEmail: z.string().min(1),
|
signerEmail: z.string().min(1),
|
||||||
signerToken: z.string(),
|
signerToken: z.string(),
|
||||||
signerId: z.number().optional(),
|
signerId: z.number(),
|
||||||
|
recipientId: z.number(),
|
||||||
pageNumber: z.number().min(1),
|
pageNumber: z.number().min(1),
|
||||||
pageX: z.number().min(0),
|
pageX: z.number().min(0),
|
||||||
pageY: z.number().min(0),
|
pageY: z.number().min(0),
|
||||||
pageWidth: z.number().min(0),
|
pageWidth: z.number().min(0),
|
||||||
pageHeight: z.number().min(0),
|
pageHeight: z.number().min(0),
|
||||||
|
signerIndex: z.number().min(0),
|
||||||
fieldMeta: ZFieldMetaSchema,
|
fieldMeta: ZFieldMetaSchema,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
actionAuth: undefined,
|
actionAuth: undefined,
|
||||||
...generateRecipientPlaceholder(1),
|
...generateRecipientPlaceholder(1),
|
||||||
signingOrder: 1,
|
signingOrder: 1,
|
||||||
|
signerIndex: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -104,6 +105,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
role: recipient.role,
|
role: recipient.role,
|
||||||
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined,
|
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? undefined,
|
||||||
signingOrder: recipient.signingOrder ?? index + 1,
|
signingOrder: recipient.signingOrder ?? index + 1,
|
||||||
|
signerIndex: index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
if (signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
@@ -174,21 +176,35 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onAddPlaceholderSelfRecipient = () => {
|
const onAddPlaceholderSelfRecipient = () => {
|
||||||
|
const currentSigners = form.getValues('signers');
|
||||||
|
const nextSignerIndex = currentSigners.length;
|
||||||
|
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
name: user?.name ?? '',
|
name: user?.name ?? '',
|
||||||
email: user?.email ?? '',
|
email: user?.email ?? '',
|
||||||
role: RecipientRole.SIGNER,
|
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 onAddPlaceholderRecipient = () => {
|
||||||
|
const currentSigners = form.getValues('signers');
|
||||||
|
const nextSignerIndex = currentSigners.length;
|
||||||
|
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
role: RecipientRole.SIGNER,
|
role: RecipientRole.SIGNER,
|
||||||
...generateRecipientPlaceholder(placeholderRecipientCount),
|
...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);
|
setPlaceholderRecipientCount((count) => count + 1);
|
||||||
|
|||||||
@@ -5,32 +5,21 @@ import { DocumentSigningOrder, RecipientRole } from '@documenso/prisma/client';
|
|||||||
|
|
||||||
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
import { ZMapNegativeOneToUndefinedSchema } from '../document-flow/add-settings.types';
|
||||||
|
|
||||||
export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
export const ZAddTemplatePlacholderRecipientsFormSchema = z.object({
|
||||||
.object({
|
signers: z.array(
|
||||||
signers: z.array(
|
z.object({
|
||||||
z.object({
|
formId: z.string().min(1),
|
||||||
formId: z.string().min(1),
|
nativeId: z.number().optional(),
|
||||||
nativeId: z.number().optional(),
|
email: z.string().min(1).email(),
|
||||||
email: z.string().min(1).email(),
|
name: z.string(),
|
||||||
name: z.string(),
|
role: z.nativeEnum(RecipientRole),
|
||||||
role: z.nativeEnum(RecipientRole),
|
signingOrder: z.number().optional(),
|
||||||
signingOrder: z.number().optional(),
|
signerIndex: z.number().min(0),
|
||||||
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(
|
actionAuth: ZMapNegativeOneToUndefinedSchema.pipe(ZRecipientActionAuthTypesSchema.optional()),
|
||||||
ZRecipientActionAuthTypesSchema.optional(),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||||
),
|
});
|
||||||
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 type TAddTemplatePlacholderRecipientsFormSchema = z.infer<
|
export type TAddTemplatePlacholderRecipientsFormSchema = z.infer<
|
||||||
typeof ZAddTemplatePlacholderRecipientsFormSchema
|
typeof ZAddTemplatePlacholderRecipientsFormSchema
|
||||||
|
|||||||
Reference in New Issue
Block a user