mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Compare commits
2 Commits
a08a77e98b
...
feat/add-o
| Author | SHA1 | Date | |
|---|---|---|---|
| 76e4807c67 | |||
| efd6531d3e |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@ -79,7 +79,7 @@
|
||||
},
|
||||
"apps/marketing": {
|
||||
"name": "@documenso/marketing",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/assets": "*",
|
||||
@ -492,7 +492,7 @@
|
||||
},
|
||||
"apps/web": {
|
||||
"name": "@documenso/web",
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.9.0-rc.1",
|
||||
"version": "1.9.0-rc.2",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"build:web": "turbo run build --filter=@documenso/web",
|
||||
|
||||
@ -35,6 +35,7 @@ import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/tem
|
||||
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
@ -637,69 +638,52 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id } = args.params;
|
||||
const { sendEmail = true } = args.body ?? {};
|
||||
|
||||
const document = await getDocumentById({
|
||||
documentId: Number(id),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already complete',
|
||||
},
|
||||
};
|
||||
}
|
||||
const { id: documentId } = args.params;
|
||||
const { sendEmail, sendCompletionEmails } = args.body;
|
||||
|
||||
try {
|
||||
// await setRecipientsForDocument({
|
||||
// userId: user.id,
|
||||
// documentId: Number(id),
|
||||
// recipients: [
|
||||
// {
|
||||
// email: body.signerEmail,
|
||||
// name: body.signerName ?? '',
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
const document = await getDocumentById({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
// await setFieldsForDocument({
|
||||
// documentId: Number(id),
|
||||
// userId: user.id,
|
||||
// fields: body.fields.map((field) => ({
|
||||
// signerEmail: body.signerEmail,
|
||||
// type: field.fieldType,
|
||||
// pageNumber: field.pageNumber,
|
||||
// pageX: field.pageX,
|
||||
// pageY: field.pageY,
|
||||
// pageWidth: field.pageWidth,
|
||||
// pageHeight: field.pageHeight,
|
||||
// })),
|
||||
// });
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// if (body.emailBody || body.emailSubject) {
|
||||
// await upsertDocumentMeta({
|
||||
// documentId: Number(id),
|
||||
// subject: body.emailSubject ?? '',
|
||||
// message: body.emailBody ?? '',
|
||||
// });
|
||||
// }
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already complete',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
|
||||
// Update document email settings if sendCompletionEmails is provided
|
||||
if (typeof sendCompletionEmails === 'boolean') {
|
||||
await upsertDocumentMeta({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
emailSettings: {
|
||||
...emailSettings,
|
||||
documentCompleted: sendCompletionEmails,
|
||||
ownerDocumentCompleted: sendCompletionEmails,
|
||||
},
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
const { Recipient: recipients, ...sentDocument } = await sendDocument({
|
||||
documentId: Number(id),
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
sendEmail,
|
||||
|
||||
@ -88,8 +88,12 @@ export const ZSendDocumentForSigningMutationSchema = z
|
||||
description:
|
||||
'Whether to send an email to the recipients asking them to action the document. If you disable this, you will need to manually distribute the document to the recipients using the generated signing links.',
|
||||
}),
|
||||
sendCompletionEmails: z.boolean().optional().openapi({
|
||||
description:
|
||||
'Whether to send completion emails when the document is fully signed. This will override the document email settings.',
|
||||
}),
|
||||
})
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true })));
|
||||
.or(z.literal('').transform(() => ({ sendEmail: true, sendCompletionEmails: undefined })));
|
||||
|
||||
export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema;
|
||||
|
||||
|
||||
137
packages/app-tests/e2e/api/v1/document-sending.spec.ts
Normal file
137
packages/app-tests/e2e/api/v1/document-sending.spec.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
test.describe('Document API', () => {
|
||||
test('sendDocument: should respect sendCompletionEmails setting', async ({ request }) => {
|
||||
const user = await seedUser();
|
||||
|
||||
const { document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: ['signer@example.com'],
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
// Test with sendCompletionEmails: false
|
||||
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendCompletionEmails: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
// Verify email settings were updated
|
||||
const updatedDocument = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument?.documentMeta?.emailSettings).toMatchObject({
|
||||
documentCompleted: false,
|
||||
ownerDocumentCompleted: false,
|
||||
});
|
||||
|
||||
// Test with sendCompletionEmails: true
|
||||
const response2 = await request.post(
|
||||
`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendCompletionEmails: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(response2.ok()).toBeTruthy();
|
||||
expect(response2.status()).toBe(200);
|
||||
|
||||
// Verify email settings were updated
|
||||
const updatedDocument2 = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument2?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('sendDocument: should not modify email settings when sendCompletionEmails is not provided', async ({
|
||||
request,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
|
||||
const { document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: ['signer@example.com'],
|
||||
});
|
||||
|
||||
// Set initial email settings
|
||||
await prisma.documentMeta.upsert({
|
||||
where: { documentId: document.id },
|
||||
create: {
|
||||
documentId: document.id,
|
||||
emailSettings: {
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
documentId: document.id,
|
||||
emailSettings: {
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { token } = await createApiToken({
|
||||
userId: user.id,
|
||||
tokenName: 'test',
|
||||
expiresIn: null,
|
||||
});
|
||||
|
||||
const response = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${document.id}/send`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
sendEmail: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
// Verify email settings were not modified
|
||||
const updatedDocument = await prisma.document.findUnique({
|
||||
where: { id: document.id },
|
||||
include: { documentMeta: true },
|
||||
});
|
||||
|
||||
expect(updatedDocument?.documentMeta?.emailSettings ?? {}).toMatchObject({
|
||||
documentCompleted: true,
|
||||
ownerDocumentCompleted: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -72,14 +72,19 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
||||
|
||||
const i18n = await getI18nInstance(document.documentMeta?.language);
|
||||
|
||||
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
document.documentMeta,
|
||||
).documentCompleted;
|
||||
const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta);
|
||||
const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted;
|
||||
const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted;
|
||||
|
||||
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
|
||||
// Send email to document owner if:
|
||||
// 1. Owner document completed emails are enabled AND
|
||||
// 2. Either:
|
||||
// - The owner is not a recipient, OR
|
||||
// - Recipient emails are disabled
|
||||
if (
|
||||
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled
|
||||
isOwnerDocumentCompletedEmailEnabled &&
|
||||
(!document.Recipient.find((recipient) => recipient.email === owner.email) ||
|
||||
!isDocumentCompletedEmailEnabled)
|
||||
) {
|
||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||
documentName: document.title,
|
||||
|
||||
@ -9,6 +9,7 @@ export enum DocumentEmailEvents {
|
||||
DocumentPending = 'documentPending',
|
||||
DocumentCompleted = 'documentCompleted',
|
||||
DocumentDeleted = 'documentDeleted',
|
||||
OwnerDocumentCompleted = 'ownerDocumentCompleted',
|
||||
}
|
||||
|
||||
export const ZDocumentEmailSettingsSchema = z
|
||||
@ -18,6 +19,7 @@ export const ZDocumentEmailSettingsSchema = z
|
||||
documentPending: z.boolean().default(true),
|
||||
documentCompleted: z.boolean().default(true),
|
||||
documentDeleted: z.boolean().default(true),
|
||||
ownerDocumentCompleted: z.boolean().default(true),
|
||||
})
|
||||
.strip()
|
||||
.catch(() => ({
|
||||
@ -26,6 +28,7 @@ export const ZDocumentEmailSettingsSchema = z
|
||||
documentPending: true,
|
||||
documentCompleted: true,
|
||||
documentDeleted: true,
|
||||
ownerDocumentCompleted: true,
|
||||
}));
|
||||
|
||||
export type TDocumentEmailSettings = z.infer<typeof ZDocumentEmailSettingsSchema>;
|
||||
@ -48,5 +51,6 @@ export const extractDerivedDocumentEmailSettings = (
|
||||
documentPending: false,
|
||||
documentCompleted: false,
|
||||
documentDeleted: false,
|
||||
ownerDocumentCompleted: emailSettings.ownerDocumentCompleted,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import type { TDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
||||
import { DocumentEmailEvents } from '@documenso/lib/types/document-email';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Checkbox } from '../../primitives/checkbox';
|
||||
|
||||
type Value = Record<DocumentEmailEvents, boolean>;
|
||||
type Value = TDocumentEmailSettings;
|
||||
|
||||
type DocumentEmailCheckboxesProps = {
|
||||
value: Value;
|
||||
@ -217,6 +218,46 @@ export const DocumentEmailCheckboxes = ({
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center">
|
||||
<Checkbox
|
||||
id={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
className="h-5 w-5"
|
||||
checkClassName="dark:text-white text-primary"
|
||||
checked={value.ownerDocumentCompleted}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...value, [DocumentEmailEvents.OwnerDocumentCompleted]: Boolean(checked) })
|
||||
}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="text-muted-foreground ml-2 flex flex-row items-center text-sm"
|
||||
htmlFor={DocumentEmailEvents.OwnerDocumentCompleted}
|
||||
>
|
||||
<Trans>Send document completed email to the owner</Trans>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
|
||||
<h2>
|
||||
<strong>
|
||||
<Trans>Document completed email to the owner</Trans>
|
||||
</strong>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<Trans>
|
||||
This will be sent to the document owner once the document has been fully
|
||||
completed.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user