Compare commits

...

22 Commits

Author SHA1 Message Date
05197993fa Revert changes to pdf.worker.min.js to match main branch 2024-05-29 15:21:32 +00:00
04cd5b58c2 fix: pdf worker changes 2024-05-29 15:19:49 +00:00
f7e9d1b3cb fix: revert changes on pdf worker 2024-05-29 15:16:58 +00:00
ca077dd3a6 fix: restore pdf worker changes 2024-05-29 15:02:35 +00:00
8e9287a7c1 fix: update health check endpoint 2024-05-29 13:45:53 +00:00
93e816a5b4 fix: update start command 2024-05-29 13:08:05 +00:00
d3734ff344 fix: add new environmental variables for render deployment 2024-05-29 11:27:58 +00:00
0c18f27b3f feat: remove the existing empty signer if its the only one (#1127)
Removes the existing empty signer if its the only one
2024-05-28 14:15:01 +10:00
b394e99f7a Merge branch 'main' into feat/start-selfSign 2024-05-28 12:53:57 +10:00
c21e30d689 chore: tidy code 2024-05-28 02:45:57 +00:00
9b92e38c52 chore: add more tests (#1079) 2024-05-27 11:17:03 +07:00
ac41086e1a Merge branch 'main' into feat/start-selfSign 2024-05-24 23:48:20 +10:00
98672560ca chore: update self signer logic
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-05-08 17:02:01 +05:30
6f6ed05569 Merge branch 'feat/start-selfSign' of https://github.com/documenso/documenso into feat/start-selfSign 2024-05-08 15:35:43 +05:30
5e3f55c616 Merge branch 'main' of https://github.com/documenso/documenso into feat/start-selfSign 2024-05-08 15:34:18 +05:30
55d8afe870 Merge branch 'main' into feat/start-selfSign 2024-05-06 11:37:16 +05:30
6df525b670 feat: updated signer logic
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-04-30 12:05:42 +05:30
db9e605031 chore: fix lint issues
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2024-04-30 01:32:58 +05:30
bde0f5893f feat: update add self signer logic 2024-04-29 17:49:50 +05:30
6b5750c7bf chore: revert previous changes 2024-04-29 17:48:00 +05:30
917c83fc5f chore: refactor removal logic 2024-04-29 17:30:01 +05:30
e82e402540 feat: remove the existing empty signer if its the only one 2024-04-29 17:10:56 +05:30
4 changed files with 291 additions and 30 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,14 @@
import { expect, test } from '@playwright/test';
import { DateTime } from 'luxon';
import path from 'node:path';
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
import {
seedBlankDocument,
seedPendingDocumentWithFullFields,
} from '@documenso/prisma/seed/documents';
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
@ -192,6 +196,102 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await unseedUser(user.id);
});
test('[DOCUMENT_FLOW]: should be able to create a document with multiple 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`,
});
// Set title
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.getByLabel('Title').fill('Test Title');
await page.getByRole('button', { name: 'Continue' }).click();
// Add signers
await expect(page.getByRole('heading', { name: 'Add Signers' })).toBeVisible();
// Add 2 signers.
await page.getByPlaceholder('Email').fill('user1@example.com');
await page.getByPlaceholder('Name').fill('User 1');
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).fill('user2@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(1).fill('User 2');
await page.locator('button[role="combobox"]').nth(1).click();
await page.getByLabel('Receives copy').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(1).fill('user3@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(2).fill('User 3');
await page.locator('button[role="combobox"]').nth(2).click();
await page.getByLabel('Needs to approve').click();
await page.getByRole('button', { name: 'Add Signer' }).click();
await page.getByRole('textbox', { name: 'Email', exact: true }).nth(2).fill('user4@example.com');
await page.getByRole('textbox', { name: 'Name', exact: true }).nth(3).fill('User 4');
await page.locator('button[role="combobox"]').nth(3).click();
await page.getByLabel('Needs to view').click();
await page.getByRole('button', { name: 'Continue' }).click();
// Add fields
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'User 1 Signature' }).click();
await page.locator('canvas').click({
position: {
x: 100,
y: 100,
},
});
await page.getByRole('button', { name: 'Email Email' }).click();
await page.locator('canvas').click({
position: {
x: 100,
y: 200,
},
});
await page.getByText('User 1 (user1@example.com)').click();
await page.getByText('User 3 (user3@example.com)').click();
await page.getByRole('button', { name: 'User 3 Signature' }).click();
await page.locator('canvas').click({
position: {
x: 500,
y: 100,
},
});
await page.getByRole('button', { name: 'Email Email' }).click();
await page.locator('canvas').click({
position: {
x: 500,
y: 200,
},
});
await page.getByRole('button', { name: 'Continue' }).click();
// Add subject and send
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
await page.getByRole('button', { name: 'Send' }).click();
await page.waitForURL('/documents');
// Assert document was created
await expect(page.getByRole('link', { name: 'Test Title' })).toBeVisible();
await unseedUser(user.id);
});
test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', async ({ page }) => {
const user = await seedUser();
const document = await seedBlankDocument(user);
@ -234,6 +334,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', asyn
await page.getByRole('link', { name: documentTitle }).click();
await page.waitForURL(/\/documents\/\d+/);
// Start signing process
const url = page.url().split('/');
const documentId = url[url.length - 1];
@ -263,6 +364,63 @@ test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', asyn
await unseedUser(user.id);
});
test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) => {
const user = await seedUser();
const { recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['user@documenso.com', 'approver@documenso.com'],
recipientsCreateOptions: [
{
email: 'user@documenso.com',
role: RecipientRole.SIGNER,
},
{
email: 'approver@documenso.com',
role: RecipientRole.APPROVER,
},
],
fields: [FieldType.SIGNATURE],
});
for (const recipient of recipients) {
const { token, Field, role } = recipient;
const signUrl = `/sign/${token}`;
await page.goto(signUrl);
await expect(
page.getByRole('heading', {
name: role === RecipientRole.SIGNER ? 'Sign Document' : 'Approve Document',
}),
).toBeVisible();
// Add signature.
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
for (const field of Field) {
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page.getByRole('button', { name: 'Complete' }).click();
await page
.getByRole('button', { name: role === RecipientRole.SIGNER ? 'Sign' : 'Approve' })
.click();
await page.waitForURL(`${signUrl}/complete`);
}
await unseedUser(user.id);
});
test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a document and redirect to redirect url', async ({
page,
}) => {
@ -333,3 +491,46 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
await unseedUser(user.id);
});
test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', async ({ page }) => {
const user = await seedUser();
const customDate = DateTime.local().toFormat('yyyy-MM-dd hh:mm a');
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['user1@example.com'],
fields: [FieldType.DATE],
});
const { token, Field } = recipients[0];
const [recipientField] = Field;
await page.goto(`/sign/${token}`);
await page.waitForURL(`/sign/${token}`);
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByRole('dialog').getByText('Complete Signing').first()).toBeVisible();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${token}/complete`);
await expect(page.getByText('Document Signed')).toBeVisible();
const field = await prisma.field.findFirst({
where: {
Recipient: {
email: 'user1@example.com',
},
documentId: Number(document.id),
},
});
expect(field?.customText).toBe(customDate);
// Check if document has been signed
const { status: completedStatus } = await getDocumentByToken(token);
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
await unseedUser(user.id);
});

View File

@ -105,10 +105,14 @@ export const AddSignersFormPartial = ({
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings);
const {
setValue,
formState: { errors, isSubmitting },
control,
watch,
} = form;
const watchedSigners = watch('signers');
const onFormSubmit = form.handleSubmit(onSubmit);
const {
@ -120,6 +124,11 @@ export const AddSignersFormPartial = ({
name: 'signers',
});
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
const isUserAlreadyARecipient = watchedSigners.some(
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
);
const hasBeenSentToRecipientId = (id?: number) => {
if (!id) {
return false;
@ -133,16 +142,6 @@ export const AddSignersFormPartial = ({
);
};
const onAddSelfSigner = () => {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
actionAuth: undefined,
});
};
const onAddSigner = () => {
appendSigner({
formId: nanoid(12),
@ -169,6 +168,21 @@ export const AddSignersFormPartial = ({
removeSigner(index);
};
const onAddSelfSigner = () => {
if (emptySignerIndex !== -1) {
setValue(`signers.${emptySignerIndex}.name`, user?.name ?? '');
setValue(`signers.${emptySignerIndex}.email`, user?.email ?? '');
} else {
appendSigner({
formId: nanoid(12),
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
actionAuth: undefined,
});
}
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
onAddSigner();
@ -218,11 +232,7 @@ export const AddSignersFormPartial = ({
type="email"
placeholder="Email"
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
onKeyDown={onKeyDown}
/>
</FormControl>
@ -248,11 +258,7 @@ export const AddSignersFormPartial = ({
<Input
placeholder="Name"
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signers[index].email === user?.email
}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
onKeyDown={onKeyDown}
/>
</FormControl>
@ -335,14 +341,12 @@ export const AddSignersFormPartial = ({
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Signer
</Button>
<Button
type="button"
variant="secondary"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
disabled={
isSubmitting ||
form.getValues('signers').some((signer) => signer.email === user?.email)
}
disabled={isSubmitting || isUserAlreadyARecipient}
onClick={() => onAddSelfSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />

View File

@ -1,11 +1,11 @@
services:
- type: web
runtime: node
name: documenso-app
env: node
plan: free
buildCommand: npm i && npm run build:web
startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npm run start
healthCheckPath: /api/trpc/health
startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npx turbo run start --filter=@documenso/web
healthCheckPath: /api/health
envVars:
# Node Version
@ -98,6 +98,62 @@ services:
- key: NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY
sync: false
# Crypto
- key: NEXT_PRIVATE_ENCRYPTION_KEY
generateValue: true
- key: NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
generateValue: true
# Auth Optional
- key: NEXT_PRIVATE_GOOGLE_CLIENT_ID
sync: false
- key: NEXT_PRIVATE_GOOGLE_CLIENT_SECRET
sync: false
# Signing
- key: NEXT_PRIVATE_SIGNING_TRANSPORT
sync: false
- key: NEXT_PRIVATE_SIGNING_PASSPHRASE
sync: false
- key: NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH
sync: false
- key: NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS
sync: false
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_KEY_PATH
sync: false
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH
sync: false
- key: NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS
sync: false
- key: NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS
sync: false
# SMTP Optional
- key: NEXT_PRIVATE_SMTP_APIKEY_USER
sync: false
- key: NEXT_PRIVATE_SMTP_APIKEY
sync: false
- key: NEXT_PRIVATE_SMTP_SECURE
sync: false
- key: NEXT_PRIVATE_RESEND_API_KEY
sync: false
- key: NEXT_PRIVATE_MAILCHANNELS_API_KEY
sync: false
- key: NEXT_PRIVATE_MAILCHANNELS_ENDPOINT
sync: false
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN
sync: false
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_SELECTOR
sync: false
- key: NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY
sync: false
- key: NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT
sync: false
# Features Optional
- key: NEXT_PUBLIC_DISABLE_SIGNUP
sync: false
databases:
- name: documenso-db
plan: free